perf(i18n): lazy-load non-zh-CN locales — shared index chunk 440KB→285KB

All 5 locales (en/ja/ko/zh-CN/zh-TW) were statically bundled into the index chunk that every one of the 5 webviews loads. Keep only zh-CN (fallback + source of truth) inline so init stays synchronous; lazy-load the other 4 as separate chunks on demand (initial non-zh-CN locale loads async after first paint, then changeLanguage; setLocalePreference awaits the locale before switching). Worst case = graceful zh-CN fallback, never broken keys. Cuts ~155KB raw / ~75KB gzip from the per-webview index; zh-CN users never load the other 4.

Build-verified: index 285KB + en/ja/ko/zh-TW separate chunks. Runtime-sanity: switch language in settings (en/ja/ko/zh-TW) to confirm it loads + applies.
This commit is contained in:
Andy Jiang committed 2026-06-15 13:13:13 +08:00
1 parent 1c0f55716c
commit cab7dc205d
1 file changed
+30 -13
+30 -13
View File
@@ -10,11 +10,7 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import { en } from './en';
import { ja } from './ja';
import { ko } from './ko';
import { zhCN } from './zh-CN';
import { zhTW } from './zh-TW';
import type { UserPreferences } from '../lib/types';
import { setRemoteLocale } from '../lib/ipc';
@@ -49,22 +45,42 @@ function getStoredLocale(): SupportedLocale | null {
const initialLng: SupportedLocale = getStoredLocale() ?? detectSystemLocale();
// 只把 fallback / source-of-truth 的 zh-CN 静态打进 index —— 5 个 webview 都加载 index,
// 把 en / ja / ko / zh-TW 四份语言包拆成按需 chunk,减小每个 webview 的常驻体积。
// 代价:非 zh-CN 用户首屏先显示 zh-CN 一瞬,语言包异步到位后切换;最坏情况(加载失败)
// 也只停在 zh-CN(可读),不会出现 key 字面量。
const localeLoaders: Record<Exclude<SupportedLocale, 'zh-CN'>, () => Promise<typeof zhCN>> = {
'zh-TW': () => import('./zh-TW').then(m => m.zhTW),
en: () => import('./en').then(m => m.en),
ja: () => import('./ja').then(m => m.ja),
ko: () => import('./ko').then(m => m.ko),
};
async function ensureLocaleLoaded(lng: SupportedLocale): Promise<void> {
if (lng === 'zh-CN' || i18n.hasResourceBundle(lng, 'translation')) return;
try {
i18n.addResourceBundle(lng, 'translation', await localeLoaders[lng]());
} catch (e) {
console.warn('[i18n] load locale failed, staying on zh-CN fallback:', lng, e);
}
}
void i18n.use(initReactI18next).init({
resources: {
'zh-CN': { translation: zhCN },
'zh-TW': { translation: zhTW },
en: { translation: en },
ja: { translation: ja },
ko: { translation: ko },
},
// 仅内联 zh-CN(fallback)。init 仍同步完成,首屏 t() 立刻可用(非 zh-CN 暂回退 zh-CN)。
resources: { 'zh-CN': { translation: zhCN } },
lng: initialLng,
fallbackLng: 'zh-CN',
supportedLngs: SUPPORTED_LOCALES as unknown as string[],
partialBundledLanguages: true, // 告诉 i18next 我们的内联资源已完整,无需 backend 拉取
partialBundledLanguages: true, // 告诉 i18next 内联资源已完整,无需 backend 拉取
interpolation: { escapeValue: false },
react: { useSuspense: false }, // 不悬挂;首次渲染必须能拿到译文(无 backend 时 init 同步完成)
react: { useSuspense: false }, // 不悬挂;首屏必须能拿到译文(zh-CN 已内联,同步完成)
});
// 初始语言非 zh-CN:异步补加载该语言包后切换(首屏先 zh-CN,到位后无缝切过去)。
if (initialLng !== 'zh-CN') {
void ensureLocaleLoaded(initialLng).then(() => i18n.changeLanguage(initialLng));
}
export default i18n;
/**
@@ -88,6 +104,7 @@ export async function setLocalePreference(
} else {
window.localStorage.setItem(LOCALE_STORAGE_KEY, pref);
}
await ensureLocaleLoaded(resolved);
await i18n.changeLanguage(resolved);
syncRemoteLocale(resolved);
return resolved;