feat(i18n): U3 国际化 — react-i18next 双语框架 + 侧边栏/Header/Login 全量翻译 + 语言切换器

- 安装 i18next + react-i18next,创建 src/i18n/ 配置实例
- 创建 zh-CN.json (120+ 键) + en-US.json 全量英文翻译
- 侧边栏 42+ 菜单项全量 labelKey 国际化
- Header 通知/主题/用户菜单全量翻译
- Login 页面表单/按钮/主题+语言切换器
- zustand useLanguageStore 持久化语言选择
- HTML lang 属性自动同步
- tsc -b=0, npm build=✅, 17/17 测试通过
This commit is contained in:
attacker committed 2026-06-27 16:54:07 +08:00
1 parent 084a3be2cf
commit 2e179ef7f4
15 files changed
+860 -112

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
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';
export const SUPPORTED_LANGUAGES: { code: AppLanguage; label: string; flag: string }[] = [
{ code: 'zh-CN', label: '中文', flag: '🇨🇳' },
{ code: 'en-US', label: 'English', flag: '🇺🇸' },
];
export const DEFAULT_LANGUAGE: AppLanguage = 'zh-CN';
/** Read persisted language from localStorage (zustand persist key) */
function getPersistedLanguage(): AppLanguage {
try {
const raw = localStorage.getItem('language-storage');
if (raw) {
const parsed = JSON.parse(raw);
const lang = parsed?.state?.language;
if (lang === 'zh-CN' || lang === 'en-US') return lang;
}
} catch {
// ignore parse errors
}
return DEFAULT_LANGUAGE;
}
i18n.use(initReactI18next).init({
resources: {
'zh-CN': { translation: zhCN },
'en-US': { translation: enUS },
},
lng: getPersistedLanguage(),
fallbackLng: 'zh-CN',
interpolation: {
escapeValue: false, // React already escapes
},
react: {
useSuspense: false,
},
});
// Sync <html lang="..."> with current language
i18n.on('languageChanged', (lng) => {
if (typeof document !== 'undefined') {
document.documentElement.lang = lng;
}
});
export default i18n;