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 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 (
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+ );
+}
diff --git a/app/loading.tsx b/app/[locale]/loading.tsx
similarity index 100%
rename from app/loading.tsx
rename to app/[locale]/loading.tsx
diff --git a/app/api/auth/webauthn/registration-options/route.ts b/app/api/auth/webauthn/registration-options/route.ts
index 3f1971b..fead9be 100644
--- a/app/api/auth/webauthn/registration-options/route.ts
+++ b/app/api/auth/webauthn/registration-options/route.ts
@@ -1,7 +1,7 @@
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { z } from 'zod';
-import { auth } from '@/app/(auth)/auth';
+import { auth } from '@/app/[locale]/(auth)/auth';
import { generatePasskeyRegistrationOptions } from '@/lib/webauthn';
import { getPasskeyAuthenticatorsByUserId } from '@/lib/db/queries/queries';
diff --git a/app/api/auth/webauthn/registration-verification/route.ts b/app/api/auth/webauthn/registration-verification/route.ts
index 48cec2d..481525d 100644
--- a/app/api/auth/webauthn/registration-verification/route.ts
+++ b/app/api/auth/webauthn/registration-verification/route.ts
@@ -1,7 +1,7 @@
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { z } from 'zod';
-import { auth, signIn } from '@/app/(auth)/auth';
+import { auth, signIn } from '@/app/[locale]/(auth)/auth';
import { verifyPasskeyRegistration } from '@/lib/webauthn';
import { ChatSDKError } from '@/lib/errors';
diff --git a/app/layout.tsx b/app/layout.tsx
index 97e4b50..a1a409a 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,11 +1,7 @@
-import { ThemeProvider } from '@/components/theme-provider';
import type { Metadata } from 'next';
-import { Toaster } from 'sonner';
-import { Analytics } from '@vercel/analytics/next';
-import { SpeedInsights } from '@vercel/speed-insights/next';
-import { SessionProvider } from 'next-auth/react';
import { getBrandName } from '@/lib/utils';
-import './globals.css';
+import { routing } from '@/lib/i18n/routing';
+import { notFound } from 'next/navigation';
export const metadata: Metadata = {
metadataBase: new URL('https://chat.vercel.ai'),
@@ -17,70 +13,23 @@ export const viewport = {
maximumScale: 1, // Disable auto-zoom on mobile Safari
};
-// 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 function generateStaticParams() {
+ return routing.locales.map((locale) => ({ locale }));
+}
export default async function RootLayout({
children,
+ params,
}: Readonly<{
children: React.ReactNode;
+ params: Promise<{ locale: string }>;
}>) {
- return (
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
- );
+ const { locale } = await params;
+
+ // Ensure that the incoming `locale` is valid
+ if (!routing.locales.includes(locale as any)) {
+ notFound();
+ }
+
+ return children;
}
diff --git a/artifacts/flowchart/client.tsx b/artifacts/flowchart/client.tsx
index 0e7df0d..999d762 100644
--- a/artifacts/flowchart/client.tsx
+++ b/artifacts/flowchart/client.tsx
@@ -5,7 +5,7 @@ import { toast } from 'sonner';
import { getSuggestions } from '../actions';
// React Flow imports
-import { createWorkflowJob } from '@/app/(chat)/actions';
+import { createWorkflowJob } from '@/app/[locale]/(chat)/actions';
import { ClockRewind } from '@/components/icons';
import type { Suggestion } from '@/lib/db/schema';
import OnStepView from '@/lib/workflow/onstep-view';
diff --git a/components/artifact.spec.tsx b/components/artifact.spec.tsx
index 2aa68d1..53dfa12 100644
--- a/components/artifact.spec.tsx
+++ b/components/artifact.spec.tsx
@@ -7,12 +7,17 @@ import { Artifact } from './artifact';
jest.mock('swr', () => ({
__esModule: true,
- default: jest.fn(() => ({ data: undefined, error: undefined, mutate: jest.fn(), isLoading: false })),
- useSWRConfig: jest.fn(() => ({ mutate: jest.fn() }))
+ default: jest.fn(() => ({
+ data: undefined,
+ error: undefined,
+ mutate: jest.fn(),
+ isLoading: false,
+ })),
+ useSWRConfig: jest.fn(() => ({ mutate: jest.fn() })),
}));
jest.mock('usehooks-ts', () => ({
useDebounceCallback: jest.fn((fn) => fn),
- useWindowSize: jest.fn(() => ({ width: 1024, height: 768 }))
+ useWindowSize: jest.fn(() => ({ width: 1024, height: 768 })),
}));
jest.mock('@/hooks/use-artifact', () => ({
useArtifact: jest.fn(() => ({
@@ -23,33 +28,39 @@ jest.mock('@/hooks/use-artifact', () => ({
content: 'Test content',
isVisible: true,
status: 'idle',
- boundingBox: { top: 0, left: 0, width: 300, height: 200 }
+ boundingBox: { top: 0, left: 0, width: 300, height: 200 },
},
setArtifact: jest.fn(),
metadata: {},
- setMetadata: jest.fn()
- }))
+ setMetadata: jest.fn(),
+ })),
}));
jest.mock('./ui/sidebar', () => ({
- useSidebar: jest.fn(() => ({ open: false }))
+ useSidebar: jest.fn(() => ({ open: false })),
}));
jest.mock('./input/multimodal-input', () => ({
- MultimodalInput: () =>
Multimodal Input
+ MultimodalInput: () => (
+
Multimodal Input
+ ),
}));
jest.mock('./artifact-messages', () => ({
- ArtifactMessages: () =>
Artifact Messages
+ ArtifactMessages: () => (
+
Artifact Messages
+ ),
}));
jest.mock('./artifact-close-button', () => ({
- ArtifactCloseButton: () =>
Close
+ ArtifactCloseButton: () => (
+
Close
+ ),
}));
jest.mock('./artifact-actions', () => ({
- ArtifactActions: () =>
Actions
+ ArtifactActions: () =>
Actions
,
}));
jest.mock('./toolbar', () => ({
- Toolbar: () =>
Toolbar
+ Toolbar: () =>
Toolbar
,
}));
jest.mock('./version-footer', () => ({
- VersionFooter: () =>
Version Footer
+ VersionFooter: () =>
Version Footer
,
}));
// Mock text artifact
@@ -61,22 +72,25 @@ jest.mock('@/artifacts/text/client', () => ({
{title}
{content}
- )
- }
+ ),
+ },
}));
// Mock other artifacts
jest.mock('@/artifacts/code/client', () => ({
- codeArtifact: { kind: 'code', content: () =>