diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts index 7f2d303..4ed806d 100644 --- a/frontend/src/i18n/index.ts +++ b/frontend/src/i18n/index.ts @@ -1,7 +1,6 @@ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import zhCN from '@/locales/zh-CN.json'; -import enUS from '@/locales/en-US.json'; export type AppLanguage = 'zh-CN' | 'en-US'; @@ -12,6 +11,24 @@ export const SUPPORTED_LANGUAGES: { code: AppLanguage; label: string; flag: stri export const DEFAULT_LANGUAGE: AppLanguage = 'zh-CN'; +/** Track which locale resource bundles have been registered with i18next. */ +const loadedLocales = new Set(['zh-CN']); + +/** + * Ensure a locale resource bundle is loaded before use. + * zh-CN is bundled inline (default/fallback language). + * en-US is lazy-loaded via dynamic import to reduce the main bundle by ~290 kB. + * Subsequent calls for an already-loaded locale are instant no-ops. + */ +export async function ensureLocaleLoaded(lang: string): Promise { + if (loadedLocales.has(lang)) return; + if (lang === 'en-US') { + const mod = await import('@/locales/en-US.json'); + i18n.addResourceBundle('en-US', 'translation', mod.default, true, true); + loadedLocales.add('en-US'); + } +} + /** Read persisted language from localStorage (zustand persist key) */ function getPersistedLanguage(): AppLanguage { try { @@ -27,12 +44,15 @@ function getPersistedLanguage(): AppLanguage { return DEFAULT_LANGUAGE; } +const persistedLang = getPersistedLanguage(); + i18n.use(initReactI18next).init({ resources: { 'zh-CN': { translation: zhCN }, - 'en-US': { translation: enUS }, }, - lng: getPersistedLanguage(), + // Start with zh-CN if the persisted language (en-US) hasn't been loaded yet. + // en-US is fetched asynchronously below and the language is switched once ready. + lng: persistedLang === 'en-US' ? 'zh-CN' : persistedLang, fallbackLng: 'zh-CN', interpolation: { escapeValue: false, // React already escapes @@ -42,6 +62,15 @@ i18n.use(initReactI18next).init({ }, }); +// If the user previously selected en-US, load it asynchronously then switch. +// There is a brief flash of Chinese before English loads — acceptable trade-off +// for a ~290 kB reduction in the main entry chunk. +if (persistedLang === 'en-US') { + void ensureLocaleLoaded('en-US').then(() => { + i18n.changeLanguage('en-US'); + }); +} + // Sync with current language i18n.on('languageChanged', (lng) => { if (typeof document !== 'undefined') { diff --git a/frontend/src/store/index.ts b/frontend/src/store/index.ts index 14b9db6..fcd3271 100644 --- a/frontend/src/store/index.ts +++ b/frontend/src/store/index.ts @@ -29,9 +29,10 @@ export const useLanguageStore = create()( (set) => ({ language: "zh-CN" as AppLanguage, setLanguage: (lang) => { - // 同步切换 i18next 实例 + // Ensure the target locale bundle is loaded (en-US is lazy) before switching. import("@/i18n") - .then(({ default: i18n }) => { + .then(async ({ default: i18n, ensureLocaleLoaded }) => { + await ensureLocaleLoaded(lang); i18n.changeLanguage(lang); }) .catch(() => {