AionUi 多语言 i18n 架构与实战指南:从 i18next 初始化到 13 语言全球化
AionUi 多语言 i18n 架构与实战指南从 i18next 初始化到 13 语言全球化【免费下载链接】AionUi免费、本地、开源的 24/7 全天候 Cowork 应用以及适用于 Gemini CLI、Claude Code、Codex、OpenCode、Qwen Code、Goose CLI、Auggie 等的 OpenClaw | 喜欢就点star吧项目地址: https://gitcode.com/iOfficeAI/AionUiAionUi 是一个基于 Electron 的本地 AI Cowork 桌面应用其渲染层renderer采用 i18next react-i18next 构建了一套可扩展的多语言体系支持简体中文、英文、日文、繁体中文、韩文、土耳其文、俄文、乌克兰文、葡萄牙文巴西、德文、西班牙文、法文与波斯文共 13 种语言。本文以仓库中的 i18n README 为核心脉络结合 i18n 初始化源码、语言配置、语言切换器组件 与 i18n 校验脚本 等实现证据系统讲解语言包的目录结构、初始化与切换机制、翻译键命名规范、新增语言包的完整流程以及数字、日期格式化和 RTL 布局等进阶主题。读完本文你将能在 AionUi 中独立完成翻译键的添加、新语言包的接入与校验并理解这套体系在桌面端与 WebUI 双端同步场景下的设计取舍。一、技术选型与总体架构AionUi 的渲染层选择i18next react-i18next作为多语言基础设施见 README 首行。其中i18next是核心的国际化引擎负责语言包资源管理、语言切换与回退fallbackreact-i18next是面向 React 的绑定层提供useTranslation()Hook让组件在语言切换时自动重渲染。围绕这两者仓库在packages/desktop/src/renderer/services/i18n/目录下组织了一整套服务层代码文件职责index.tsi18next 实例初始化、语言探测、懒加载与主进程同步list.ts基于Intl.ListFormat的本地化列表拼接format.ts基于Intl.*的数字、货币、日期、字节大小格式化direction.ts依据应用语言推导文档方向LTR/RTLi18n-keys.d.ts自动生成的翻译键联合类型I18nKeylocales/各语言的语言包目录值得注意的是当前仓库实际实现的架构与 README 中最初的“单一zh-CN.json/en-US.json”结构相比已经大幅演进语言包被拆分为按功能模块组织的多个 JSON 文件每个语言目录下还有聚合入口index.ts例如 zh-CN/index.ts。README 中描述的“两个语言包”是最简模型而真实仓库已将其扩展为 13 语言的完整体系本文后续将基于真实代码结构展开。二、目录结构与语言包组织2.1 语言包目录所有语言包位于packages/desktop/src/renderer/services/i18n/locales/每个语言一个子目录目录名采用标准 BCP 47 标签连字符分隔locales/ ├── zh-CN/ # 简体中文 ├── en-US/ # 英文 ├── ja-JP/ # 日文 ├── zh-TW/ # 繁体中文 ├── ko-KR/ # 韩文 ├── tr-TR/ # 土耳其文 ├── ru-RU/ # 俄文 ├── uk-UA/ # 乌克兰文 ├── pt-BR/ # 葡萄牙文巴西 ├── de-DE/ # 德文 ├── es-ES/ # 西班牙文 ├── fr-FR/ # 法文 └── fa-IR/ # 波斯文唯一的 RTL 语言每个语言目录下包含统一的模块文件集合由 i18n-config.json 中的modules数组定义共 19 个模块common、agentMode、update、login、fileSelection、preview、conversation、settings、messages、mcp、acp、codex、tools、google、cron、guid、agent、team、pet。以 zh-CN/ 为例zh-CN/ ├── index.ts # 聚合所有模块 JSON 并默认导出 ├── common.json # 通用按钮、文案 ├── conversation.json # 会话与聊天界面 ├── settings.json # 设置界面 ├── acp.json # ACP 代理协议相关 ├── agent.json # Agent 运行时相关 ├── cron.json # 定时任务 ├── preview.json # 文件预览 ├── team.json # 团队协作 ├── pet.json # 桌面宠物 └── ... # 其余模块每个index.ts负责把本语言的各模块 JSON 导入并聚合导出见 zh-CN/index.tsimport common from ./common.json; import conversation from ./conversation.json; // ...其余模块 export default { common, conversation, // ...其余模块 };模块化拆分的好处翻译键天然按功能分区多人协作时冲突面更小同时 i18next 支持按命名空间namespace加载为未来的按需加载留出了空间。2.2 配置中心i18n-config.json语言支持情况与模块清单由 i18n-config.json 统一声明它是主进程与渲染进程共享的单一事实来源single source of truth{ referenceLanguage: en-US, fallbackLanguage: en-US, supportedLanguages: [ zh-CN, en-US, ja-JP, zh-TW, ko-KR, tr-TR, ru-RU, uk-UA, pt-BR, de-DE, es-ES, fr-FR, fa-IR ], modules: [common, agentMode, update, login, fileSelection, preview, conversation, settings, messages, mcp, acp, codex, tools, google, cron, guid, agent, team, pet] }字段语义referenceLanguageen-US翻译基准语言。校验脚本以它为基准比对各语言键的完整性fallbackLanguageen-US回退语言。当某语言缺少某个键时i18next 会回退到该语言包取值supportedLanguages应用实际支持并随包分发的语言列表modules每个语言必须提供的翻译模块清单。该配置被 i18n.ts 读取并导出为SUPPORTED_LANGUAGES、DEFAULT_LANGUAGE常量供主进程与渲染进程两侧共用。三、初始化流程与语言探测策略3.1 不使用浏览器语言探测器一个值得注意的设计决策是AionUi 刻意不使用i18next-browser-languagedetector。在 index.ts 的初始化注释中明确说明了原因在 WebUI 模式下浏览器的 localStorage 与 Electron 渲染进程位于不同的 origin语言探测器会读取到错误或缺失的值并回退到navigator.language导致语言不一致Issue #1176。因此项目采用自定义的多级语言探测策略核心逻辑在getInitialLanguage()中function getInitialLanguage(): SupportedLanguage { const backendStartupFailed ...; const localStorageLanguage getLocalStorageLanguageHint(); // localStorage 的 i18nextLng const injectedLanguage getInjectedLanguageHint(); // window.__initialLanguage const systemLanguage backendStartupFailed ? getElectronSystemLanguageHint() : null; const hint backendStartupFailed ? injectedLanguage || localStorageLanguage || systemLanguage : localStorageLanguage || injectedLanguage; return normalizeLanguageCode(hint || DEFAULT_LANGUAGE); }语言提示来源优先级如下localStorage 中的i18nextLng用户上次选择的语言普通启动时的首选window.__initialLanguageElectron 注入的本地配置语言提示navigator.language仅在后端启动失败__backendStartupFailed时作为兜底用于保证失败页也能以用户系统语言呈现以上都不可用时回退到DEFAULT_LANGUAGEen-US。3.2 同步加载回退语言避免 FOUC为避免首帧出现错误的语言闪烁FOUC初始化时会将默认回退语言与初始语言同步注入 i18next 的 resourcesconst initialResources { [DEFAULT_LANGUAGE]: { translation: fallbackLocale }, }; if (initialLanguage ! DEFAULT_LANGUAGE) { initialResources[initialLanguage] { translation: getLocaleModules(initialLanguage) }; } i18n.use(initReactI18next).init({ resources: initialResources, lng: initialLanguage, fallbackLng: DEFAULT_LANGUAGE, interpolation: { escapeValue: false }, });interpolation.escapeValue: false是 react-i18next 的标准配置——React 自身已经处理了 XSS 转义无需 i18next 再转义一次。3.3 configService 作为语言权威来源初始化完成后initLanguage()会等待configService.whenReady()从后端配置中读取权威的语言设置并切换async function initLanguage(): Promisevoid { await configService.whenReady(); const savedLanguage configService.get(language); const language savedLanguage || normalizeLanguageCode(navigator.language || DEFAULT_LANGUAGE); await ensureAndSwitch(i18n, language, loadLocaleModules); localStorage.setItem(i18nextLng, normalizeLanguageCode(language)); }这套流程形成了清晰的层级localStorage / 注入提示只是“快速提示”后端 configService 才是权威真相。语言选择落盘到后端配置后即使清空浏览器缓存语言也不会丢失。3.4 语言切换的懒加载i18next 初始化后通过监听languageChanged事件实现按需加载语言包i18n.on(languageChanged, async (lang: string) { const normalizedLang normalizeLanguageCode(lang); if (i18n.hasResourceBundle(normalizedLang, translation)) return; const translation await loadLocaleModules(normalizedLang); i18n.addResourceBundle(normalizedLang, translation, translation, true, true); });语言包本身是静态 import见 index.ts 顶部的一系列import enUS from ./locales/en-US/index以保证打包后的应用始终可以离线切换任意语言无需网络请求。3.5 语言代码归一化normalizeLanguageCode用户提供的语言代码可能是zh、ja_JP、zh-Hant等多种形态i18n.ts 中的normalizeLanguageCode()负责将其归一为受支持的 BCP 47 标签export function normalizeLanguageCode(language: string): SupportedLanguage { const normalized language.replace(/_/g, -); if (SUPPORTED_LANGUAGES.includes(normalized)) return normalized; const lower normalized.toLowerCase(); // 繁体中文区域的读者期望 zh-TW不能降级为简体 if (lower.startsWith(zh)) { return /\bhant\b|-hk\b|-mo\b/.test(lower) ? zh-TW : zh-CN; } // 按语言主码映射ja→ja-JP、ko→ko-KR、fa→fa-IR ... const langOnly lower.split(-)[0]; switch (langOnly) { /* ... */ default: return DEFAULT_LANGUAGE; } }一个关键的细节是zh-HK、zh-MO、zh-Hant-*会归一化为zh-TW而非zh-CN避免繁体字区域用户被错误地切到简体中文。3.6 回退合并mergeWithFallback当某语言缺少部分翻译键时AionUi 并不完全依赖 i18next 的运行时回退而是在加载时就做深度合并export function mergeWithFallback(fallback, target) { const merged { ...fallback }; for (const [key, value] of Object.entries(target)) { if (isPlainObject(merged[key]) isPlainObject(value)) { merged[key] mergeWithFallback(merged[key], value); // 递归合并 } else { merged[key] value; } } return merged; }在 index.ts 的getLocaleModules()中非默认语言加载时都会调用mergeWithFallback(fallbackLocale, modules)确保缺失键在运行时也能拿到兜底文案。四、在组件中使用翻译4.1 useTranslation Hook 的基本用法在任意 React 组件中通过useTranslation()获取t函数import { useTranslation } from react-i18next; const MyComponent () { const { t } useTranslation(); return ( div h1{t(common.title)}/h1 p{t(common.description)}/p /div ); };t(key)支持点号分隔的嵌套键路径例如t(common.send)会解析到common模块下send键的值。react-i18next 会在i18n.language变化时自动触发使用该 Hook 的组件重渲染。4.2 完整可用的语言切换器实现仓库真实的语言切换器位于 LanguageSwitcher.tsx它比 README 中的简化示例更完整可作为生产级参考import AionSelect from /renderer/components/base/AionSelect; import { useTranslation } from react-i18next; import { changeLanguage } from /renderer/services/i18n; const LanguageSwitcher: React.FC () { const { i18n } useTranslation(); const selectRef useRefSelectHandle(null); const handleLanguageChange useCallback((value: string) { // 先 blur 触发元素避免下拉层与语言切换竞争布局 selectRef.current?.blur?.(); const applyLanguage () { changeLanguage(value).catch((error: Error) { console.error(Failed to change language:, error); }); }; // 延迟到下一帧执行确保 DOM 动画完成 if (typeof window ! undefined requestAnimationFrame in window) { window.requestAnimationFrame(() window.requestAnimationFrame(applyLanguage)); } else { setTimeout(applyLanguage, 0); } }, []); return ( div classNameflex items-center gap-8px AionSelect ref{selectRef} classNamew-160px value{i18n.language} onChange{handleLanguageChange} AionSelect.Option valuezh-CN简体中文/AionSelect.Option AionSelect.Option valuezh-TW繁體中文/AionSelect.Option AionSelect.Option valueja-JP日本語/AionSelect.Option AionSelect.Option valueko-KR한국어/AionSelect.Option AionSelect.Option valuetr-TRTürkçe/AionSelect.Option AionSelect.Option valueru-RUРусский/AionSelect.Option AionSelect.Option valueuk-UAУкраїнська/AionSelect.Option AionSelect.Option valuept-BRPortuguês (BR)/AionSelect.Option AionSelect.Option valuede-DEDeutsch/AionSelect.Option AionSelect.Option valuees-ESEspañol/AionSelect.Option AionSelect.Option valuefr-FRFrançais/AionSelect.Option AionSelect.Option valuefa-IRفارسی/AionSelect.Option AionSelect.Option valueen-USEnglish/AionSelect.Option /AionSelect /div ); };该组件体现了两个工程细节一是切换前先 blur 下拉框避免弹层与语言切换互相干扰布局二是通过双重requestAnimationFrame把changeLanguage推迟到下一帧确保 DOM 动画结束后再触发重渲染。4.3 切换语言的核心函数 changeLanguage组件最终调用的是 index.ts 导出的changeLanguage()它的完整链路是export async function changeLanguage(lang: string): Promisevoid { await ensureAndSwitch(i18n, lang, loadLocaleModules); // 加载语言包并切换 const normalized normalizeLanguageCode(lang); await configService.set(language, normalized); // 写入后端配置权威 localStorage.setItem(i18nextLng, normalized); // 同步 localStorage 提示 ipcBridge.systemSettings.changeLanguage.invoke({ language: normalized }); // 通知主进程 }它依次完成四件事加载并切换通过ensureAndSwitch()见 i18n.ts判断语言包是否已加载未加载则先addResourceBundle再changeLanguage持久化到后端配置configService.set(language, ...)保证重启后语言生效同步 localStorage让 WebUI 下次加载时能快速命中通知主进程通过 IPC 广播语言变更驱动系统托盘菜单等主进程侧文案同步更新。4.4 桌面端与 WebUI 的实时同步index.ts 还监听了主进程广播的语言变更事件ipcBridge.systemSettings.languageChanged.on(async ({ language }) { const normalized normalizeLanguageCode(language); if (i18n.language normalized) return; // 自己触发的变更直接跳过 await ensureAndSwitch(i18n, normalized, loadLocaleModules); localStorage.setItem(i18nextLng, normalized); });这意味着桌面端与 WebUI 中任意一端切换语言另一端会立即同步更新无需重启。五、翻译键的命名规范与语言包格式5.1 命名规范README 明确规定了翻译键的命名规范当前仓库的实现完全遵循使用点号分隔的层级结构common.send、conversation.welcome.title使用小写字母和下划线如copySuccess、openInSystemBrowser、fileAttach.uploading等均为小驼峰或下划线风格键内单词以下划线连接按功能模块分组键的顶层命名空间与模块 JSON 文件名一一对应。以 README 中的示例为基础对照仓库真实语言包一个完整的语言包片段zh-CN如下{ common: { send: 发送, cancel: 取消, save: 保存, delete: 删除, confirm: 确定 }, conversation: { welcome: { title: 今天有什么安排 } } }实际仓库中conversation模块要丰富得多例如conversation.welcome.title、conversation.welcome.placeholder、conversation.welcome.newConversation等欢迎区文案见 en-US 的 i18n-keys.d.ts 中conversation.welcome.*键。5.2 键的自动生成类型I18nKey仓库通过 generate-i18n-types.js 从参考语言包自动生成 i18n-keys.d.ts其中导出一个包含全部翻译键字符串字面量的联合类型export type I18nKey | common.send | common.cancel | conversation.welcome.title | conversation.agentError.codes.USER_AGENT_DISCONNECTED.title | /* ... 数百个键 */;该类型在渲染进程的I18nModule类型中被引用export type { I18nKey, I18nModule } from ./i18n-keys让t()的键参数享受完整的 TypeScript 编译期检查——写错键名会在编译阶段直接报错而不是等到运行时才发现文案缺失。5.3 插值Interpolation与复数i18next 的插值语法在语言包中同样可用。例如conversation.minimap.count_one/conversation.minimap.count_other这样的键对就是 i18next 的复数形式_one/_other后缀组件中通过t(conversation.minimap.count, { count })调用i18next 会根据count值自动选择合适的复数形态。六、新增翻译与新增语言的完整流程6.1 新增一个翻译键三步骤在 zh-CN 对应模块 JSON 中添加中文翻译例如在packages/desktop/src/renderer/services/i18n/locales/zh-CN/common.json中加入retry: 重试在 en-US 对应模块 JSON 中添加对应英文翻译参考语言例如在packages/desktop/src/renderer/services/i18n/locales/en-US/common.json中加入retry: Retry在组件中使用t(common.retry)获取翻译。注意由于 en-US 是 referenceLanguage新增键时必须同步更新 en-US 语言包否则校验脚本会报错。6.2 新增一个语言包当前架构下新增语言以新增it-IT为例需要在 i18n-config.json 的supportedLanguages数组中添加it-IT复制任意现有语言目录为locales/it-IT/按 en-US 基准翻译全部 19 个模块 JSON在 index.ts 中静态 import 并注册it-IT: itIT到localeData映射在normalizeLanguageCode()i18n.ts中补充case it: return it-IT;的语言主码映射如语言为 RTL 布局还需在 direction.ts 的RTL_LANGUAGES集合中登记在 LanguageSwitcher.tsx 的下拉选项中添加对应选项以该语言母语名称展示。6.3 自动校验check-i18n.js仓库提供了强大的 i18n 一致性校验脚本 check-i18n.js用于 pre-commit 钩子node scripts/check-i18n.js脚本的校验维度包括目录结构检查每个受支持语言目录是否存在、19 个必需模块文件是否齐全、JSON 语法是否合法、是否残留旧式的单一语言包文件zh-CN.json这种旧格式会被报错要求删除键一致性以 en-USreferenceLanguage为基准比对其他语言包是否缺失键或存在多余键硬编码检测扫描渲染层源码检测未被翻译函数包裹的用户可见文本。✅ Locale directory exists: en-US ❌ Missing module file: it-IT/cron.json Checking translation key consistency...这条流水线保证了 13 种语言在每次提交时都保持键的完全对齐从机制上杜绝了“某语言漏翻”悄悄进入主分支。七、进阶主题本地化格式化与 RTL 布局7.1 为什么不能直接使用 toLocaleString()README 未涉及的另一个重要话题是本地化格式化。AionUi 在 format.ts 中实现了全套格式化工具其动机在文件注释中讲得很清楚Intl.NumberFormat(undefined, …)、Date.prototype.toLocaleString()系列解析的是宿主操作系统的语言环境与用户在 AionUi 内选择的语言无关。一台运行英文版 AionUi 的德文系统会渲染出0,42 $和17.8.2025与英文标签混在一起。因此所有用户可见的数字、日期都必须以应用语言显式格式化语言参数来自useTranslation().i18n.language。工具函数如下函数作用示例formatNumber(value, language)数字格式化de-DE 下12.6→12,6formatCurrency(amount, currency, language)货币格式化含非法货币码兜底非法 code 时回退为12.5 USD形式formatDateTime(value, language)日期时间复刻toLocaleString()默认值formatDate/formatTime纯日期 / 纯时间toLocaleDateString()/toLocaleTimeString()的替代formatByteSize(bytes, language)字节大小12.5 MBde-DE 渲染12,5 MBformatByteRate(bytesPerSecond, language)传输速率1.2 MB/sformatNameList(names, language)人名/列表拼接基于Intl.ListFormatzh 用顿号、fa 用阿拉伯逗号、de/fr 用 und/et实现上还做了性能优化Intl.NumberFormat/Intl.DateTimeFormat的构造相对昂贵而这些格式化函数经常在列表渲染中调用因此 format.ts 以locale|options为键缓存了已构造的 formatter 实例。7.2 RTL波斯文的布局适配方向处理 从应用语言推导文档方向而非宿主系统const RTL_LANGUAGES: ReadonlySetstring new Set([fa-IR]); export function applyDocumentDirection(language) { document.documentElement.dir isRtlLanguage(language) ? rtl : ltr; document.documentElement.lang normalizeLanguageCode(language); }fa-IR波斯文是 AionUi 唯一自带的 RTL 语言。方向切换后html dir驱动渲染层中使用的 CSS 逻辑属性logical properties与 flex/grid 的 start/end 方向html lang驱动拼写检查、CJK 字形选择和无障碍技术assistive technology。index.ts 单独注册了一个languageChanged监听器调用applyDocumentDirection并在模块加载时立即执行一次保证首次渲染方向正确。注释还给出了一条实用的开发约定代码块、终端输出、文件路径等天生从左到右的内容应在容器上用dirltr局部跳出 RTL而不是与文档方向对抗。八、测试与质量保障AionUi 对 i18n 的测试覆盖在多份测试文件中可见位于 tests/unit/common/ 等目录i18n.test.ts覆盖语言代码归一化、回退合并等核心工具函数i18nDirection.dom.test.tsDOM 环境下方向应用与切换行为的测试i18nFormat.test.ts验证数字、日期、货币等格式化函数在多种语言下的输出。结合 check-i18n.js 的 pre-commit 校验、generate-i18n-types.js 的类型生成以及测试套件形成了「生成类型 → 组件编译期校验 → 提交前键一致性检查 → 单元/DOM 测试」的完整质量闭环。九、开发注意事项与最佳实践综合 README 的「注意事项」一节与仓库实现整理如下所有用户可见文本都应使用翻译函数避免在代码中硬编码字符串——check-i18n.js会扫描渲染层源码检测硬编码文本翻译键应具有描述性便于维护优先conversation.welcome.title而非conversation.w1这类无意义缩写新增翻译时确保中英文都有对应翻译en-US 是 referenceLanguage 与 fallbackLanguage缺了它校验脚本会直接报错不要在组件里自行拼接列表分隔符中文的顿号、德文的 und 各不相同应使用formatNameList()不要用宿主系统语言格式化数字/日期统一走format.ts并把i18n.language显式传入不要直接修改 i18n-keys.d.ts该文件由脚本自动生成文件头有AUTO-GENERATED FILE - DO NOT EDIT声明修改语言包后重新运行生成脚本即可RTL 语言的内容容器要按需局部dirltr例如代码块与终端输出。十、总结AionUi 的 i18n 体系围绕i18next react-i18next构建但在工程化层面做了大量超越框架默认能力的强化自定义语言探测绕开i18next-browser-languagedetector在 WebUI/桌面双端 origin 不一致的坑、模块化语言包 自动生成的I18nKey类型、configService作为语言权威来源、桌面端与 WebUI 的语言实时同步、基于Intl.*的本地化格式化层、以及 fa-IR 的 RTL 布局支持。13 种语言包在 pre-commit 校验脚本与测试套件的守护下保持键级对齐。对于开发者而言日常接触最多的仍是三个动作在语言包 JSON 中加键、在组件中用t()取文案、跑node scripts/check-i18n.js校验。理解了本文的初始化链路与工程化细节后你不仅能熟练完成这些操作还能在遇到「语言切换不生效」「WebUI 与桌面端语言不一致」「某语言缺翻译」等问题时快速定位到对应的机制环节。相关资源i18n 说明文档i18n 初始化源码语言支持配置i18n 工具函数归一化/回退合并语言切换器组件本地化格式化工具RTL 方向处理翻译键自动生成脚本i18n 一致性校验脚本【免费下载链接】AionUi免费、本地、开源的 24/7 全天候 Cowork 应用以及适用于 Gemini CLI、Claude Code、Codex、OpenCode、Qwen Code、Goose CLI、Auggie 等的 OpenClaw | 喜欢就点star吧项目地址: https://gitcode.com/iOfficeAI/AionUi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →