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",
diff --git a/src/api/axios.ts b/src/api/axios.ts
index 759c339..2df774e 100644
--- a/src/api/axios.ts
+++ b/src/api/axios.ts
@@ -1,5 +1,6 @@
import axios from 'axios'
import { useAuthStore } from '../store/useAuthStore'
+import i18n from '../i18n'
const baseURL = import.meta.env.VITE_API_URL
? `${import.meta.env.VITE_API_URL}/api/v1`
@@ -18,6 +19,11 @@ apiClient.interceptors.request.use(
if (user?.accessToken && config.headers) {
config.headers.Authorization = `Bearer ${user.accessToken}`
}
+
+ if (config.headers) {
+ config.headers['Accept-Language'] = i18n.language || 'en'
+ }
+
return config
},
(error) => Promise.reject(error)
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/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/layouts/AuthLayout.tsx b/src/layouts/AuthLayout.tsx
index 1244dcd..a75e0c3 100644
--- a/src/layouts/AuthLayout.tsx
+++ b/src/layouts/AuthLayout.tsx
@@ -1,9 +1,12 @@
import { Outlet, Navigate, Link } from 'react-router-dom'
import { useAuthStore } from '../store/useAuthStore'
import { Toaster } from 'react-hot-toast'
+import { useTranslation } from 'react-i18next'
+import LanguageSwitcher from '../components/LanguageSwitcher'
export default function AuthLayout() {
const isAuthenticated = useAuthStore((state) => state.isAuthenticated)
+ const { t } = useTranslation()
if (isAuthenticated) {
return
Manage your links in one place
+{t('login.manage_links')}
Welcome back, {user?.name}
+{t('dashboard.welcome_back', { name: user?.name })}
> )}View and manage system users and VIP access
+{t('admin.subtitle')}