From f43aec8a036520b9d64fed18f6d8ddd89dd448c9 Mon Sep 17 00:00:00 2001
From: Ho Ngoc Hai
Date: Fri, 2 Jan 2026 10:06:22 +0700
Subject: [PATCH] Implement internationalization support and enhance user
experience in web-client
- Added `next-intl` dependency for improved internationalization capabilities.
- Integrated translation hooks across various components, including authentication, chat, and settings, to support dynamic language switching.
- Updated UI elements to utilize translated strings for better accessibility and user experience.
- Refactored forms and validation schemas to include localized messages for error handling and user prompts.
- Enhanced chat functionality with localized messages for actions and notifications.
These changes aim to provide a more inclusive experience for users by supporting multiple languages and improving overall usability.
---
...iện_đa_ngôn_ngữ_cho_web-client_a7794ae1.plan.md | 446 ++++++++++++++++++
apps/web-client/package.json | 3 +-
.../src/app/(auth)/forgot-password/page.tsx | 71 ++-
apps/web-client/src/app/(auth)/login/page.tsx | 67 +--
.../src/app/(auth)/register/page.tsx | 122 ++---
.../src/app/(dashboard)/chat/page.tsx | 28 +-
.../(dashboard)/settings/api-keys/page.tsx | 154 +++---
.../src/app/(dashboard)/settings/layout.tsx | 89 ++--
.../(dashboard)/settings/preferences/page.tsx | 104 ++--
.../app/(dashboard)/settings/profile/page.tsx | 113 +++--
.../(dashboard)/settings/security/page.tsx | 180 ++++---
apps/web-client/src/app/layout.tsx | 13 +-
apps/web-client/src/app/page.tsx | 14 +-
.../src/components/chat/chat-input.tsx | 16 +-
.../src/components/chat/chat-layout.tsx | 12 +-
.../components/chat/conversation-sidebar.tsx | 27 +-
.../components/chat/message-actions-menu.tsx | 26 +-
.../src/components/chat/message-bubble.tsx | 54 ++-
.../src/components/chat/typing-indicator.tsx | 8 +-
apps/web-client/src/contexts/i18n-context.tsx | 142 ++++++
apps/web-client/src/hooks/use-translation.ts | 40 ++
apps/web-client/src/i18n/config.ts | 30 ++
apps/web-client/src/i18n/messages/en.json | 348 ++++++++++++++
apps/web-client/src/i18n/messages/vi.json | 348 ++++++++++++++
apps/web-client/src/i18n/request.ts | 29 ++
.../src/providers/i18n-provider.tsx | 47 ++
apps/web-client/src/stores/auth-store.ts | 4 +-
pnpm-lock.yaml | 244 ++++------
28 files changed, 2104 insertions(+), 675 deletions(-)
create mode 100644 .cursor/plans/điều_chỉnh_giao_diện_đa_ngôn_ngữ_cho_web-client_a7794ae1.plan.md
create mode 100644 apps/web-client/src/contexts/i18n-context.tsx
create mode 100644 apps/web-client/src/hooks/use-translation.ts
create mode 100644 apps/web-client/src/i18n/config.ts
create mode 100644 apps/web-client/src/i18n/messages/en.json
create mode 100644 apps/web-client/src/i18n/messages/vi.json
create mode 100644 apps/web-client/src/i18n/request.ts
create mode 100644 apps/web-client/src/providers/i18n-provider.tsx
diff --git a/.cursor/plans/điều_chỉnh_giao_diện_đa_ngôn_ngữ_cho_web-client_a7794ae1.plan.md b/.cursor/plans/điều_chỉnh_giao_diện_đa_ngôn_ngữ_cho_web-client_a7794ae1.plan.md
new file mode 100644
index 00000000..f3948224
--- /dev/null
+++ b/.cursor/plans/điều_chỉnh_giao_diện_đa_ngôn_ngữ_cho_web-client_a7794ae1.plan.md
@@ -0,0 +1,446 @@
+---
+name: Điều chỉnh giao diện đa ngôn ngữ cho web-client
+overview: Thiết lập hệ thống i18n hoàn chỉnh với next-intl, migrate tất cả hardcoded text sang translation files, và tích hợp với language preferences để hiển thị đúng ngôn ngữ người dùng chọn.
+todos:
+ - id: install-deps
+ content: Cài đặt next-intl package - Update package.json và chạy pnpm install
+ status: completed
+ - id: create-i18n-config
+ content: Tạo src/i18n/config.ts - Định nghĩa locales ['en', 'vi'], default 'en'
+ status: completed
+ - id: create-i18n-request
+ content: Tạo src/i18n/request.ts - Request handler với locale detection logic
+ status: completed
+ - id: create-i18n-context
+ content: Tạo src/contexts/i18n-context.tsx - Context với setLocale, getLocale, localStorage sync
+ status: completed
+ - id: create-i18n-provider
+ content: Tạo src/providers/i18n-provider.tsx - Provider component với browser detection
+ status: completed
+ - id: create-translation-hook
+ content: Tạo src/hooks/use-translation.ts - Hook useTranslation() với type safety
+ status: completed
+ - id: update-root-layout
+ content: Update src/app/layout.tsx - Wrap với I18nProvider, dynamic lang attribute
+ status: completed
+ - id: extract-common-translations
+ content: Tạo translation keys cho common namespace - buttons, labels, placeholders (en.json + vi.json)
+ status: completed
+ - id: extract-auth-translations
+ content: Tạo translation keys cho auth namespace - login, register, forgot-password (en.json + vi.json)
+ status: completed
+ - id: extract-chat-translations
+ content: Tạo translation keys cho chat namespace - messages, sidebar, input (en.json + vi.json)
+ status: completed
+ - id: extract-settings-translations
+ content: Tạo translation keys cho settings namespace - preferences, profile, security, api-keys (en.json + vi.json)
+ status: completed
+ - id: extract-validation-translations
+ content: Tạo translation keys cho validation namespace - form errors, Zod messages (en.json + vi.json)
+ status: completed
+ - id: extract-error-translations
+ content: Tạo translation keys cho errors namespace - API errors, general errors (en.json + vi.json)
+ status: completed
+ - id: migrate-login-page
+ content: Migrate src/app/(auth)/login/page.tsx - Replace hardcoded text với useTranslation()
+ status: completed
+ - id: migrate-register-page
+ content: Migrate src/app/(auth)/register/page.tsx - Replace hardcoded text với useTranslation()
+ status: completed
+ - id: migrate-forgot-password-page
+ content: Migrate src/app/(auth)/forgot-password/page.tsx - Replace hardcoded text với useTranslation()
+ status: completed
+ - id: migrate-login-validation
+ content: Update Zod schema trong login page - Sử dụng translated validation messages
+ status: completed
+ - id: migrate-register-validation
+ content: Update Zod schema trong register page - Sử dụng translated validation messages
+ status: completed
+ - id: migrate-forgot-password-validation
+ content: Update Zod schema trong forgot-password page - Sử dụng translated validation messages
+ status: completed
+ - id: migrate-home-page
+ content: Migrate src/app/page.tsx - Replace hardcoded text với useTranslation()
+ status: completed
+ - id: migrate-chat-page
+ content: Migrate src/app/(dashboard)/chat/page.tsx - Replace hardcoded text với useTranslation()
+ status: completed
+ - id: migrate-settings-layout
+ content: Migrate src/app/(dashboard)/settings/layout.tsx - Replace hardcoded text với useTranslation()
+ status: completed
+ - id: migrate-preferences-page
+ content: Migrate src/app/(dashboard)/settings/preferences/page.tsx - Replace hardcoded text với useTranslation()
+ status: completed
+ - id: migrate-profile-page
+ content: Migrate src/app/(dashboard)/settings/profile/page.tsx - Replace hardcoded text với useTranslation()
+ status: completed
+ - id: migrate-security-page
+ content: Migrate src/app/(dashboard)/settings/security/page.tsx - Replace hardcoded text với useTranslation()
+ status: completed
+ - id: migrate-api-keys-page
+ content: Migrate src/app/(dashboard)/settings/api-keys/page.tsx - Replace hardcoded text với useTranslation()
+ status: completed
+ - id: migrate-chat-input
+ content: Migrate src/components/chat/chat-input.tsx - Replace hardcoded text với useTranslation()
+ status: completed
+ - id: migrate-conversation-sidebar
+ content: Migrate src/components/chat/conversation-sidebar.tsx - Replace hardcoded text, format dates với useTranslation()
+ status: completed
+ - id: migrate-message-bubble
+ content: Migrate src/components/chat/message-bubble.tsx - Replace hardcoded text với useTranslation() (nếu có)
+ status: completed
+ - id: migrate-chat-layout
+ content: Migrate src/components/chat/chat-layout.tsx - Replace hardcoded text với useTranslation() (nếu có)
+ status: completed
+ - id: migrate-message-actions-menu
+ content: Migrate src/components/chat/message-actions-menu.tsx - Replace hardcoded text với useTranslation() (nếu có)
+ status: completed
+ - id: migrate-typing-indicator
+ content: Migrate src/components/chat/typing-indicator.tsx - Replace hardcoded text với useTranslation() (nếu có)
+ status: completed
+ - id: check-ui-components
+ content: Review src/components/ui/*.tsx - Kiểm tra và migrate nếu có hardcoded text
+ status: completed
+ - id: migrate-chat-store
+ content: Review src/stores/chat-store.ts - Migrate error messages, status messages nếu có
+ status: completed
+ - id: migrate-auth-store
+ content: Review src/stores/auth-store.ts - Migrate error messages, status messages nếu có
+ status: completed
+ - id: integrate-preferences-language-switch
+ content: Update preferences page - Kết nối language selector với i18n context, trigger re-render
+ status: completed
+ - id: add-date-formatters
+ content: Implement date formatting - Sử dụng next-intl formatters cho relative time trong sidebar
+ status: completed
+ - id: add-number-formatters
+ content: Implement number formatting - Sử dụng next-intl formatters cho numbers (nếu cần)
+ status: completed
+ - id: test-language-switching
+ content: Test language switching - Verify UI updates khi đổi ngôn ngữ trong preferences
+ status: completed
+ - id: test-browser-detection
+ content: Test browser language detection - Verify auto-detect từ navigator.language
+ status: completed
+ - id: test-persistence
+ content: Test persistence - Verify language preference lưu trong localStorage và persist qua reload
+ status: completed
+ - id: test-all-pages-en
+ content: Test tất cả pages với English - Verify không có missing translations, UI hiển thị đúng
+ status: completed
+ - id: test-all-pages-vi
+ content: Test tất cả pages với Vietnamese - Verify không có missing translations, UI hiển thị đúng
+ status: completed
+ - id: verify-html-lang-attribute
+ content: Verify HTML lang attribute - Check updates đúng khi đổi ngôn ngữ
+ status: completed
+ - id: test-validation-messages
+ content: Test validation messages - Verify form validation hiển thị đúng ngôn ngữ
+ status: completed
+---
+
+# Plan: Điều chỉnh giao diện đa ngôn ngữ cho web-client
+
+## Tổng quan
+
+Hiện tại web-client đang sử dụng format hardcode song ngữ "English / Tiếng Việt" cho tất cả text. Plan này sẽ:
+
+- Setup next-intl cho Next.js App Router
+- Tạo translation files cho en và vi
+- Migrate tất cả hardcoded text sang translation system
+- Tích hợp với language preferences từ localStorage
+- Detect browser language cho user mới
+- Update HTML lang attribute động
+
+## Kiến trúc
+
+```
+apps/web-client/
+├── src/
+│ ├── i18n/
+│ │ ├── config.ts # Cấu hình next-intl
+│ │ ├── request.ts # Request handler cho SSR
+│ │ └── messages/
+│ │ ├── en.json # English translations
+│ │ └── vi.json # Vietnamese translations
+│ ├── contexts/
+│ │ └── i18n-context.tsx # Client-side i18n context
+│ ├── hooks/
+│ │ └── use-translation.ts # Custom hook cho translations
+│ └── providers/
+│ └── i18n-provider.tsx # I18n provider component
+```
+
+## Các bước thực hiện
+
+### 1. Cài đặt dependencies
+
+- Thêm `next-intl` vào `package.json`
+- Cài đặt package: `pnpm add next-intl`
+
+### 2. Tạo cấu trúc i18n
+
+**File: `src/i18n/config.ts`**
+
+- Định nghĩa supported locales: `['en', 'vi']`
+- Default locale: `'en'`
+- Export locale configuration
+
+**File: `src/i18n/request.ts`**
+
+- Setup request handler cho next-intl
+- Detect locale từ:
+
+ 1. localStorage preferences (nếu có)
+ 2. Browser language (nếu không có preference)
+ 3. Default 'en'
+
+**File: `src/i18n/messages/en.json`**
+
+- Tất cả English translations được tổ chức theo namespace:
+ - `common`: buttons, labels, placeholders
+ - `auth`: login, register, forgot password
+ - `chat`: chat interface, messages
+ - `settings`: settings pages
+ - `errors`: error messages
+ - `validation`: form validation messages
+
+**File: `src/i18n/messages/vi.json`**
+
+- Tất cả Vietnamese translations với cùng structure
+
+### 3. Tạo I18n Context và Provider
+
+**File: `src/contexts/i18n-context.tsx`**
+
+- Context để quản lý locale state
+- Functions: `setLocale`, `getLocale`
+- Sync với localStorage preferences
+- Detect browser language cho user mới
+
+**File: `src/providers/i18n-provider.tsx`**
+
+- Provider component wrap app
+- Initialize locale từ preferences hoặc browser
+- Update HTML lang attribute khi locale thay đổi
+
+### 4. Tạo custom hook
+
+**File: `src/hooks/use-translation.ts`**
+
+- Hook `useTranslation()` để access translations
+- Type-safe với TypeScript
+- Support namespaces: `t('common.save')`, `t('auth.login')`
+
+### 5. Update Root Layout
+
+**File: `src/app/layout.tsx`**
+
+- Wrap app với `I18nProvider`
+- Update `` động
+- Remove hardcoded `lang="en"`
+
+### 6. Migrate Components
+
+Migrate tất cả hardcoded text trong các files sau:
+
+**Pages:**
+
+- `src/app/page.tsx` - Home page
+- `src/app/(auth)/login/page.tsx` - Login page
+- `src/app/(auth)/register/page.tsx` - Register page
+- `src/app/(auth)/forgot-password/page.tsx` - Forgot password
+- `src/app/(dashboard)/chat/page.tsx` - Chat page
+- `src/app/(dashboard)/settings/preferences/page.tsx` - Preferences
+- `src/app/(dashboard)/settings/profile/page.tsx` - Profile
+- `src/app/(dashboard)/settings/security/page.tsx` - Security
+- `src/app/(dashboard)/settings/api-keys/page.tsx` - API Keys
+- `src/app/(dashboard)/settings/layout.tsx` - Settings layout
+
+**Components:**
+
+- `src/components/chat/chat-input.tsx` - Chat input
+- `src/components/chat/conversation-sidebar.tsx` - Sidebar
+- `src/components/chat/message-bubble.tsx` - Message bubble
+- `src/components/ui/input.tsx` - Input component (nếu có hardcoded text)
+- `src/components/ui/select.tsx` - Select component (nếu có hardcoded text)
+
+**Stores:**
+
+- `src/stores/chat-store.ts` - Chat store messages (nếu có)
+- `src/stores/auth-store.ts` - Auth store messages (nếu có)
+
+### 7. Tích hợp với Preferences
+
+**File: `src/app/(dashboard)/settings/preferences/page.tsx`**
+
+- Khi user thay đổi language, update i18n context
+- Sync với localStorage
+- Trigger re-render để update toàn bộ UI
+
+### 8. Update Validation Schemas
+
+**Files có Zod schemas:**
+
+- `src/app/(auth)/login/page.tsx` - Login validation
+- `src/app/(auth)/register/page.tsx` - Register validation
+- `src/app/(auth)/forgot-password/page.tsx` - Forgot password validation
+
+- Tạo translation keys cho validation messages
+- Update Zod schemas để sử dụng translated messages
+
+### 9. Format Dates và Numbers
+
+- Sử dụng next-intl formatters cho dates
+- Format relative time trong conversation sidebar
+- Format timestamps trong chat messages
+
+### 10. Testing
+
+- Test language switching
+- Test browser language detection
+- Test persistence qua page reloads
+- Test tất cả pages với cả 2 ngôn ngữ
+- Verify HTML lang attribute updates
+
+## Translation Structure Example
+
+```json
+{
+ "common": {
+ "save": "Save",
+ "cancel": "Cancel",
+ "loading": "Loading...",
+ "error": "Error"
+ },
+ "auth": {
+ "login": {
+ "title": "Sign In",
+ "email": "Email",
+ "password": "Password",
+ "rememberMe": "Remember me",
+ "forgotPassword": "Forgot password?",
+ "signUp": "Sign up"
+ }
+ },
+ "chat": {
+ "newChat": "New Chat",
+ "searchPlaceholder": "Search conversations...",
+ "typeMessage": "Type your message...",
+ "send": "Send message"
+ },
+ "settings": {
+ "title": "Settings",
+ "preferences": {
+ "title": "Preferences",
+ "language": "Language",
+ "theme": "Theme"
+ }
+ }
+}
+```
+
+## Lưu ý kỹ thuật
+
+1. **SSR Compatibility**: next-intl hỗ trợ SSR, nhưng với client-side approach, chúng ta sẽ detect locale ở client-side
+2. **Type Safety**: Sử dụng TypeScript để type-check translation keys
+3. **Performance**: Lazy load translation files nếu cần
+4. **Fallback**: Luôn có fallback về 'en' nếu translation key không tồn tại
+5. **Browser Detection**: Sử dụng `navigator.language` hoặc `navigator.languages[0]` để detect
+
+## Migration Strategy
+
+1. Setup infrastructure trước (i18n config, provider, hook)
+2. Migrate từng page/component một
+3. Test sau mỗi migration
+4. Update preferences page cuối cùng để hoàn thiện integration
+
+## Phân chia Tasks cho Multiple Agents
+
+### Agent Roles
+
+1. **setup** - Setup infrastructure (phải làm trước, tuần tự)
+
+ - Cài đặt dependencies
+ - Tạo config, context, provider, hook
+ - Update root layout
+
+2. **translations** - Tạo translation files (có thể làm song song)
+
+ - Extract và tạo translation keys cho các namespaces
+ - Có thể chia theo namespace: common, auth, chat, settings, validation, errors
+
+3. **migration-auth** - Migrate auth pages (có thể làm song song sau khi có infrastructure)
+
+ - Login, register, forgot-password pages
+ - Validation schemas cho auth forms
+
+4. **migration-dashboard** - Migrate dashboard pages (có thể làm song song)
+
+ - Home, chat, settings pages
+ - Có thể chia theo page: home, chat, preferences, profile, security, api-keys
+
+5. **migration-components** - Migrate components (có thể làm song song)
+
+ - Chat components
+ - UI components (nếu có hardcoded text)
+
+6. **migration-stores** - Migrate stores (có thể làm song song)
+
+ - Chat store, auth store messages
+
+7. **integration** - Integration tasks (phải làm sau khi có migrations)
+
+ - Preferences language switching
+ - Date/number formatters
+
+8. **testing** - Testing tasks (phải làm cuối)
+
+ - Test tất cả functionality
+
+### Execution Order
+
+**Phase 1 (Tuần tự):**
+
+- `setup` agent làm tất cả infrastructure tasks theo thứ tự dependencies
+
+**Phase 2 (Song song):**
+
+- `translations` agent có thể làm tất cả translation extraction tasks cùng lúc
+
+**Phase 3-6 (Song song, sau Phase 1+2):**
+
+- `migration-auth`, `migration-dashboard`, `migration-components`, `migration-stores` có thể làm song song
+- Mỗi agent làm các file độc lập của mình
+
+**Phase 7 (Sau migrations):**
+
+- `integration` agent làm integration tasks
+
+**Phase 8 (Cuối cùng):**
+
+- `testing` agent test toàn bộ system
+
+### Dependencies Matrix
+
+```
+install-deps
+ └─> create-i18n-config
+ ├─> create-i18n-request
+ ├─> create-i18n-context
+ │ ├─> create-i18n-provider
+ │ │ └─> update-root-layout
+ │ └─> create-translation-hook
+ │ └─> [tất cả migration tasks]
+ └─> [tất cả translation extraction tasks]
+ └─> [tất cả migration tasks]
+```
+
+### Parallelization Opportunities
+
+- **Translation extraction**: 6 tasks có thể làm song song (common, auth, chat, settings, validation, errors)
+- **Auth pages migration**: 3 pages + 3 validation schemas có thể làm song song
+- **Dashboard pages migration**: 7 pages có thể làm song song
+- **Components migration**: 7+ components có thể làm song song
+- **Stores migration**: 2 stores có thể làm song song
+- **Testing**: Một số test cases có thể chạy song song (nhưng nên test tuần tự để dễ debug)
\ No newline at end of file
diff --git a/apps/web-client/package.json b/apps/web-client/package.json
index 75704321..412248d4 100644
--- a/apps/web-client/package.json
+++ b/apps/web-client/package.json
@@ -28,7 +28,8 @@
"zustand": "^4.4.7",
"axios": "^1.6.5",
"lucide-react": "^0.344.0",
- "@tanstack/react-query": "^5.17.0"
+ "@tanstack/react-query": "^5.17.0",
+ "next-intl": "^3.15.0"
},
"devDependencies": {
"@goodgo/eslint-config": "workspace:*",
diff --git a/apps/web-client/src/app/(auth)/forgot-password/page.tsx b/apps/web-client/src/app/(auth)/forgot-password/page.tsx
index f8fbc1be..31da9c0b 100644
--- a/apps/web-client/src/app/(auth)/forgot-password/page.tsx
+++ b/apps/web-client/src/app/(auth)/forgot-password/page.tsx
@@ -9,23 +9,21 @@ import { authApi } from '@/services/api/auth.api';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card';
+import { useTranslation } from '@/hooks/use-translation';
/**
- * EN: Forgot password form validation schema using Zod
- * VI: Schema validation cho form quên mật khẩu sử dụng Zod
+ * EN: Create forgot password schema with translated messages
+ * VI: Tạo forgot password schema với thông báo đã dịch
*/
-const forgotPasswordSchema = z.object({
- email: z
- .string()
- .min(1, 'Email is required / Email là bắt buộc')
- .email('Invalid email format / Định dạng email không hợp lệ'),
-});
+function createForgotPasswordSchema(t: (key: string) => string) {
+ return z.object({
+ email: z
+ .string()
+ .min(1, t('validation.emailRequired'))
+ .email(t('validation.email')),
+ });
+}
-/**
- * EN: Type inference from forgot password schema
- * VI: Suy luận kiểu từ forgot password schema
- */
-type ForgotPasswordFormData = z.infer;
/**
* EN: Forgot Password page component - allows users to request password reset link
@@ -46,6 +44,9 @@ type ForgotPasswordFormData = z.infer;
* 4. Success → Redirect to login
*/
export default function ForgotPasswordPage() {
+ // EN: Translation hook / VI: Hook translation
+ const { t } = useTranslation();
+
// EN: Success state - shows confirmation after email is sent
// VI: Trạng thái thành công - hiển thị xác nhận sau khi email được gửi
const [isSuccess, setIsSuccess] = useState(false);
@@ -55,6 +56,10 @@ export default function ForgotPasswordPage() {
// VI: Trạng thái lỗi chung cho lỗi API
const [apiError, setApiError] = useState('');
+ // EN: Create schema with translations / VI: Tạo schema với translations
+ const forgotPasswordSchema = createForgotPasswordSchema(t);
+ type ForgotPasswordFormData = z.infer;
+
// EN: React Hook Form setup with Zod resolver
// VI: Setup React Hook Form với Zod resolver
const {
@@ -88,13 +93,13 @@ export default function ForgotPasswordPage() {
setSubmittedEmail(data.email);
} else {
setApiError(
- response.error?.message || 'Failed to send reset link / Gửi link đặt lại thất bại'
+ response.error?.message || t('auth.forgotPassword.failedToSend')
);
}
} catch (err: any) {
// EN: Set error message from API response
// VI: Đặt thông báo lỗi từ phản hồi API
- setApiError(err.message || 'An error occurred / Đã xảy ra lỗi');
+ setApiError(err.message || t('errors.generic'));
}
};
@@ -105,12 +110,12 @@ export default function ForgotPasswordPage() {
- Forgot Password / Quên mật khẩu
+ {t('auth.forgotPassword.title')}
{isSuccess
- ? 'Check your email for reset instructions / Kiểm tra email để xem hướng dẫn đặt lại'
- : 'Enter your email address and we\'ll send you a reset link / Nhập địa chỉ email và chúng tôi sẽ gửi link đặt lại cho bạn'}
+ ? t('auth.forgotPassword.checkEmail')
+ : t('auth.forgotPassword.description')}
@@ -138,28 +143,18 @@ export default function ForgotPasswordPage() {
/>
- Reset link sent! / Link đặt lại đã được gửi!
+ {t('auth.forgotPassword.resetLinkSent')}
{/* EN: Detailed success message / VI: Thông báo thành công chi tiết */}
- We've sent a password reset link to{' '}
- {submittedEmail}
-
-
- Chúng tôi đã gửi link đặt lại mật khẩu đến{' '}
- {submittedEmail}
+ {t('auth.forgotPassword.resetLinkSentDetail', { email: submittedEmail })}
- Please check your inbox and follow the instructions to reset your password. If you
- don't see the email, check your spam folder.
-
-
- Vui lòng kiểm tra hộp thư và làm theo hướng dẫn để đặt lại mật khẩu. Nếu bạn không
- thấy email, hãy kiểm tra thư mục spam.
+ {t('auth.forgotPassword.checkInbox')}
@@ -175,7 +170,7 @@ export default function ForgotPasswordPage() {
setSubmittedEmail('');
}}
>
- Send to another email / Gửi đến email khác
+ {t('auth.forgotPassword.sendToAnotherEmail')}
@@ -211,7 +206,7 @@ export default function ForgotPasswordPage() {
{/* EN: Email input field / VI: Trường nhập email */}
{isSubmitting
- ? 'Sending... / Đang gửi...'
- : 'Send Reset Link / Gửi link đặt lại'}
+ ? t('auth.forgotPassword.sending')
+ : t('auth.forgotPassword.sendResetLink')}
@@ -246,17 +241,17 @@ export default function ForgotPasswordPage() {
href="/login"
className="text-sm text-accent-primary hover:brightness-110 transition-colors focus:outline-none focus:ring-2 focus:ring-accent-primary focus:ring-offset-2 focus:ring-offset-bg-primary rounded"
>
- ← Back to Login / Quay lại đăng nhập
+ ← {t('auth.forgotPassword.backToLogin')}
{/* EN: Sign up link / VI: Link đăng ký */}
- Don't have an account? / Chưa có tài khoản?{' '}
+ {t('auth.forgotPassword.noAccount')}{' '}
- Sign up / Đăng ký
+ {t('auth.forgotPassword.signUp')}
diff --git a/apps/web-client/src/app/(auth)/login/page.tsx b/apps/web-client/src/app/(auth)/login/page.tsx
index af1cba0e..bc473d9a 100644
--- a/apps/web-client/src/app/(auth)/login/page.tsx
+++ b/apps/web-client/src/app/(auth)/login/page.tsx
@@ -10,28 +10,30 @@ import { useAuthStore } from '@/stores/auth-store';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card';
+import { useTranslation } from '@/hooks/use-translation';
/**
- * EN: Login form validation schema using Zod
- * VI: Schema validation cho form đăng nhập sử dụng Zod
+ * EN: Create login schema with translated messages
+ * VI: Tạo login schema với thông báo đã dịch
*/
-const loginSchema = z.object({
- email: z
- .string()
- .min(1, 'Email is required / Email là bắt buộc')
- .email('Invalid email format / Định dạng email không hợp lệ'),
- password: z
- .string()
- .min(1, 'Password is required / Mật khẩu là bắt buộc')
- .min(8, 'Password must be at least 8 characters / Mật khẩu phải có ít nhất 8 ký tự'),
- rememberMe: z.boolean().optional(),
-});
+function createLoginSchema(t: (key: string) => string) {
+ return z.object({
+ email: z
+ .string()
+ .min(1, t('validation.emailRequired'))
+ .email(t('validation.email')),
+ password: z
+ .string()
+ .min(1, t('validation.password'))
+ .min(8, t('validation.passwordMin')),
+ rememberMe: z.boolean().optional(),
+ });
+}
/**
- * EN: Type inference from login schema
- * VI: Suy luận kiểu từ login schema
+ * EN: LoginFormData type - will be inferred from schema in component
+ * VI: Kiểu LoginFormData - sẽ được suy luận từ schema trong component
*/
-type LoginFormData = z.infer;
/**
* EN: Login page component for user authentication
@@ -46,6 +48,9 @@ type LoginFormData = z.infer;
* - Error handling
*/
export default function LoginPage() {
+ // EN: Translation hook / VI: Hook translation
+ const { t } = useTranslation();
+
// EN: Next.js router for navigation
// VI: Next.js router để điều hướng
const router = useRouter();
@@ -58,6 +63,10 @@ export default function LoginPage() {
// VI: Trạng thái lỗi chung cho lỗi API
const [apiError, setApiError] = useState('');
+ // EN: Create schema with translations / VI: Tạo schema với translations
+ const loginSchema = createLoginSchema(t);
+ type LoginFormData = z.infer;
+
// EN: React Hook Form setup with Zod resolver
// VI: Setup React Hook Form với Zod resolver
const {
@@ -91,22 +100,22 @@ export default function LoginPage() {
} catch (err: any) {
// EN: Set error message from API response
// VI: Đặt thông báo lỗi từ phản hồi API
- setApiError(err.message || 'Login failed / Đăng nhập thất bại');
+ setApiError(err.message || t('auth.login.loginFailed'));
}
};
return (
// EN: Centered login form layout with dark mode background
// VI: Layout form đăng nhập được căn giữa với nền dark mode
-
+
- Sign In / Đăng nhập
+ {t('auth.login.title')}
- Enter your credentials to access your account / Nhập thông tin đăng nhập để truy cập tài khoản
+ {t('auth.login.description')}
@@ -139,7 +148,7 @@ export default function LoginPage() {
{/* EN: Email input field / VI: Trường nhập email */}
- Remember me / Nhớ đăng nhập
+ {t('auth.login.rememberMe')}
@@ -177,7 +186,7 @@ export default function LoginPage() {
href="/forgot-password"
className="text-sm text-accent-primary hover:brightness-110 transition-colors focus:outline-none focus:ring-2 focus:ring-accent-primary focus:ring-offset-2 focus:ring-offset-bg-primary rounded"
>
- Forgot password? / Quên mật khẩu?
+ {t('auth.login.forgotPassword')}
@@ -193,18 +202,18 @@ export default function LoginPage() {
disabled={isLoading || isSubmitting}
>
{isLoading || isSubmitting
- ? 'Signing in... / Đang đăng nhập...'
- : 'Sign In / Đăng nhập'}
+ ? t('auth.login.signingIn')
+ : t('auth.login.title')}
{/* EN: Sign up link / VI: Link đăng ký */}
- Don't have an account? / Chưa có tài khoản?{' '}
+ {t('auth.login.noAccount')}{' '}
- Sign up / Đăng ký
+ {t('auth.login.signUp')}
diff --git a/apps/web-client/src/app/(auth)/register/page.tsx b/apps/web-client/src/app/(auth)/register/page.tsx
index 01e52f1f..165adc39 100644
--- a/apps/web-client/src/app/(auth)/register/page.tsx
+++ b/apps/web-client/src/app/(auth)/register/page.tsx
@@ -10,47 +10,45 @@ import { useAuthStore } from '@/stores/auth-store';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card';
+import { useTranslation } from '@/hooks/use-translation';
/**
- * EN: Register form validation schema using Zod
- * VI: Schema validation cho form đăng ký sử dụng Zod
+ * EN: Create register schema with translated messages
+ * VI: Tạo register schema với thông báo đã dịch
*/
-const registerSchema = z
- .object({
- fullName: z
- .string()
- .min(1, 'Full name is required / Họ tên là bắt buộc')
- .min(2, 'Full name must be at least 2 characters / Họ tên phải có ít nhất 2 ký tự')
- .max(100, 'Full name must be less than 100 characters / Họ tên phải ít hơn 100 ký tự'),
- email: z
- .string()
- .min(1, 'Email is required / Email là bắt buộc')
- .email('Invalid email format / Định dạng email không hợp lệ'),
- password: z
- .string()
- .min(1, 'Password is required / Mật khẩu là bắt buộc')
- .min(8, 'Password must be at least 8 characters / Mật khẩu phải có ít nhất 8 ký tự')
- .regex(/[A-Z]/, 'Password must contain at least one uppercase letter / Mật khẩu phải chứa ít nhất một chữ hoa')
- .regex(/[a-z]/, 'Password must contain at least one lowercase letter / Mật khẩu phải chứa ít nhất một chữ thường')
- .regex(/[0-9]/, 'Password must contain at least one number / Mật khẩu phải chứa ít nhất một số')
- .regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character / Mật khẩu phải chứa ít nhất một ký tự đặc biệt'),
- confirmPassword: z
- .string()
- .min(1, 'Please confirm your password / Vui lòng xác nhận mật khẩu'),
- terms: z.boolean().refine((val) => val === true, {
- message: 'You must accept the terms and conditions / Bạn phải chấp nhận điều khoản và điều kiện',
- }),
- })
- .refine((data) => data.password === data.confirmPassword, {
- message: 'Passwords do not match / Mật khẩu không khớp',
- path: ['confirmPassword'],
- });
+function createRegisterSchema(t: (key: string) => string) {
+ return z
+ .object({
+ fullName: z
+ .string()
+ .min(1, t('validation.fullNameRequired'))
+ .min(2, t('validation.fullNameMin'))
+ .max(100, t('validation.fullNameMax')),
+ email: z
+ .string()
+ .min(1, t('validation.emailRequired'))
+ .email(t('validation.email')),
+ password: z
+ .string()
+ .min(1, t('validation.password'))
+ .min(8, t('validation.passwordMin'))
+ .regex(/[A-Z]/, t('validation.passwordUppercase'))
+ .regex(/[a-z]/, t('validation.passwordLowercase'))
+ .regex(/[0-9]/, t('validation.passwordNumber'))
+ .regex(/[^A-Za-z0-9]/, t('validation.passwordSpecial')),
+ confirmPassword: z
+ .string()
+ .min(1, t('validation.passwordConfirmRequired')),
+ terms: z.boolean().refine((val) => val === true, {
+ message: t('validation.termsRequired'),
+ }),
+ })
+ .refine((data) => data.password === data.confirmPassword, {
+ message: t('validation.passwordConfirm'),
+ path: ['confirmPassword'],
+ });
+}
-/**
- * EN: Type inference from register schema
- * VI: Suy luận kiểu từ register schema
- */
-type RegisterFormData = z.infer;
/**
* EN: Password strength levels
@@ -100,18 +98,19 @@ function calculatePasswordStrength(password: string): {
let strength: PasswordStrength;
let feedback: string;
+ // EN: This will be translated in the component / VI: Sẽ được dịch trong component
if (score < 25) {
strength = 'weak';
- feedback = 'Weak / Yếu';
+ feedback = 'weak';
} else if (score < 50) {
strength = 'fair';
- feedback = 'Fair / Trung bình';
+ feedback = 'fair';
} else if (score < 75) {
strength = 'good';
- feedback = 'Good / Tốt';
+ feedback = 'good';
} else {
strength = 'strong';
- feedback = 'Strong / Mạnh';
+ feedback = 'strong';
}
return { strength, percentage: score, feedback };
@@ -129,6 +128,9 @@ function calculatePasswordStrength(password: string): {
* - Error handling
*/
export default function RegisterPage() {
+ // EN: Translation hook / VI: Hook translation
+ const { t } = useTranslation();
+
// EN: Next.js router for navigation
// VI: Next.js router để điều hướng
const router = useRouter();
@@ -141,6 +143,10 @@ export default function RegisterPage() {
// VI: Trạng thái lỗi chung cho lỗi API
const [apiError, setApiError] = useState('');
+ // EN: Create schema with translations / VI: Tạo schema với translations
+ const registerSchema = createRegisterSchema(t);
+ type RegisterFormData = z.infer;
+
// EN: React Hook Form setup with Zod resolver
// VI: Setup React Hook Form với Zod resolver
const {
@@ -192,7 +198,7 @@ export default function RegisterPage() {
} catch (err: any) {
// EN: Set error message from API response
// VI: Đặt thông báo lỗi từ phản hồi API
- setApiError(err.message || 'Registration failed / Đăng ký thất bại');
+ setApiError(err.message || t('auth.register.registrationFailed'));
}
};
@@ -220,10 +226,10 @@ export default function RegisterPage() {
- Create Account / Tạo tài khoản
+ {t('auth.register.createAccount')}
- Sign up to get started / Đăng ký để bắt đầu
+ {t('auth.register.signUpToStart')}
@@ -256,7 +262,7 @@ export default function RegisterPage() {
{/* EN: Full name input field / VI: Trường nhập họ tên */}
@@ -320,7 +326,7 @@ export default function RegisterPage() {
: 'text-accent-success'
}`}
>
- {passwordStrength.feedback}
+ {t(`auth.register.${passwordStrength.feedback}`)}
)}
@@ -329,8 +335,8 @@ export default function RegisterPage() {
{/* EN: Confirm password input field / VI: Trường xác nhận mật khẩu */}
- I agree to the{' '}
+ {t('auth.register.agreeToTerms')}{' '}
- Terms and Conditions / Điều khoản và điều kiện
+ {t('auth.register.termsAndConditions')}
@@ -393,18 +399,18 @@ export default function RegisterPage() {
disabled={isLoading || isSubmitting}
>
{isLoading || isSubmitting
- ? 'Creating account... / Đang tạo tài khoản...'
- : 'Create Account / Tạo tài khoản'}
+ ? t('auth.register.creatingAccount')
+ : t('auth.register.createAccount')}
{/* EN: Sign in link / VI: Link đăng nhập */}
- Already have an account? / Đã có tài khoản?{' '}
+ {t('auth.register.alreadyHaveAccount')}{' '}
- Sign in / Đăng nhập
+ {t('auth.register.signIn')}
diff --git a/apps/web-client/src/app/(dashboard)/chat/page.tsx b/apps/web-client/src/app/(dashboard)/chat/page.tsx
index 38f9351a..38c34df9 100644
--- a/apps/web-client/src/app/(dashboard)/chat/page.tsx
+++ b/apps/web-client/src/app/(dashboard)/chat/page.tsx
@@ -10,6 +10,7 @@ const TypingIndicator = React.lazy(() => import('@/components/chat/typing-indica
import { LiveRegion } from '@/components/accessibility/live-region';
import { useKeyboardShortcuts, CHAT_SHORTCUTS } from '@/hooks/use-keyboard-shortcuts';
import { useChatStore, MessageSender } from '@/stores/chat-store';
+import { useTranslation } from '@/hooks/use-translation';
/**
* EN: Chat page component - Main chat interface
@@ -32,6 +33,9 @@ import { useChatStore, MessageSender } from '@/stores/chat-store';
* - Thông báo screen reader
*/
export default function ChatPage() {
+ // EN: Translation hook / VI: Hook translation
+ const { t } = useTranslation();
+
const [sidebarVisible, setSidebarVisible] = React.useState(true);
const [announcement, setAnnouncement] = React.useState('');
@@ -60,22 +64,22 @@ export default function ChatPage() {
}
try {
await sendMessage(conversationId, content);
- setAnnouncement('Message sent / Tin nhắn đã gửi');
+ setAnnouncement(t('chat.messageSent'));
} catch (error) {
- setAnnouncement('Failed to send message / Không thể gửi tin nhắn');
+ setAnnouncement(t('chat.messageSendFailed'));
}
};
// EN: Handle new chat / VI: Xử lý chat mới
const handleNewChat = () => {
createConversation();
- setAnnouncement('New conversation created / Đã tạo cuộc trò chuyện mới');
+ setAnnouncement(t('chat.newConversationCreated'));
};
// EN: Handle select conversation / VI: Xử lý chọn conversation
const handleSelectConversation = (conversationId: string) => {
selectConversation(conversationId);
- setAnnouncement(`Switched to conversation / Đã chuyển sang cuộc trò chuyện`);
+ setAnnouncement(t('chat.switchedToConversation'));
};
// EN: Keyboard shortcuts / VI: Phím tắt bàn phím
@@ -83,7 +87,7 @@ export default function ChatPage() {
{
key: CHAT_SHORTCUTS.NEW_CHAT,
handler: () => handleNewChat(),
- description: 'New chat / Chat mới',
+ description: t('chat.newChat'),
preventDefault: true,
},
{
@@ -93,7 +97,7 @@ export default function ChatPage() {
const searchInput = document.querySelector('input[type="search"]') as HTMLInputElement;
searchInput?.focus();
},
- description: 'Open search / Mở tìm kiếm',
+ description: t('chat.openSearch'),
preventDefault: true,
},
]);
@@ -120,14 +124,14 @@ export default function ChatPage() {
onSidebarToggle={setSidebarVisible}
>
{/* EN: Messages container / VI: Container tin nhắn */}
-
+
{currentMessages.length === 0 ? (
- Start a conversation / Bắt đầu cuộc trò chuyện
+ {t('chat.startConversation')}
- Type a message below to get started / Nhập tin nhắn bên dưới để bắt đầu
+ {t('chat.startConversationDesc')}
) : (
@@ -140,7 +144,7 @@ export default function ChatPage() {
showActions
onCopy={() => {
navigator.clipboard.writeText(message.content);
- setAnnouncement('Message copied / Đã sao chép tin nhắn');
+ setAnnouncement(t('chat.messageCopied'));
}}
/>
))
@@ -149,7 +153,7 @@ export default function ChatPage() {
{(() => {
const { typingUsers } = useChatStore.getState();
return Object.values(typingUsers).some((typing) => typing) ? (
- }>
+ }>
) : null;
@@ -159,7 +163,7 @@ export default function ChatPage() {
{/* EN: Chat input / VI: Chat input */}
>
diff --git a/apps/web-client/src/app/(dashboard)/settings/api-keys/page.tsx b/apps/web-client/src/app/(dashboard)/settings/api-keys/page.tsx
index 5a39d906..7fa2a68b 100644
--- a/apps/web-client/src/app/(dashboard)/settings/api-keys/page.tsx
+++ b/apps/web-client/src/app/(dashboard)/settings/api-keys/page.tsx
@@ -32,6 +32,7 @@ import {
AlertCircle,
CheckCircle2,
} from 'lucide-react';
+import { useTranslation } from '@/hooks/use-translation';
/**
* EN: API Key interface
@@ -48,32 +49,28 @@ interface ApiKey {
}
/**
- * EN: Create API key form validation schema using Zod
- * VI: Schema validation cho form tạo API key sử dụng Zod
+ * EN: Create API key schema with translated messages
+ * VI: Tạo API key schema với thông báo đã dịch
*/
-const createApiKeySchema = z.object({
- name: z
- .string()
- .min(1, 'Name is required / Tên là bắt buộc')
- .max(100, 'Name must be less than 100 characters / Tên phải ít hơn 100 ký tự'),
- description: z
- .string()
- .max(500, 'Description must be less than 500 characters / Mô tả phải ít hơn 500 ký tự')
- .optional(),
-});
-
-/**
- * EN: Type inference from create API key schema
- * VI: Suy luận kiểu từ create API key schema
- */
-type CreateApiKeyFormData = z.infer;
+function createApiKeySchema(t: (key: string) => string) {
+ return z.object({
+ name: z
+ .string()
+ .min(1, t('settings.apiKeys.nameRequired'))
+ .max(100, t('validation.maxLength', { max: 100 })),
+ description: z
+ .string()
+ .max(500, t('validation.maxLength', { max: 500 }))
+ .optional(),
+ });
+}
/**
* EN: Format timestamp to relative time string
* VI: Format timestamp thành chuỗi thời gian tương đối
*/
-function formatRelativeTime(date: string | null): string {
- if (!date) return 'Never / Không bao giờ';
+function formatRelativeTime(date: string | null, t: (key: string, values?: any) => string, locale: string): string {
+ if (!date) return t('settings.security.never');
const dateObj = new Date(date);
const now = new Date();
const diffMs = now.getTime() - dateObj.getTime();
@@ -81,11 +78,11 @@ function formatRelativeTime(date: string | null): string {
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
- if (diffMins < 1) return 'Just now / Vừa xong';
- if (diffMins < 60) return `${diffMins}m ago / ${diffMins} phút trước`;
- if (diffHours < 24) return `${diffHours}h ago / ${diffHours} giờ trước`;
- if (diffDays < 7) return `${diffDays}d ago / ${diffDays} ngày trước`;
- return dateObj.toLocaleDateString('en-US', {
+ if (diffMins < 1) return t('chat.justNow');
+ if (diffMins < 60) return t('chat.minutesAgo', { minutes: diffMins });
+ if (diffHours < 24) return t('chat.hoursAgo', { hours: diffHours });
+ if (diffDays < 7) return t('chat.daysAgo', { days: diffDays });
+ return dateObj.toLocaleDateString(locale === 'vi' ? 'vi-VN' : 'en-US', {
month: 'short',
day: 'numeric',
year: dateObj.getFullYear() !== now.getFullYear() ? 'numeric' : undefined,
@@ -96,9 +93,9 @@ function formatRelativeTime(date: string | null): string {
* EN: Format date to readable string
* VI: Format ngày thành chuỗi dễ đọc
*/
-function formatDate(date: string | null): string {
- if (!date) return 'Never / Không bao giờ';
- return new Date(date).toLocaleDateString('en-US', {
+function formatDate(date: string | null, t: (key: string) => string, locale: string): string {
+ if (!date) return t('settings.security.never');
+ return new Date(date).toLocaleDateString(locale === 'vi' ? 'vi-VN' : 'en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
@@ -118,6 +115,13 @@ function formatDate(date: string | null): string {
* - Display creation date, last used date, expiration date
*/
export default function ApiKeysPage() {
+ // EN: Translation hook / VI: Hook translation
+ const { t, locale } = useTranslation();
+
+ // EN: Create schema with translations / VI: Tạo schema với translations
+ const createApiKeySchema = createApiKeySchema(t);
+ type CreateApiKeyFormData = z.infer;
+
// EN: State management / VI: Quản lý state
const [apiKeys, setApiKeys] = useState([]);
const [isLoading, setIsLoading] = useState(true);
@@ -171,7 +175,7 @@ export default function ApiKeysPage() {
await new Promise((resolve) => setTimeout(resolve, 500));
setApiKeys([]);
} catch (err: any) {
- setError(err.message || 'Failed to load API keys / Không thể tải API keys');
+ setError(err.message || t('settings.apiKeys.failedToLoad'));
} finally {
setIsLoading(false);
}
@@ -223,7 +227,7 @@ export default function ApiKeysPage() {
};
setApiKeys((prev) => [newKey, ...prev]);
} catch (err: any) {
- setError(err.message || 'Failed to create API key / Không thể tạo API key');
+ setError(err.message || t('settings.apiKeys.failedToCreate'));
}
};
@@ -232,7 +236,7 @@ export default function ApiKeysPage() {
* VI: Xử lý xóa API key
*/
const handleDeleteKey = async (keyId: string, keyName: string) => {
- if (!confirm(`Are you sure you want to delete "${keyName}"? This action cannot be undone. / Bạn có chắc chắn muốn xóa "${keyName}"? Hành động này không thể hoàn tác.`)) {
+ if (!confirm(t('settings.apiKeys.confirmDelete', { name: keyName }))) {
return;
}
@@ -250,10 +254,10 @@ export default function ApiKeysPage() {
// EN: Mock implementation / VI: Implementation mock
await new Promise((resolve) => setTimeout(resolve, 500));
setApiKeys((prev) => prev.filter((key) => key.id !== keyId));
- setSuccess('API key deleted successfully / API key đã được xóa thành công');
+ setSuccess(t('settings.apiKeys.deletedSuccessfully'));
setTimeout(() => setSuccess(''), 3000);
} catch (err: any) {
- setError(err.message || 'Failed to delete API key / Không thể xóa API key');
+ setError(err.message || t('settings.apiKeys.failedToDelete'));
} finally {
setDeletingKeyId(null);
}
@@ -285,7 +289,7 @@ export default function ApiKeysPage() {
setCopiedKeyId(keyId);
setTimeout(() => setCopiedKeyId(null), 2000);
} catch (err) {
- console.error('Failed to copy to clipboard / Không thể sao chép vào clipboard:', err);
+ console.error('Failed to copy to clipboard:', err);
}
};
@@ -310,10 +314,10 @@ export default function ApiKeysPage() {
{/* EN: Page header / VI: Header trang */}
- API Keys / Khóa API
+ {t('settings.apiKeys.title')}
- Manage your API keys for programmatic access / Quản lý khóa API để truy cập theo chương trình
+ {t('settings.apiKeys.manageKeys')}
@@ -340,10 +344,10 @@ export default function ApiKeysPage() {
- Your API Keys / Khóa API của bạn
+ {t('settings.apiKeys.yourApiKeys')}
- Create and manage API keys for accessing the API / Tạo và quản lý khóa API để truy cập API
+ {t('settings.apiKeys.createAndManage')}