From b5b808f17171509bf47058ef9a074e04d37fea37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C6=B0=C6=A1ng=20L=C3=AA=20Anh=20V=C5=A9?= Date: Thu, 19 Mar 2026 10:22:23 +0700 Subject: [PATCH 1/9] feat(i18n): add i18n dependencies for internationalization support --- package-lock.json | 105 ++++++++++++++++++++++++++++++++++++++++++++-- package.json | 3 ++ 2 files changed, 104 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 58b516e..7320ab0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,11 +11,14 @@ "@tailwindcss/vite": "^4.2.1", "axios": "^1.13.5", "clsx": "^2.1.1", + "i18next": "^25.8.18", + "i18next-browser-languagedetector": "^8.2.1", "jwt-decode": "^4.0.0", "lucide-react": "^0.575.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-hot-toast": "^2.6.0", + "react-i18next": "^16.5.8", "react-router-dom": "^7.13.1", "recharts": "^3.8.0", "tailwind-merge": "^3.5.0", @@ -271,6 +274,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -2990,9 +3002,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -3221,6 +3233,55 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "25.8.18", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.18.tgz", + "integrity": "sha512-lzY5X83BiL5AP77+9DydbrqkQHFN9hUzWGjqjLpPcp5ZOzuu1aSoKaU3xbBLSjWx9dAzW431y+d+aogxOZaKRA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz", + "integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3991,6 +4052,33 @@ "react-dom": ">=16" } }, + "node_modules/react-i18next": { + "version": "16.5.8", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.8.tgz", + "integrity": "sha512-2ABeHHlakxVY+LSirD+OiERxFL6+zip0PaHo979bgwzeHg27Sqc82xxXWIrSFmfWX0ZkrvXMHwhsi/NGUf5VQg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 25.6.2", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", @@ -4335,7 +4423,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -4522,6 +4610,15 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 2aac6b6..024e409 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,14 @@ "@tailwindcss/vite": "^4.2.1", "axios": "^1.13.5", "clsx": "^2.1.1", + "i18next": "^25.8.18", + "i18next-browser-languagedetector": "^8.2.1", "jwt-decode": "^4.0.0", "lucide-react": "^0.575.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-hot-toast": "^2.6.0", + "react-i18next": "^16.5.8", "react-router-dom": "^7.13.1", "recharts": "^3.8.0", "tailwind-merge": "^3.5.0", From 09dacc1392eadf54bd3cc22fb5fdd7d69973e2fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C6=B0=C6=A1ng=20L=C3=AA=20Anh=20V=C5=A9?= Date: Thu, 19 Mar 2026 10:23:14 +0700 Subject: [PATCH 2/9] feat(i18n): add i18n configuration and translation files for English and Vietnamese --- src/i18n.ts | 26 ++++++++++++++++++++++++++ src/locales/en.json | 45 +++++++++++++++++++++++++++++++++++++++++++++ src/locales/vi.json | 45 +++++++++++++++++++++++++++++++++++++++++++++ src/main.tsx | 1 + 4 files changed, 117 insertions(+) create mode 100644 src/i18n.ts create mode 100644 src/locales/en.json create mode 100644 src/locales/vi.json diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..63a5017 --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,26 @@ +import i18n from 'i18next' +import { initReactI18next } from 'react-i18next' +import LanguageDetector from 'i18next-browser-languagedetector' + +import enTranslations from './locales/en.json' +import viTranslations from './locales/vi.json' + +i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + resources: { + en: { translation: enTranslations }, + vi: { translation: viTranslations }, + }, + fallbackLng: 'en', + interpolation: { + escapeValue: false, + }, + detection: { + order: ['querystring', 'cookie', 'localStorage', 'navigator', 'htmlTag'], + caches: ['localStorage', 'cookie'], + }, + }) + +export default i18n diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 0000000..f972dc1 --- /dev/null +++ b/src/locales/en.json @@ -0,0 +1,45 @@ +{ + "common": { + "loading": "Loading...", + "error": "Error", + "success": "Success", + "cancel": "Cancel", + "confirm": "Confirm", + "delete": "Delete", + "save": "Save", + "edit": "Edit", + "back": "Back", + "home": "Home" + }, + "nav": { + "dashboard": "Dashboard", + "login": "Login", + "register": "Register", + "logout": "Logout", + "vip": "VIP Upgrade", + "admin": "Admin" + }, + "home": { + "title": "Shorten your links", + "subtitle": "A powerful URL shortener with analytics and more.", + "shorten_btn": "Shorten", + "placeholder": "Enter your long URL here...", + "custom_alias": "Custom alias (optional)" + }, + "login": { + "title": "Welcome back", + "email": "Email address", + "password": "Password", + "submit": "Sign in", + "forgot_password": "Forgot password?", + "no_account": "Don't have an account? Register" + }, + "register": { + "title": "Create account", + "username": "Username", + "email": "Email address", + "password": "Password", + "submit": "Register", + "has_account": "Already have an account? Login" + } +} diff --git a/src/locales/vi.json b/src/locales/vi.json new file mode 100644 index 0000000..21f2224 --- /dev/null +++ b/src/locales/vi.json @@ -0,0 +1,45 @@ +{ + "common": { + "loading": "Đang tải...", + "error": "Lỗi", + "success": "Thành công", + "cancel": "Hủy", + "confirm": "Xác nhận", + "delete": "Xóa", + "save": "Lưu", + "edit": "Sửa", + "back": "Quay lại", + "home": "Trang chủ" + }, + "nav": { + "dashboard": "Bảng điều khiển", + "login": "Đăng nhập", + "register": "Đăng ký", + "logout": "Đăng xuất", + "vip": "Nâng cấp VIP", + "admin": "Quản trị" + }, + "home": { + "title": "Rút gọn liên kết của bạn", + "subtitle": "Công cụ rút gọn URL mạnh mẽ với phân tích và hơn thế nữa.", + "shorten_btn": "Rút gọn", + "placeholder": "Nhập URL dài của bạn tại đây...", + "custom_alias": "Bí danh tùy chỉnh (tùy chọn)" + }, + "login": { + "title": "Chào mừng trở lại", + "email": "Địa chỉ Email", + "password": "Mật khẩu", + "submit": "Đăng nhập", + "forgot_password": "Quên mật khẩu?", + "no_account": "Chưa có tài khoản? Đăng ký ngay" + }, + "register": { + "title": "Tạo tài khoản", + "username": "Tên người dùng", + "email": "Địa chỉ Email", + "password": "Mật khẩu", + "submit": "Đăng ký", + "has_account": "Đã có tài khoản? Đăng nhập ngay" + } +} diff --git a/src/main.tsx b/src/main.tsx index bef5202..cd81cee 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,7 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' +import './i18n' import App from './App.tsx' createRoot(document.getElementById('root')!).render( From f7c686362a4fa71bd28c9092ef775f9abee2c9db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C6=B0=C6=A1ng=20L=C3=AA=20Anh=20V=C5=A9?= Date: Thu, 19 Mar 2026 10:43:45 +0700 Subject: [PATCH 3/9] feat(i18n): update translation bundles with keys for all core pages --- src/locales/en.json | 126 ++++++++++++++++++++++++++++++++++++++++---- src/locales/vi.json | 126 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 232 insertions(+), 20 deletions(-) diff --git a/src/locales/en.json b/src/locales/en.json index f972dc1..c723526 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -20,26 +20,132 @@ "admin": "Admin" }, "home": { - "title": "Shorten your links", - "subtitle": "A powerful URL shortener with analytics and more.", - "shorten_btn": "Shorten", - "placeholder": "Enter your long URL here...", - "custom_alias": "Custom alias (optional)" + "title": "Shorten Your Links", + "title_highlight": "Expand Your Reach", + "subtitle": "LinkForge helps you create, track, and manage your links effortlessly with power and security.", + "shorten_btn": "Shorten Now", + "processing": "Processing...", + "placeholder": "Paste your long URL here...", + "already_logged_in": "You are already logged in to LinkForge.", + "go_to_dashboard": "Go to Dashboard", + "success_msg": "Link shortened successfully!", + "error_default": "An error occurred. Please try again later.", + "copied": "Copied to clipboard!", + "result_title": "Your shortened link", + "delete_token_title": "Delete Token (Save this!)", + "guest_warning": "Since you created this link as a guest, you must save this token if you ever wish to", + "delete_link_text": "delete the link", + "original_label": "Original:" }, "login": { "title": "Welcome back", - "email": "Email address", + "subtitle": "Please enter your details to sign in.", + "email": "Email", "password": "Password", "submit": "Sign in", + "signing_in": "Signing in...", "forgot_password": "Forgot password?", - "no_account": "Don't have an account? Register" + "no_account": "Don't have an account?", + "signup": "Sign up", + "success_msg": "Welcome back, {{name}}!", + "error_verify": "Please verify your email first", + "error_invalid": "Invalid email or password. Please try again." }, "register": { "title": "Create account", + "subtitle": "Join LinkForge today and start shortening.", "username": "Username", - "email": "Email address", + "email": "Email", "password": "Password", - "submit": "Register", - "has_account": "Already have an account? Login" + "submit": "Create account", + "creating": "Creating account...", + "has_account": "Already have an account?", + "login": "Sign in", + "success_msg": "Account created! Please check your email for verification.", + "error_password_match": "Passwords do not match" + }, + "dashboard": { + "create_title": "Create New Short Link", + "shorten_btn": "Shorten", + "vip_ad_bypass": "Want to completely bypass advertisement pages for your visitors?", + "upgrade_vip": "Upgrade to VIP today!", + "just_created": "Just Created", + "my_links": "My Links", + "total_links": "{{count}} link total", + "total_links_plural": "{{count}} links total", + "search_placeholder": "Search links...", + "no_links": "No links yet", + "create_first": "Create your first short link above!", + "status_expired": "Expired", + "status_never": "Never", + "qr_modal_title": "QR Code", + "qr_not_generated": "No QR code generated yet", + "qr_generate": "Generate QR Code", + "qr_delete": "Delete QR Code", + "qr_vip_only": "QR codes are exclusive to VIP members.", + "qr_scan_share": "Scan to share {{url}}", + "delete_qr_title": "Delete QR Code?", + "delete_qr_desc": "This will permanently remove the QR code for this link. You can always regenerate it later.", + "delete_link_title": "Delete this link?", + "delete_link_desc": "This action cannot be undone. All analytics data for this link will also be lost forever.", + "confirm_delete_qr": "Yes, Delete QR Code", + "confirm_delete_link": "Confirm Delete" + }, + "table": { + "created": "Created", + "expires": "Expires", + "url": "URL", + "clicks": "Clicks", + "actions": "Actions" + }, + "pagination": { + "page_info": "Page {{current}} of {{total}}", + "show": "Show", + "first": "First Page", + "prev": "Previous Page", + "next": "Next Page", + "last": "Last Page" + }, + "verify_email": { + "title": "Verify your email", + "subtitle": "We sent a 6-digit code to {{email}}", + "code_label": "Verification Code", + "submit": "Verify Email", + "verifying": "Verifying...", + "resend_btn": "Resend code", + "resend_cooldown": "Resend code in {{count}}s", + "resend_success": "New verification code sent!", + "success_title": "Email Verified!", + "success_subtitle": "Redirecting to sign in...", + "error_default": "Verification failed" + }, + "forgot_password": { + "title": "Forgot password?", + "reset_title": "Reset password", + "subtitle": "Enter your email and we'll send you a reset code.", + "reset_subtitle": "Enter the code sent to {{email}}", + "email_label": "Email", + "code_label": "Reset Code", + "new_password_label": "New Password", + "confirm_password_label": "Confirm New Password", + "submit_send": "Send Reset Code", + "submit_reset": "Reset Password", + "sending": "Sending...", + "resetting": "Resetting...", + "back_to_login": "Back to log in", + "remember_password": "Remember your password?", + "login": "Sign in", + "success_send": "Reset code sent to your email!", + "success_reset": "Password reset successfully!", + "success_title": "Password Reset!", + "success_subtitle": "Redirecting to sign in...", + "error_send": "Failed to send reset code", + "error_reset": "Reset failed", + "error_invalid_otp": "Invalid or expired OTP." + }, + "not_found": { + "title": "404 - Page Not Found", + "subtitle": "Sorry, the page you're looking for doesn't exist.", + "back_home": "Go back home" } } diff --git a/src/locales/vi.json b/src/locales/vi.json index 21f2224..0c9125f 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -20,26 +20,132 @@ "admin": "Quản trị" }, "home": { - "title": "Rút gọn liên kết của bạn", - "subtitle": "Công cụ rút gọn URL mạnh mẽ với phân tích và hơn thế nữa.", - "shorten_btn": "Rút gọn", - "placeholder": "Nhập URL dài của bạn tại đây...", - "custom_alias": "Bí danh tùy chỉnh (tùy chọn)" + "title": "Rút gọn liên kết", + "title_highlight": "Mở rộng tầm với", + "subtitle": "LinkForge giúp bạn tạo, theo dõi và quản lý liên kết một cách dễ dàng, mạnh mẽ và an toàn.", + "shorten_btn": "Rút gọn ngay", + "processing": "Đang xử lý...", + "placeholder": "Dán URL dài của bạn tại đây...", + "already_logged_in": "Bạn đã đăng nhập vào LinkForge.", + "go_to_dashboard": "Đi đến Bảng điều khiển", + "success_msg": "Rút gọn liên kết thành công!", + "error_default": "Đã xảy ra lỗi. Vui lòng thử lại sau.", + "copied": "Đã sao chép vào bộ nhớ tạm!", + "result_title": "Liên kết đã rút gọn của bạn", + "delete_token_title": "Mã xóa (Hãy lưu lại!)", + "guest_warning": "Vì bạn tạo liên kết này với tư cách khách, bạn phải lưu mã này nếu muốn", + "delete_link_text": "xóa liên kết", + "original_label": "Gốc:" }, "login": { "title": "Chào mừng trở lại", - "email": "Địa chỉ Email", + "subtitle": "Vui lòng nhập thông tin để đăng nhập.", + "email": "Email", "password": "Mật khẩu", "submit": "Đăng nhập", + "signing_in": "Đang đăng nhập...", "forgot_password": "Quên mật khẩu?", - "no_account": "Chưa có tài khoản? Đăng ký ngay" + "no_account": "Chưa có tài khoản?", + "signup": "Đăng ký ngay", + "success_msg": "Chào mừng trở lại, {{name}}!", + "error_verify": "Vui lòng xác thực email trước", + "error_invalid": "Email hoặc mật khẩu không đúng. Vui lòng thử lại." }, "register": { "title": "Tạo tài khoản", + "subtitle": "Tham gia LinkForge ngay hôm nay.", "username": "Tên người dùng", - "email": "Địa chỉ Email", + "email": "Email", "password": "Mật khẩu", - "submit": "Đăng ký", - "has_account": "Đã có tài khoản? Đăng nhập ngay" + "submit": "Tạo tài khoản", + "creating": "Đang tạo tài khoản...", + "has_account": "Đã có tài khoản?", + "login": "Đăng nhập", + "success_msg": "Đã tạo tài khoản! Vui lòng kiểm tra email để xác thực.", + "error_password_match": "Mật khẩu không khớp" + }, + "dashboard": { + "create_title": "Tạo liên kết rút gọn mới", + "shorten_btn": "Rút gọn", + "vip_ad_bypass": "Muốn bỏ qua hoàn toàn các trang quảng cáo cho khách truy cập của bạn?", + "upgrade_vip": "Nâng cấp lên VIP ngay hôm nay!", + "just_created": "Vừa tạo xong", + "my_links": "Liên kết của tôi", + "total_links": "Tổng cộng {{count}} liên kết", + "total_links_plural": "Tổng cộng {{count}} liên kết", + "search_placeholder": "Tìm kiếm liên kết...", + "no_links": "Chưa có liên kết nào", + "create_first": "Hãy tạo liên kết rút gọn đầu tiên của bạn ở trên!", + "status_expired": "Đã hết hạn", + "status_never": "Không bao giờ", + "qr_modal_title": "Mã QR", + "qr_not_generated": "Chưa có mã QR nào được tạo", + "qr_generate": "Tạo mã QR", + "qr_delete": "Xóa mã QR", + "qr_vip_only": "Mã QR chỉ dành riêng cho thành viên VIP.", + "qr_scan_share": "Quét để chia sẻ {{url}}", + "delete_qr_title": "Xóa mã QR?", + "delete_qr_desc": "Hành động này sẽ xóa vĩnh viễn mã QR cho liên kết này. Bạn luôn có thể tạo lại sau.", + "delete_link_title": "Xóa liên kết này?", + "delete_link_desc": "Hành động này không thể hoàn tác. Tất cả dữ liệu phân tích cho liên kết này cũng sẽ bị mất vĩnh viễn.", + "confirm_delete_qr": "Vâng, Xóa mã QR", + "confirm_delete_link": "Xác nhận xóa" + }, + "table": { + "created": "Đã tạo", + "expires": "Hết hạn", + "url": "URL", + "clicks": "Lượt nhấp", + "actions": "Hành động" + }, + "pagination": { + "page_info": "Trang {{current}} trên {{total}}", + "show": "Hiển thị", + "first": "Trang đầu", + "prev": "Trang trước", + "next": "Trang sau", + "last": "Trang cuối" + }, + "verify_email": { + "title": "Xác thực email", + "subtitle": "Chúng tôi đã gửi mã gồm 6 chữ số đến {{email}}", + "code_label": "Mã xác thực", + "submit": "Xác thực Email", + "verifying": "Đang xác thực...", + "resend_btn": "Gửi lại mã", + "resend_cooldown": "Gửi lại mã sau {{count}} giây", + "resend_success": "Mã xác thực mới đã được gửi!", + "success_title": "Email đã được xác thực!", + "success_subtitle": "Đang chuyển hướng đến trang đăng nhập...", + "error_default": "Xác thực thất bại" + }, + "forgot_password": { + "title": "Quên mật khẩu?", + "reset_title": "Đặt lại mật khẩu", + "subtitle": "Nhập email của bạn và chúng tôi sẽ gửi mã đặt lại.", + "reset_subtitle": "Nhập mã đã được gửi đến {{email}}", + "email_label": "Email", + "code_label": "Mã đặt lại", + "new_password_label": "Mật khẩu mới", + "confirm_password_label": "Xác nhận mật khẩu mới", + "submit_send": "Gửi mã đặt lại", + "submit_reset": "Đặt lại mật khẩu", + "sending": "Đang gửi...", + "resetting": "Đang đặt lại...", + "back_to_login": "Quay lại đăng nhập", + "remember_password": "Đã nhớ mật khẩu?", + "login": "Đăng nhập", + "success_send": "Mã đặt lại đã được gửi đến email của bạn!", + "success_reset": "Đặt lại mật khẩu thành công!", + "success_title": "Đã đặt lại mật khẩu!", + "success_subtitle": "Đang chuyển hướng đến trang đăng nhập...", + "error_send": "Gửi mã đặt lại thất bại", + "error_reset": "Đặt lại thất bại", + "error_invalid_otp": "Mã OTP không hợp lệ hoặc đã hết hạn." + }, + "not_found": { + "title": "404 - Không tìm thấy trang", + "subtitle": "Rất tiếc, trang bạn đang tìm kiếm không tồn tại.", + "back_home": "Quay lại trang chủ" } } From 35f4b12cae1d4dd4d42ba0300660f6cd29eb4a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C6=B0=C6=A1ng=20L=C3=AA=20Anh=20V=C5=A9?= Date: Thu, 19 Mar 2026 10:43:46 +0700 Subject: [PATCH 4/9] feat(i18n): implement LanguageSwitcher and update RootLayout --- src/components/LanguageSwitcher.tsx | 22 ++++++++++++++++++++++ src/layouts/RootLayout.tsx | 19 +++++++++++-------- 2 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 src/components/LanguageSwitcher.tsx diff --git a/src/components/LanguageSwitcher.tsx b/src/components/LanguageSwitcher.tsx new file mode 100644 index 0000000..d68cc0a --- /dev/null +++ b/src/components/LanguageSwitcher.tsx @@ -0,0 +1,22 @@ +import { useTranslation } from 'react-i18next' +import { Globe } from 'lucide-react' + +export default function LanguageSwitcher() { + const { i18n } = useTranslation() + + const toggleLanguage = () => { + const newLang = i18n.language === 'en' ? 'vi' : 'en' + i18n.changeLanguage(newLang) + } + + return ( + + ) +} diff --git a/src/layouts/RootLayout.tsx b/src/layouts/RootLayout.tsx index 7aca77f..5c47625 100644 --- a/src/layouts/RootLayout.tsx +++ b/src/layouts/RootLayout.tsx @@ -1,9 +1,12 @@ import { Outlet, Link, useNavigate } from 'react-router-dom' import { Toaster } from 'react-hot-toast' +import { useTranslation } from 'react-i18next' import { useAuthStore } from '../store/useAuthStore' import { apiClient } from '../api/axios' +import LanguageSwitcher from '../components/LanguageSwitcher' export default function RootLayout() { + const { t } = useTranslation() const { isAuthenticated, user, clearAuth } = useAuthStore() const navigate = useNavigate() @@ -27,26 +30,26 @@ export default function RootLayout() { LinkForge - @@ -85,8 +88,8 @@ export default function DashboardLayout() {
{!(location.pathname.includes('/vip-upgrade') || location.pathname.includes('/payment-success')) && ( <> -

Dashboard

-

Welcome back, {user?.name}

+

{t('dashboard.sidebar.dashboard')}

+

{t('dashboard.welcome_back', { name: user?.name })}

)}
@@ -94,10 +97,14 @@ export default function DashboardLayout() {
{user?.vip && ( - VIP Active + {t('dashboard.vip_active')} )} +
+ +
+