diff --git a/README.md b/README.md index 1ce060d..e47f490 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,10 @@ - **Tailwind CSS** + **shadcn/ui** for modern UI components - **SWR** for efficient data fetching and caching - **Drizzle ORM** with PostgreSQL for robust data persistence +- **Internationalization (i18n)** - Multi-language support with next-intl + - English (default) + - Chinese Simplified (简体中文) + - Chinese Traditional (繁體中文) ### 🤖 Advanced AI Capabilities @@ -418,7 +422,9 @@ For commercial licensing inquiries, please contact us through GitHub Issues. - **Issues**: [GitHub Issues](https://github.com/rxtech-lab/rxchat-web/issues) - **Discussions**: [GitHub Discussions](https://github.com/rxtech-lab/rxchat-web/discussions) -- **Documentation**: See component READMEs in `components/` directories +- **Documentation**: + - See component READMEs in `components/` directories + - [Internationalization Guide](./docs/i18n.md) - Multi-language support documentation ## Acknowledgments diff --git a/app/(auth)/api/auth/[...nextauth]/route.ts b/app/(auth)/api/auth/[...nextauth]/route.ts deleted file mode 100644 index ba24234..0000000 --- a/app/(auth)/api/auth/[...nextauth]/route.ts +++ /dev/null @@ -1 +0,0 @@ -export { GET, POST } from '@/app/(auth)/auth'; diff --git a/app/(auth)/actions.ts b/app/[locale]/(auth)/actions.ts similarity index 100% rename from app/(auth)/actions.ts rename to app/[locale]/(auth)/actions.ts diff --git a/app/[locale]/(auth)/api/auth/[...nextauth]/route.ts b/app/[locale]/(auth)/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..9df29b5 --- /dev/null +++ b/app/[locale]/(auth)/api/auth/[...nextauth]/route.ts @@ -0,0 +1 @@ +export { GET, POST } from '@/app/[locale]/(auth)/auth'; diff --git a/app/(auth)/api/auth/guest/route.ts b/app/[locale]/(auth)/api/auth/guest/route.ts similarity index 91% rename from app/(auth)/api/auth/guest/route.ts rename to app/[locale]/(auth)/api/auth/guest/route.ts index 25af1fa..de6a56f 100644 --- a/app/(auth)/api/auth/guest/route.ts +++ b/app/[locale]/(auth)/api/auth/guest/route.ts @@ -1,4 +1,4 @@ -import { signIn } from '@/app/(auth)/auth'; +import { signIn } from '@/app/[locale]/(auth)/auth'; import { isDevelopmentEnvironment } from '@/lib/constants'; import { getToken } from 'next-auth/jwt'; import { NextResponse } from 'next/server'; diff --git a/app/(auth)/api/auth/link/telegram/actions.ts b/app/[locale]/(auth)/api/auth/link/telegram/actions.ts similarity index 97% rename from app/(auth)/api/auth/link/telegram/actions.ts rename to app/[locale]/(auth)/api/auth/link/telegram/actions.ts index 31afac0..74d40c8 100644 --- a/app/(auth)/api/auth/link/telegram/actions.ts +++ b/app/[locale]/(auth)/api/auth/link/telegram/actions.ts @@ -1,6 +1,6 @@ 'use server'; -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import { getUserTelegramLink, unlinkTelegramFromUser, diff --git a/app/(auth)/api/auth/link/telegram/route.ts b/app/[locale]/(auth)/api/auth/link/telegram/route.ts similarity index 100% rename from app/(auth)/api/auth/link/telegram/route.ts rename to app/[locale]/(auth)/api/auth/link/telegram/route.ts diff --git a/app/(auth)/auth.config.ts b/app/[locale]/(auth)/auth.config.ts similarity index 100% rename from app/(auth)/auth.config.ts rename to app/[locale]/(auth)/auth.config.ts diff --git a/app/(auth)/auth.ts b/app/[locale]/(auth)/auth.ts similarity index 100% rename from app/(auth)/auth.ts rename to app/[locale]/(auth)/auth.ts diff --git a/app/(auth)/login/page.tsx b/app/[locale]/(auth)/login/page.tsx similarity index 81% rename from app/(auth)/login/page.tsx rename to app/[locale]/(auth)/login/page.tsx index 87f9f06..ed171f5 100644 --- a/app/(auth)/login/page.tsx +++ b/app/[locale]/(auth)/login/page.tsx @@ -1,6 +1,5 @@ 'use client'; -import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { useActionState, useEffect, useState } from 'react'; import { toast } from '@/components/toast'; @@ -10,9 +9,13 @@ import { SubmitButton } from '@/components/submit-button'; import { login, type LoginActionState } from '../actions'; import { useSession } from 'next-auth/react'; +import { useTranslations } from 'next-intl'; +import { Link } from '@/lib/i18n/routing'; export default function Page() { const router = useRouter(); + // Use translations from the auth namespace + const t = useTranslations('auth'); const [email, setEmail] = useState(''); const [isSuccessful, setIsSuccessful] = useState(false); @@ -31,12 +34,12 @@ export default function Page() { if (state.status === 'failed') { toast({ type: 'error', - description: 'Invalid credentials!', + description: t('invalidCredentials'), }); } else if (state.status === 'invalid_data') { toast({ type: 'error', - description: 'Failed validating your submission!', + description: t('invalidData'), }); } else if (state.status === 'success') { setIsSuccessful(true); @@ -55,20 +58,22 @@ export default function Page() {
-

Sign In

+

+ {t('signIn')} +

- {"Don't have an account? "} + {t('noAccount')} - Sign up + {t('signUp')} - {' for free.'} + {t('forFree')}

- Sign in + {t('signIn')}
diff --git a/app/(auth)/register/page.tsx b/app/[locale]/(auth)/register/page.tsx similarity index 76% rename from app/(auth)/register/page.tsx rename to app/[locale]/(auth)/register/page.tsx index 69bd361..d7b38f5 100644 --- a/app/(auth)/register/page.tsx +++ b/app/[locale]/(auth)/register/page.tsx @@ -1,6 +1,5 @@ 'use client'; -import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { useActionState, useEffect, useState } from 'react'; @@ -10,9 +9,13 @@ import { SubmitButton } from '@/components/submit-button'; import { register, type RegisterActionState } from '../actions'; import { toast } from '@/components/toast'; import { useSession } from 'next-auth/react'; +import { useTranslations } from 'next-intl'; +import { Link } from '@/lib/i18n/routing'; export default function Page() { const router = useRouter(); + // Use translations from the auth namespace + const t = useTranslations('auth'); const [email, setEmail] = useState(''); const [isSuccessful, setIsSuccessful] = useState(false); @@ -28,21 +31,21 @@ export default function Page() { useEffect(() => { if (state.status === 'user_exists') { - toast({ type: 'error', description: 'Account already exists!' }); + toast({ type: 'error', description: t('userExists') }); } else if (state.status === 'failed') { - toast({ type: 'error', description: 'Failed to create account!' }); + toast({ type: 'error', description: t('failedToCreate') }); } else if (state.status === 'invalid_data') { toast({ type: 'error', - description: 'Failed validating your submission!', + description: t('invalidData'), }); } else if (state.status === 'passwords_dont_match') { toast({ type: 'error', - description: 'Passwords do not match!', + description: t('passwordsDontMatch'), }); } else if (state.status === 'success') { - toast({ type: 'success', description: 'Account created successfully!' }); + toast({ type: 'success', description: t('accountCreated') }); setIsSuccessful(true); updateSession(); router.refresh(); @@ -59,29 +62,31 @@ export default function Page() {
-

Sign Up

+

+ {t('signUp')} +

- {'Already have an account? '} + {t('hasAccount')} - Sign in + {t('signIn')} - {' instead.'} + {t('instead')}

- Sign Up + {t('signUp')}

- {'Already have an account? '} + {t('hasAccount')} - Sign in + {t('signIn')} - {' instead.'} + {t('instead')}

diff --git a/app/(chat)/actions-mcp.ts b/app/[locale]/(chat)/actions-mcp.ts similarity index 96% rename from app/(chat)/actions-mcp.ts rename to app/[locale]/(chat)/actions-mcp.ts index f5e6330..b5daf8d 100644 --- a/app/(chat)/actions-mcp.ts +++ b/app/[locale]/(chat)/actions-mcp.ts @@ -1,6 +1,6 @@ 'use server'; -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import { getMCPRouterClient } from '@/lib/api/mcp-router/api'; import type { Tool } from '@/lib/api/mcp-router/client'; diff --git a/app/(chat)/actions.spec.ts b/app/[locale]/(chat)/actions.spec.ts similarity index 99% rename from app/(chat)/actions.spec.ts rename to app/[locale]/(chat)/actions.spec.ts index 2739fd2..a5cc054 100644 --- a/app/(chat)/actions.spec.ts +++ b/app/[locale]/(chat)/actions.spec.ts @@ -62,7 +62,7 @@ jest.mock('next/headers', () => ({ cookies: jest.fn(), })); -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import { createPromptRunner } from '@/lib/agent/prompt-runner/runner'; import { createMCPClient } from '@/lib/ai/mcp'; import { db } from '@/lib/db/queries/client'; diff --git a/app/(chat)/actions.ts b/app/[locale]/(chat)/actions.ts similarity index 100% rename from app/(chat)/actions.ts rename to app/[locale]/(chat)/actions.ts diff --git a/app/(chat)/api/chat/route.ts b/app/[locale]/(chat)/api/chat/route.ts similarity index 99% rename from app/(chat)/api/chat/route.ts rename to app/[locale]/(chat)/api/chat/route.ts index 3848fe0..9286c08 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/[locale]/(chat)/api/chat/route.ts @@ -1,4 +1,4 @@ -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import { createPromptRunner } from '@/lib/agent/prompt-runner/runner'; import { entitlementsByUserRole } from '@/lib/ai/entitlements'; import { createMCPClient } from '@/lib/ai/mcp'; diff --git a/app/(chat)/api/chat/schema.ts b/app/[locale]/(chat)/api/chat/schema.ts similarity index 100% rename from app/(chat)/api/chat/schema.ts rename to app/[locale]/(chat)/api/chat/schema.ts diff --git a/app/(chat)/api/document/route.ts b/app/[locale]/(chat)/api/document/route.ts similarity index 98% rename from app/(chat)/api/document/route.ts rename to app/[locale]/(chat)/api/document/route.ts index af13ba4..7ea7d09 100644 --- a/app/(chat)/api/document/route.ts +++ b/app/[locale]/(chat)/api/document/route.ts @@ -1,4 +1,4 @@ -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import type { ArtifactKind } from '@/components/artifact'; import { deleteDocumentsByIdAfterTimestamp, diff --git a/app/(chat)/api/documents/[id]/download/route.ts b/app/[locale]/(chat)/api/documents/[id]/download/route.ts similarity index 96% rename from app/(chat)/api/documents/[id]/download/route.ts rename to app/[locale]/(chat)/api/documents/[id]/download/route.ts index 6363cc4..f6938dc 100644 --- a/app/(chat)/api/documents/[id]/download/route.ts +++ b/app/[locale]/(chat)/api/documents/[id]/download/route.ts @@ -1,4 +1,4 @@ -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import { getPresignedDownloadUrl } from '@/lib/document/actions/action_server'; import { ChatSDKError } from '@/lib/errors'; import { z } from 'zod'; diff --git a/app/(chat)/api/documents/[id]/route.ts b/app/[locale]/(chat)/api/documents/[id]/route.ts similarity index 98% rename from app/(chat)/api/documents/[id]/route.ts rename to app/[locale]/(chat)/api/documents/[id]/route.ts index ba7de79..a5c43a8 100644 --- a/app/(chat)/api/documents/[id]/route.ts +++ b/app/[locale]/(chat)/api/documents/[id]/route.ts @@ -1,4 +1,4 @@ -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import { deleteDocument, renameDocument, diff --git a/app/(chat)/api/documents/complete-upload/route.ts b/app/[locale]/(chat)/api/documents/complete-upload/route.ts similarity index 97% rename from app/(chat)/api/documents/complete-upload/route.ts rename to app/[locale]/(chat)/api/documents/complete-upload/route.ts index a979c33..83b6d3d 100644 --- a/app/(chat)/api/documents/complete-upload/route.ts +++ b/app/[locale]/(chat)/api/documents/complete-upload/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from 'next/server'; import { z } from 'zod'; -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import { completeDocumentUpload } from '@/lib/document/actions/action_server'; const CompleteUploadSchema = z.object({ diff --git a/app/(chat)/api/documents/route.ts b/app/[locale]/(chat)/api/documents/route.ts similarity index 98% rename from app/(chat)/api/documents/route.ts rename to app/[locale]/(chat)/api/documents/route.ts index 00defeb..372d2c1 100644 --- a/app/(chat)/api/documents/route.ts +++ b/app/[locale]/(chat)/api/documents/route.ts @@ -1,4 +1,4 @@ -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import { listDocuments, searchDocuments, diff --git a/app/(chat)/api/files/upload/route.ts b/app/[locale]/(chat)/api/files/upload/route.ts similarity index 98% rename from app/(chat)/api/files/upload/route.ts rename to app/[locale]/(chat)/api/files/upload/route.ts index eb354e7..1502b1b 100644 --- a/app/(chat)/api/files/upload/route.ts +++ b/app/[locale]/(chat)/api/files/upload/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from 'next/server'; import { z } from 'zod'; -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import { createS3Client } from '@/lib/s3'; import { isImageType, diff --git a/app/(chat)/api/history/route.ts b/app/[locale]/(chat)/api/history/route.ts similarity index 94% rename from app/(chat)/api/history/route.ts rename to app/[locale]/(chat)/api/history/route.ts index ab785dc..84aa287 100644 --- a/app/(chat)/api/history/route.ts +++ b/app/[locale]/(chat)/api/history/route.ts @@ -1,4 +1,4 @@ -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import type { NextRequest } from 'next/server'; import { getChatsByUserId } from '@/lib/db/queries/queries'; import { ChatSDKError } from '@/lib/errors'; diff --git a/app/(chat)/api/prompts/route.spec.ts b/app/[locale]/(chat)/api/prompts/route.spec.ts similarity index 99% rename from app/(chat)/api/prompts/route.spec.ts rename to app/[locale]/(chat)/api/prompts/route.spec.ts index 198ef5b..2581758 100644 --- a/app/(chat)/api/prompts/route.spec.ts +++ b/app/[locale]/(chat)/api/prompts/route.spec.ts @@ -14,7 +14,7 @@ jest.mock('@/app/(auth)/auth', () => ({ })); import { PATCH } from './route'; -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import { createUser, deleteUserAccount } from '@/lib/db/queries/queries'; import { createPrompt } from '@/lib/db/queries/prompts'; import { generateRandomTestUser } from '@/tests/helpers'; diff --git a/app/(chat)/api/prompts/route.ts b/app/[locale]/(chat)/api/prompts/route.ts similarity index 98% rename from app/(chat)/api/prompts/route.ts rename to app/[locale]/(chat)/api/prompts/route.ts index 34c8b76..8e099b1 100644 --- a/app/(chat)/api/prompts/route.ts +++ b/app/[locale]/(chat)/api/prompts/route.ts @@ -1,4 +1,4 @@ -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import { getPromptsByUserId, createPrompt, diff --git a/app/(chat)/api/suggestions/route.ts b/app/[locale]/(chat)/api/suggestions/route.ts similarity index 94% rename from app/(chat)/api/suggestions/route.ts rename to app/[locale]/(chat)/api/suggestions/route.ts index ade31da..91af4c9 100644 --- a/app/(chat)/api/suggestions/route.ts +++ b/app/[locale]/(chat)/api/suggestions/route.ts @@ -1,4 +1,4 @@ -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import { getSuggestionsByDocumentId } from '@/lib/db/queries/queries'; import { ChatSDKError } from '@/lib/errors'; diff --git a/app/(chat)/api/user/route.ts b/app/[locale]/(chat)/api/user/route.ts similarity index 86% rename from app/(chat)/api/user/route.ts rename to app/[locale]/(chat)/api/user/route.ts index cca9145..9ee6960 100644 --- a/app/(chat)/api/user/route.ts +++ b/app/[locale]/(chat)/api/user/route.ts @@ -1,4 +1,4 @@ -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import { NextResponse } from 'next/server'; export async function GET() { diff --git a/app/(chat)/api/vote/route.ts b/app/[locale]/(chat)/api/vote/route.ts similarity index 97% rename from app/(chat)/api/vote/route.ts rename to app/[locale]/(chat)/api/vote/route.ts index d1c550d..bd80656 100644 --- a/app/(chat)/api/vote/route.ts +++ b/app/[locale]/(chat)/api/vote/route.ts @@ -1,4 +1,4 @@ -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import { getChatById, getVotesByChatId, diff --git a/app/(chat)/chat/[id]/page.tsx b/app/[locale]/(chat)/chat/[id]/page.tsx similarity index 99% rename from app/(chat)/chat/[id]/page.tsx rename to app/[locale]/(chat)/chat/[id]/page.tsx index 6a4193d..36b5fd8 100644 --- a/app/(chat)/chat/[id]/page.tsx +++ b/app/[locale]/(chat)/chat/[id]/page.tsx @@ -1,7 +1,7 @@ import { cookies } from 'next/headers'; import { notFound, redirect } from 'next/navigation'; -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import { Chat } from '@/components/chat'; import { DataStreamHandler } from '@/components/data-stream-handler'; import { entitlementsByUserRole } from '@/lib/ai/entitlements'; diff --git a/app/(chat)/jobs/[id]/results/actions.ts b/app/[locale]/(chat)/jobs/[id]/results/actions.ts similarity index 99% rename from app/(chat)/jobs/[id]/results/actions.ts rename to app/[locale]/(chat)/jobs/[id]/results/actions.ts index 192d79e..f236380 100644 --- a/app/(chat)/jobs/[id]/results/actions.ts +++ b/app/[locale]/(chat)/jobs/[id]/results/actions.ts @@ -1,6 +1,6 @@ 'use server'; -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import { getJobById, getJobResultsByJobId, diff --git a/app/(chat)/jobs/[id]/results/job-control-buttons.tsx b/app/[locale]/(chat)/jobs/[id]/results/job-control-buttons.tsx similarity index 100% rename from app/(chat)/jobs/[id]/results/job-control-buttons.tsx rename to app/[locale]/(chat)/jobs/[id]/results/job-control-buttons.tsx diff --git a/app/(chat)/jobs/[id]/results/page-refresher.tsx b/app/[locale]/(chat)/jobs/[id]/results/page-refresher.tsx similarity index 100% rename from app/(chat)/jobs/[id]/results/page-refresher.tsx rename to app/[locale]/(chat)/jobs/[id]/results/page-refresher.tsx diff --git a/app/(chat)/jobs/[id]/results/page.tsx b/app/[locale]/(chat)/jobs/[id]/results/page.tsx similarity index 99% rename from app/(chat)/jobs/[id]/results/page.tsx rename to app/[locale]/(chat)/jobs/[id]/results/page.tsx index d3f5af2..6a1464f 100644 --- a/app/(chat)/jobs/[id]/results/page.tsx +++ b/app/[locale]/(chat)/jobs/[id]/results/page.tsx @@ -1,4 +1,4 @@ -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import { JobResultCard } from '@/components/job-result-card'; import { JobResultsFilter } from '@/components/job-results-filter'; import { Badge } from '@/components/ui/badge'; diff --git a/app/(chat)/jobs/actions.spec.ts b/app/[locale]/(chat)/jobs/actions.spec.ts similarity index 97% rename from app/(chat)/jobs/actions.spec.ts rename to app/[locale]/(chat)/jobs/actions.spec.ts index 792b400..d0fd210 100644 --- a/app/(chat)/jobs/actions.spec.ts +++ b/app/[locale]/(chat)/jobs/actions.spec.ts @@ -46,7 +46,7 @@ jest.mock('next/cache', () => ({ revalidatePath: jest.fn(), })); -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import { db } from '@/lib/db/queries/client'; import { deleteJob, @@ -128,14 +128,14 @@ describe('Jobs Server Actions', () => { return await callback(mockTx as any); }); mockRevalidatePath.mockReturnValue(undefined); - + // Setup mock returns for QStash and Workflow instances mockQStashInstance.schedules.delete.mockResolvedValue(undefined); mockQStashInstance.schedules.pause.mockResolvedValue(undefined); mockQStashInstance.schedules.resume.mockResolvedValue(undefined); mockQStashInstance.publishJSON.mockResolvedValue({ messageId: 'msg-123' }); mockWorkflowInstance.trigger.mockResolvedValue(undefined); - + // Setup job query mocks mockDeleteJob.mockResolvedValue(undefined); mockDeleteJobsByIds.mockResolvedValue(undefined); @@ -231,7 +231,9 @@ describe('Jobs Server Actions', () => { status: undefined, runningStatus: undefined, }), - ).rejects.toThrow('You need to sign in to view this chat. Please sign in and try again.'); + ).rejects.toThrow( + 'You need to sign in to view this chat. Please sign in and try again.', + ); }); test('should validate pagination parameters', async () => { @@ -260,7 +262,9 @@ describe('Jobs Server Actions', () => { test('should throw error for non-existent job', async () => { mockGetJobById.mockResolvedValue(null); - await expect(getJob({ id: 'non-existent-id' })).rejects.toThrow('The requested chat was not found'); + await expect(getJob({ id: 'non-existent-id' })).rejects.toThrow( + 'The requested chat was not found', + ); }); test('should return error when user not authenticated', async () => { @@ -275,9 +279,9 @@ describe('Jobs Server Actions', () => { // Since the function doesn't validate UUID format, it will try to fetch // The mock is set up to return mockJob, so it will succeed mockGetJobById.mockResolvedValue(mockJob as any); - + const result = await getJob({ id: 'invalid-uuid' }); - + expect(result).toEqual(mockJob); }); }); @@ -291,7 +295,10 @@ describe('Jobs Server Actions', () => { expect(result.success).toBe(true); expect(result.message).toBe('Job deleted successfully'); - expect(mockDeleteJob).toHaveBeenCalledWith({ id: mockJob.id, dbConnection: expect.any(Object) }); + expect(mockDeleteJob).toHaveBeenCalledWith({ + id: mockJob.id, + dbConnection: expect.any(Object), + }); expect(mockRevalidatePath).toHaveBeenCalledWith('/jobs'); }); @@ -369,7 +376,7 @@ describe('Jobs Server Actions', () => { test('should handle deletion errors', async () => { const validUuid1 = '123e4567-e89b-12d3-a456-426614174001'; const validUuid2 = '123e4567-e89b-12d3-a456-426614174002'; - + // Mock to return valid jobs first mockGetJobById.mockResolvedValue(mockJob as any); mockDeleteJobsByIds.mockRejectedValue(new Error('Database error')); @@ -478,7 +485,7 @@ describe('Jobs Server Actions', () => { test('should validate job ID parameter', async () => { // Override mock to return null for invalid ID test mockGetJobById.mockResolvedValue(null); - + const result = await triggerJobAction({ id: 'invalid-uuid' }); expect(result.success).toBe(false); @@ -488,7 +495,7 @@ describe('Jobs Server Actions', () => { test('should handle QStash errors', async () => { // Override mock to return null for error test mockGetJobById.mockResolvedValue(null); - + const result = await triggerJobAction({ id: 'nonexistent-job-id' }); expect(result.success).toBe(false); diff --git a/app/(chat)/jobs/actions.ts b/app/[locale]/(chat)/jobs/actions.ts similarity index 99% rename from app/(chat)/jobs/actions.ts rename to app/[locale]/(chat)/jobs/actions.ts index 4031d26..1a30824 100644 --- a/app/(chat)/jobs/actions.ts +++ b/app/[locale]/(chat)/jobs/actions.ts @@ -1,6 +1,6 @@ 'use server'; -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import { db } from '@/lib/db/queries/client'; import { deleteJob, diff --git a/app/(chat)/jobs/layout.tsx b/app/[locale]/(chat)/jobs/layout.tsx similarity index 100% rename from app/(chat)/jobs/layout.tsx rename to app/[locale]/(chat)/jobs/layout.tsx diff --git a/app/(chat)/jobs/page.tsx b/app/[locale]/(chat)/jobs/page.tsx similarity index 99% rename from app/(chat)/jobs/page.tsx rename to app/[locale]/(chat)/jobs/page.tsx index 7a28423..9a754f9 100644 --- a/app/(chat)/jobs/page.tsx +++ b/app/[locale]/(chat)/jobs/page.tsx @@ -1,4 +1,4 @@ -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import { JobsList } from '@/components/jobs-list'; import { getJobs } from './actions'; import { Suspense } from 'react'; diff --git a/app/(chat)/layout.tsx b/app/[locale]/(chat)/layout.tsx similarity index 100% rename from app/(chat)/layout.tsx rename to app/[locale]/(chat)/layout.tsx diff --git a/app/(chat)/opengraph-image.png b/app/[locale]/(chat)/opengraph-image.png similarity index 100% rename from app/(chat)/opengraph-image.png rename to app/[locale]/(chat)/opengraph-image.png diff --git a/app/(chat)/page.tsx b/app/[locale]/(chat)/page.tsx similarity index 100% rename from app/(chat)/page.tsx rename to app/[locale]/(chat)/page.tsx diff --git a/app/(chat)/profile/actions.spec.ts b/app/[locale]/(chat)/profile/actions.spec.ts similarity index 97% rename from app/(chat)/profile/actions.spec.ts rename to app/[locale]/(chat)/profile/actions.spec.ts index 7489b58..fa700df 100644 --- a/app/(chat)/profile/actions.spec.ts +++ b/app/[locale]/(chat)/profile/actions.spec.ts @@ -20,7 +20,7 @@ jest.mock('next/cache', () => ({ revalidatePath: jest.fn(), })); -import { auth, signOut } from '@/app/(auth)/auth'; +import { auth, signOut } from '@/app/[locale]/(auth)/auth'; import { deleteUserAccount, updateUserPassword, @@ -92,7 +92,7 @@ describe('Profile Server Actions', () => { mockDeleteUserAccount.mockResolvedValue(undefined); mockSignOut.mockResolvedValue(undefined); mockRevalidatePath.mockReturnValue(undefined); - + // Setup passkey mocks mockGetPasskeyAuthenticatorsByUserId.mockResolvedValue([]); mockGetPasskeyAuthenticatorByCredentialId.mockResolvedValue({ @@ -185,7 +185,9 @@ describe('Profile Server Actions', () => { const result = await resetPassword(null, validFormData); expect(result.success).toBe(false); - expect(result.message).toBe('Failed to update password. Please try again.'); + expect(result.message).toBe( + 'Failed to update password. Please try again.', + ); }); }); @@ -254,7 +256,9 @@ describe('Profile Server Actions', () => { const result = await deleteAccount(null, validFormData); expect(result.success).toBe(false); - expect(result.message).toBe('Failed to delete account. Please try again.'); + expect(result.message).toBe( + 'Failed to delete account. Please try again.', + ); }); }); @@ -366,7 +370,7 @@ describe('Profile Server Actions', () => { test('should validate credential ID', async () => { // When credential ID is empty, the query should return null mockGetPasskeyAuthenticatorByCredentialId.mockResolvedValue(null); - + const result = await deletePasskey(''); expect(result.success).toBe(false); diff --git a/app/(chat)/profile/actions.ts b/app/[locale]/(chat)/profile/actions.ts similarity index 99% rename from app/(chat)/profile/actions.ts rename to app/[locale]/(chat)/profile/actions.ts index 3171e8d..0267a4d 100644 --- a/app/(chat)/profile/actions.ts +++ b/app/[locale]/(chat)/profile/actions.ts @@ -1,6 +1,6 @@ 'use server'; -import { auth, signOut } from '@/app/(auth)/auth'; +import { auth, signOut } from '@/app/[locale]/(auth)/auth'; import { deleteUserAccount, updateUserPassword, diff --git a/app/(chat)/profile/page.tsx b/app/[locale]/(chat)/profile/page.tsx similarity index 97% rename from app/(chat)/profile/page.tsx rename to app/[locale]/(chat)/profile/page.tsx index 3bbcfbc..b03b575 100644 --- a/app/(chat)/profile/page.tsx +++ b/app/[locale]/(chat)/profile/page.tsx @@ -1,5 +1,5 @@ import { redirect } from 'next/navigation'; -import { auth } from '@/app/(auth)/auth'; +import { auth } from '@/app/[locale]/(auth)/auth'; import { AccountTab } from '@/components/profile/account-tab'; import { LinkingTab } from '@/components/profile/linking-tab'; import { ProfileHeader } from '@/components/profile-header'; diff --git a/app/(chat)/twitter-image.png b/app/[locale]/(chat)/twitter-image.png similarity index 100% rename from app/(chat)/twitter-image.png rename to app/[locale]/(chat)/twitter-image.png diff --git a/app/globals.css b/app/[locale]/globals.css similarity index 100% rename from app/globals.css rename to app/[locale]/globals.css diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx new file mode 100644 index 0000000..47cb3d0 --- /dev/null +++ b/app/[locale]/layout.tsx @@ -0,0 +1,84 @@ +import { ThemeProvider } from '@/components/theme-provider'; +import { Toaster } from 'sonner'; +import { Analytics } from '@vercel/analytics/next'; +import { SpeedInsights } from '@vercel/speed-insights/next'; +import { SessionProvider } from 'next-auth/react'; +import { NextIntlClientProvider } from 'next-intl'; +import { getMessages } from 'next-intl/server'; +import './globals.css'; + +// Fallback font configuration for build environments without internet access +const geist = { + variable: '--font-geist', +}; + +const geistMono = { + variable: '--font-geist-mono', +}; + +const LIGHT_THEME_COLOR = 'hsl(0 0% 100%)'; +const DARK_THEME_COLOR = 'hsl(240deg 10% 3.92%)'; +const THEME_COLOR_SCRIPT = `\ +(function() { + var html = document.documentElement; + var meta = document.querySelector('meta[name="theme-color"]'); + if (!meta) { + meta = document.createElement('meta'); + meta.setAttribute('name', 'theme-color'); + document.head.appendChild(meta); + } + function updateThemeColor() { + var isDark = html.classList.contains('dark'); + meta.setAttribute('content', isDark ? '${DARK_THEME_COLOR}' : '${LIGHT_THEME_COLOR}'); + } + var observer = new MutationObserver(updateThemeColor); + observer.observe(html, { attributes: true, attributeFilter: ['class'] }); + updateThemeColor(); +})();`; + +export default async function LocaleLayout({ + children, + params, +}: Readonly<{ + children: React.ReactNode; + params: Promise<{ locale: string }>; +}>) { + const { locale } = await params; + // Providing all messages to the client side is the easiest way to get started + const messages = await getMessages(); + + return ( + + +