diff --git a/i18n/en.json b/i18n/en.json new file mode 100644 index 0000000..e69de29 diff --git a/i18n/zh.json b/i18n/zh.json new file mode 100644 index 0000000..e69de29 diff --git a/i18next.config.ts b/i18next.config.ts new file mode 100644 index 0000000..db4f725 --- /dev/null +++ b/i18next.config.ts @@ -0,0 +1,32 @@ +export default { + // supported languages + locales: ["en", "zh", "de", "fr"], + + // extraction config (for i18next-cli) + extract: { + input: "src/**/*.{js,jsx,ts,tsx}", + output: "public/locales/{{language}}/{{ns}}.json", + namespaceSeparator: false, + }, + + // default namespace + defaultNamespace: "translation", + + // output path pattern + resource: { + jsonIndent: 2, + lineEnding: "\n", + }, + + // sort keys in output + sort: true, + + // fail on missing keys during dev + // quiet: false, + + // metadata for translation files + metadata: { + description: "Generated by i18next-cli", + generatedAt: new Date().toISOString(), + }, +}; \ No newline at end of file diff --git a/src/lib/i18n.ts b/src/lib/i18n.ts new file mode 100644 index 0000000..9c4d9c1 --- /dev/null +++ b/src/lib/i18n.ts @@ -0,0 +1,28 @@ +import i18n from 'i18next'; +import {initReactI18next} from 'react-i18next'; +import HttpBackend from 'i18next-http-backend'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +i18n + .use(HttpBackend) + .use(LanguageDetector) + .use(initReactI18next) + .init({ + fallbackLng: 'en', + supportedLngs: ['en', 'zh', 'de', 'fr'], + defaultNS: 'translation', + ns: ['translation'], + backend: { + loadPath: '/locales/{{lng}}/{{ns}}.json', + }, + detection: { + order: ['localStorage', 'navigator'], + caches: ['localStorage'], + lookupLocalStorage: 'i18nextLng', + }, + interpolation: { + escapeValue: false, + }, + }); + +export default i18n; diff --git a/src/lib/language-provider.tsx b/src/lib/language-provider.tsx new file mode 100644 index 0000000..91fa031 --- /dev/null +++ b/src/lib/language-provider.tsx @@ -0,0 +1,32 @@ +import {useQuery} from '@tanstack/react-query'; +import {type ReactNode, useEffect} from 'react'; +import {getPreferences} from '@/client'; +import {useUser} from '@/contexts'; +import i18n from '@/lib/i18n'; + +/** + * Syncs the user's server-side language preference to i18n after login. + * This ensures the API-stored preference takes precedence over browser detection. + */ +export function LanguageProvider({children}: {children: ReactNode}) { + const {isAuthenticated} = useUser(); + + const {data: preferences} = useQuery({ + queryKey: ['userPreferences'], + queryFn: async () => { + const resp = await getPreferences(); + return resp.data?.data ?? null; + }, + enabled: isAuthenticated, + staleTime: 5 * 60 * 1000, + retry: false, + }); + + useEffect(() => { + if (preferences?.language && preferences.language !== i18n.language) { + i18n.changeLanguage(preferences.language); + } + }, [preferences]); + + return children; +}