AionUi 多语言架构指南:基于 i18next 的国际化实现与源码解析
【免费下载链接】AionUiOpen-source 24/7 Cowork app for OpenClaw, Hermes, Claude Code, Codex, OpenCode and 20+ more CLI Agent | Customize your assistants | Team them up|Star if you like it!项目地址: https://gitcode.com/GitHub_Trending/ai/AionUi
AionUi(桌面端)的多语言支持基于 i18next 与 react-i18next 构建,本文以 i18n README 为主体骨架,结合仓库内真实的初始化代码、语言包目录、类型生成脚本与单元测试,系统讲解 AionUi 的语言体系结构、翻译键组织规范、运行时语言切换机制,以及 RTL(从右到左)布局与本地化数字日期格式的处理方式。读完本文,你将掌握如何在 AionUi 中定位任意界面文案、添加新翻译、切换默认语言,并理解桌面端与 WebUI 之间语言实时同步的底层原理。
技术栈与支持的语言
AionUi 渲染进程(renderer)的多语言方案以两个 npm 包为核心:
- i18next:负责语言包加载、资源合并、语言切换与插值(interpolation);
- react-i18next:为 React 组件提供
useTranslationHook,将 i18next 实例与组件渲染周期绑定。
仓库 README 中提到“中文 (zh-CN) 为默认语言、英文 (en-US)”,但对照当前仓库实际配置 i18n-config.json 可知,该项目实际上已扩展为13 种支持语言,并且以en-US作为参考语言(referenceLanguage)与兜底语言(fallbackLanguage):
| 语言 | BCP 47 标签 | 方向 |
|---|---|---|
| 简体中文 | zh-CN | LTR |
| 英文 | en-US | LTR |
| 日文 | ja-JP | LTR |
| 繁体中文 | zh-TW | LTR |
| 韩文 | ko-KR | LTR |
| 土耳其文 | tr-TR | LTR |
| 俄文 | ru-RU | LTR |
| 乌克兰文 | uk-UA | LTR |
| 巴西葡萄牙文 | pt-BR | LTR |
| 德文 | de-DE | LTR |
| 西班牙文 | es-ES | LTR |
| 法文 | fr-FR | LTR |
| 波斯文 | fa-IR | RTL |
配置文件中还声明了全部翻译模块(modules)清单:common、agentMode、update、login、fileSelection、preview、conversation、settings、messages、mcp、acp、codex、tools、google、cron、guid、agent、team、pet,共 19 个业务模块。也就是说,翻译资源不是一个大 JSON,而是按模块拆分的多份 JSON。
文件结构与翻译包组织
原 README 描述的文件结构是src/renderer/i18n/,而当前仓库中该目录的实际位置与形态如下:
packages/desktop/src/renderer/services/i18n/ ├── index.ts # i18next 初始化与语言切换核心逻辑 ├── direction.ts # RTL/LTR 文档方向工具 ├── format.ts # 本地化数字、日期、货币、字节大小格式化 ├── list.ts # 本地化名称列表(Intl.ListFormat) ├── i18n-keys.d.ts # 自动生成的 I18nKey 联合类型(勿手改) ├── README.md # 说明文档 └── locales/ ├── en-US/ # 英文语言包(参考语言) │ ├── index.ts # 汇总导出本语言全部模块 │ ├── common.json │ ├── conversation.json │ └── ... # 共 19 个模块 JSON ├── zh-CN/ ├── zh-TW/ ├── ja-JP/ ├── ko-KR/ ├── de-DE/ ├── es-ES/ ├── fr-FR/ ├── tr-TR/ ├── ru-RU/ ├── uk-UA/ ├── pt-BR/ └── fa-IR/每个语言目录下都有一套同名同结构的 JSON 文件,例如 locales/zh-CN/index.ts 会把该语言目录下的 19 个 JSON 模块统一导入并导出为一个大对象。这种“按模块拆包”的设计有两个好处:
- 按需加载:配合 i18next 的
addResourceBundle,切换语言时只加载目标语言包; - 类型化兜底:缺失的键可以回退到
en-US参考包,而不是直接显示裸键名。
i18next 初始化原理:为什么不用语言检测器
i18n/index.ts 是渲染进程 i18n 的初始化入口,其初始化代码大致如下:
import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; i18n .use(initReactI18next) .init({ resources: initialResources, lng: initialLanguage, fallbackLng: DEFAULT_LANGUAGE, debug: false, interpolation: { escapeValue: false }, }) .catch((error: Error) => { console.error('Failed to initialize i18n:', error); });值得注意的源码细节是:项目刻意不使用i18next-browser-languagedetector。源码注释明确解释了原因(Issue #1176):
在 WebUI 模式下,浏览器 localStorage 的 origin 与 Electron 渲染进程不同,语言检测器会读到错误的(或缺失的)值并回退到
navigator.language,导致语言不匹配。
因此 AionUi 采用了自己的一套“语言提示”链路,getInitialLanguage()依次尝试:
- localStorage 提示:读取
localStorage.getItem('i18nextLng')(WebUI 与桌面端共用,作为快速提示); - 注入提示:读取
window.__initialLanguage(Electron 主进程注入的配置语言); - 系统语言提示:仅在后端启动失败的降级场景下,才回退使用
navigator.language; - 最终通过
normalizeLanguageCode归一到受支持的语言标签,否则使用DEFAULT_LANGUAGE。
初始语言在模块加载时同步注入resources,目的是避免首屏出现“先英文后切换”的 FOUC(闪白)问题。随后异步执行initLanguage():等待configService.whenReady()后,以configService.get('language')为**唯一权威来源(single source of truth)**进行ensureAndSwitch,并将结果写回 localStorage 供下次启动快速命中。
桌面端与 WebUI 的实时同步
index.ts中注册了两个languageChanged监听器:
- 一个负责懒加载新语言的资源包:
i18n.hasResourceBundle不存在时调用loadLocaleModules再addResourceBundle; - 另一个负责应用文档方向:调用
applyDocumentDirection(lang)更新<html>的dir与lang属性。
除此之外,changeLanguage()还会通过ipcBridge.systemSettings.changeLanguage.invoke()通知主进程(用于托盘菜单等主进程侧文案),并通过ipcBridge.systemSettings.languageChanged事件广播给其他渲染进程——这正是桌面端与 WebUI 一处切换语言、另一处无需重启即可实时跟随的实现基础:
ipcBridge.systemSettings.languageChanged.on(async ({ language }) => { const normalized = normalizeLanguageCode(language); if (i18n.language === normalized) return; // 自己触发的变更直接跳过 await ensureAndSwitch(i18n, normalized, loadLocaleModules); localStorage.setItem('i18nextLng', normalized); });在组件中使用翻译
原 README 给出的组件用法完全适用于当前仓库:通过react-i18next的useTranslationHook 获取t函数,用点号路径访问翻译键:
import { useTranslation } from 'react-i18next'; const MyComponent = () => { const { t } = useTranslation(); return ( <div> <h1>{t('common.title')}</h1> <p>{t('common.description')}</p> </div> ); };类型安全的翻译键
AionUi 更进一步:仓库中 i18n-keys.d.ts 是一个自动生成的联合类型文件,将en-US参考语言包中的所有翻译键展开为I18nKey类型(文件头注释明确标注 “AUTO-GENERATED FILE - DO NOT EDIT”)。其生成脚本是 scripts/generate-i18n-types.js,核心逻辑为:
- 读取 i18n-config.json 中的
modules清单; - 递归遍历
en-US/{module}.json提取全部叶子键,拼成模块名.键路径; - 输出形如
'conversation.welcome.title' | 'common.send' | ...的联合类型; - 若文件内容未变化则跳过写入,避免无谓的格式化。
借助I18nKey类型,t()的入参在编译期即可校验,翻译键写错或删除后会直接产生 TypeScript 报错,极大降低文案维护成本。
切换语言与持久化
原 README 中的“切换语言”示例同样成立,但在 AionUi 中切换语言不应直接调用裸的i18n.changeLanguage,而是推荐使用index.ts导出的changeLanguage()封装,它会同步完成三件事:
export async function changeLanguage(lang: string): Promise<void> { await ensureAndSwitch(i18n, lang, loadLocaleModules); const normalized = normalizeLanguageCode(lang); await configService.set('language', normalized); // 持久化到后端配置 if (typeof localStorage !== 'undefined') { localStorage.setItem('i18nextLng', normalized); // 写回 localStorage 提示 } ipcBridge.systemSettings.changeLanguage.invoke({ language: normalized }).catch(() => {}); }ensureAndSwitch(定义于 common/config/i18n.ts):先检查hasResourceBundle,缺失则懒加载并addResourceBundle,再去重changeLanguage调用;configService.set('language', normalized):把语言写入后端配置,保证重启后仍然生效;- localStorage 与 IPC 通知:分别服务 WebUI 快速提示和主进程/其他渲染进程同步。
顶层导航栏中的语言切换器(语言选择保存在 localStorage,下次访问自动应用上次选择)即基于此链路实现。同时 README 提醒:语言选择会保存在 localStorage 中,下次访问时会自动应用上次选择的语言,这与getLocalStorageLanguageHint()的读取逻辑完全对应。
添加新的翻译
步骤
- 在
packages/desktop/src/renderer/services/i18n/locales/zh-CN/对应模块 JSON(如common.json)中添加中文翻译; - 在
packages/desktop/src/renderer/services/i18n/locales/en-US/对应模块 JSON 中添加对应的英文翻译(en-US 是参考语言与兜底语言,必须齐全); - 在组件中使用
t('key')获取翻译。
翻译键的命名规范
原 README 规定的三条命名规范在当前仓库中被严格执行:
- 使用点号分隔的层级结构;
- 使用小写字母与下划线(部分键也使用 camelCase,如
copySuccess、fileAttach.uploadSuccess,但整体遵循小写下划线原则); - 按功能模块分组。
对照真实的 locales/zh-CN/common.json 可以看到实际形态:
{ "send": "发送", "cancel": "取消", "save": "保存", "delete": "删除", "confirm": "确定", "file": "文件", "folder": "文件夹", "workspace": "项目", "settings": "设置", "loading": "请稍候..." }更复杂的层级示例来自conversation模块,例如conversation.welcome.title、conversation.agentError.codes.USER_AGENT_DISCONNECTED.title这类“模块 → 场景 → 错误码 → 字段”的多级结构。
兜底合并机制
为了让缺少某条翻译的语言不会显示裸键名,common/config/i18n.ts 提供了mergeWithFallback(fallback, target)深合并函数:递归地把en-US参考包中缺失的键补进目标语言包,目标语言已有的键保持优先。getLocaleModules()在渲染进程侧把这一机制与懒加载缓存结合,保证任何语言包都“隐含”完整的 en-US 兜底。
语言代码规范化
normalizeLanguageCode()是语言体系中最核心的工具函数(位于 common/config/i18n.ts),它把任意“语言提示”归一到受支持的 13 个 BCP 47 标签之一,规则包括:
- 下划线转连字符:
zh_CN→zh-CN、de_DE→de-DE; - 基础语言码映射到区域:
zh→zh-CN、ja→ja-JP、ko→ko-KR、tr→tr-TR、ru→ru-RU、uk→uk-UA、pt→pt-BR、de→de-DE、es→es-ES、fr→fr-FR、fa→fa-IR; - 繁体中文保护:
zh-HK、zh-MO、zh-Hant及zh-Hant-*一律归到zh-TW,绝不降级为简体中文; - 区域变体归一:
de-AT、de-CH等归到de-DE; - 不支持的语言回退默认:
it、空字符串等回退到DEFAULT_LANGUAGE。
这些规则在单元测试 tests/unit/common/i18n.test.ts 中有完整覆盖,例如normalizeLanguageCode('zh_HK') === 'zh-TW'、normalizeLanguageCode('de-CH') === 'de-DE'、normalizeLanguageCode('it') === DEFAULT_LANGUAGE等断言,可以作为新增语言映射时修改与验证的参考。
RTL 布局与文档方向
波斯文(fa-IR)是 AionUi 唯一从右到左的语言。方向处理集中在 direction.ts:
isRtlLanguage():判断当前应用语言是否为 RTL(内部维护RTL_LANGUAGES = new Set(['fa-IR']));directionForLanguage():返回'rtl' | 'ltr';applyDocumentDirection():把dir与lang属性写入document.documentElement,dir驱动全应用 CSS 逻辑属性与 flex/grid 的 start/end,lang驱动拼写检查、CJK 字形选择与辅助技术(读屏)。
源码注释强调:布局方向永远由应用语言决定,而不是宿主操作系统,这是为了保证界面语言与布局方向不会互相矛盾。另外,对于代码块、终端输出、文件路径这类天生 LTR 的内容,应在容器上局部设置dir="ltr",而不是与文档级方向对抗。
本地化数字、日期与列表
仅翻译文案并不够——数字与日期格式同样需要随语言变化。原 README 未覆盖这部分,但仓库提供了完整的格式化服务(位于 format.ts):
formatNumber(value, language, options):按应用语言格式化数字(德语环境下12.6渲染为12,6);formatCurrency(amount, currency, language, options):货币格式化,无法渲染的货币代码回退为<number> <code>;formatDateTime / formatDate / formatTime:时间日期格式化,默认复刻toLocaleString()的数值日期加时间;formatByteSize / formatByteRate:二进制(1024)字节单位,如12.5 MB、1.2 MB/s;- 内部按
locale|options键缓存Intl.NumberFormat/Intl.DateTimeFormat实例,避免列表渲染中的重复构造开销。
其设计动机同样源于源码注释:Intl.NumberFormat(undefined, …)与裸toLocaleString()解析的是宿主操作系统的语言环境,与用户在 AionUi 内选择的应用语言无关——一台运行英文界面的德语系统桌面,曾经会渲染出0,42 $与17.8.2025。因此所有用户可见的数字与日期必须显式传入useTranslation().i18n.language进行格式化。
类似的还有 list.ts 中的formatNameList():用Intl.ListFormat按应用语言拼接名称列表,中文会用顿号与“和”,德语/法语会使用 “und”/“et”,波斯文会使用阿拉伯逗号,避免硬编码、或,造成的多语言错误。
注意事项与最佳实践
综合原 README 的“注意事项”与源码实现,AionUi 的 i18n 开发约束可归纳为:
- 所有用户可见文本都应使用翻译函数,不要在组件中硬编码中文或英文文案;
- 翻译键应具有描述性,按
模块.场景.字段层级组织,便于维护与检索; - 新增翻译时确保中英文都有对应条目——
en-US是参考语言与兜底语言,缺失键会从 en-US 回退,但显示内容可能不是目标语言; - 不要直接修改
i18n-keys.d.ts,它由scripts/generate-i18n-types.js自动生成,改完 JSON 后应重新运行生成脚本,仓库根目录另有 scripts/check-i18n.js 可用于检查语言包一致性; - 不要绕过
changeLanguage()直接调用 i18next 裸接口,否则会丢失配置持久化、localStorage 提示与 IPC 广播同步; - 数字与日期格式化请走
@/renderer/services/i18n/format,直接使用toLocaleString()系列会错误地依赖宿主系统语言; - RTL 语言(fa-IR)的文档方向由应用语言驱动,代码块等 LTR 内容局部用
dir="ltr"处理。
小结
AionUi 的国际化体系远比 README 单页描述得完整:13 种语言按 19 个业务模块拆包管理,以 en-US 为参考语言做深合并兜底;初始化阶段刻意绕开浏览器语言检测器,改为“localStorage 提示 + configService 权威配置 + IPC 广播同步”的三层链路,兼顾桌面端与 WebUI 的一致性;同时用I18nKey联合类型、语言代码规范化、RTL 方向工具与Intl格式化服务,把“文案、布局、数字、日期”四大国际化维度全部纳入类型安全与测试保障之中。开发者在 AionUi 中接入或扩展语言时,可以从 i18n 目录 与 i18n 配置 入手,遵循本文的键规范与注意事项即可平滑落地。
【免费下载链接】AionUiOpen-source 24/7 Cowork app for OpenClaw, Hermes, Claude Code, Codex, OpenCode and 20+ more CLI Agent | Customize your assistants | Team them up|Star if you like it!项目地址: https://gitcode.com/GitHub_Trending/ai/AionUi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考