AionUi 多语言架构指南:基于 i18next 的国际化实现与源码解析
2026/9/11 23:31:31 网站建设 项目流程

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-CNLTR
英文en-USLTR
日文ja-JPLTR
繁体中文zh-TWLTR
韩文ko-KRLTR
土耳其文tr-TRLTR
俄文ru-RULTR
乌克兰文uk-UALTR
巴西葡萄牙文pt-BRLTR
德文de-DELTR
西班牙文es-ESLTR
法文fr-FRLTR
波斯文fa-IRRTL

配置文件中还声明了全部翻译模块(modules)清单:commonagentModeupdateloginfileSelectionpreviewconversationsettingsmessagesmcpacpcodextoolsgooglecronguidagentteampet,共 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 模块统一导入并导出为一个大对象。这种“按模块拆包”的设计有两个好处:

  1. 按需加载:配合 i18next 的addResourceBundle,切换语言时只加载目标语言包;
  2. 类型化兜底:缺失的键可以回退到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()依次尝试:

  1. localStorage 提示:读取localStorage.getItem('i18nextLng')(WebUI 与桌面端共用,作为快速提示);
  2. 注入提示:读取window.__initialLanguage(Electron 主进程注入的配置语言);
  3. 系统语言提示:仅在后端启动失败的降级场景下,才回退使用navigator.language
  4. 最终通过normalizeLanguageCode归一到受支持的语言标签,否则使用DEFAULT_LANGUAGE

初始语言在模块加载时同步注入resources,目的是避免首屏出现“先英文后切换”的 FOUC(闪白)问题。随后异步执行initLanguage():等待configService.whenReady()后,以configService.get('language')为**唯一权威来源(single source of truth)**进行ensureAndSwitch,并将结果写回 localStorage 供下次启动快速命中。

桌面端与 WebUI 的实时同步

index.ts中注册了两个languageChanged监听器:

  • 一个负责懒加载新语言的资源包:i18n.hasResourceBundle不存在时调用loadLocaleModulesaddResourceBundle
  • 另一个负责应用文档方向:调用applyDocumentDirection(lang)更新<html>dirlang属性。

除此之外,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-i18nextuseTranslationHook 获取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,核心逻辑为:

  1. 读取 i18n-config.json 中的modules清单;
  2. 递归遍历en-US/{module}.json提取全部叶子键,拼成模块名.键路径
  3. 输出形如'conversation.welcome.title' | 'common.send' | ...的联合类型;
  4. 若文件内容未变化则跳过写入,避免无谓的格式化。

借助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()的读取逻辑完全对应。

添加新的翻译

步骤

  1. packages/desktop/src/renderer/services/i18n/locales/zh-CN/对应模块 JSON(如common.json)中添加中文翻译;
  2. packages/desktop/src/renderer/services/i18n/locales/en-US/对应模块 JSON 中添加对应的英文翻译(en-US 是参考语言与兜底语言,必须齐全);
  3. 在组件中使用t('key')获取翻译。

翻译键的命名规范

原 README 规定的三条命名规范在当前仓库中被严格执行:

  • 使用点号分隔的层级结构
  • 使用小写字母与下划线(部分键也使用 camelCase,如copySuccessfileAttach.uploadSuccess,但整体遵循小写下划线原则);
  • 功能模块分组

对照真实的 locales/zh-CN/common.json 可以看到实际形态:

{ "send": "发送", "cancel": "取消", "save": "保存", "delete": "删除", "confirm": "确定", "file": "文件", "folder": "文件夹", "workspace": "项目", "settings": "设置", "loading": "请稍候..." }

更复杂的层级示例来自conversation模块,例如conversation.welcome.titleconversation.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_CNzh-CNde_DEde-DE
  • 基础语言码映射到区域zhzh-CNjaja-JPkoko-KRtrtr-TRruru-RUukuk-UAptpt-BRdede-DEeses-ESfrfr-FRfafa-IR
  • 繁体中文保护zh-HKzh-MOzh-Hantzh-Hant-*一律归到zh-TW绝不降级为简体中文
  • 区域变体归一de-ATde-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():把dirlang属性写入document.documentElementdir驱动全应用 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 MB1.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 开发约束可归纳为:

  1. 所有用户可见文本都应使用翻译函数,不要在组件中硬编码中文或英文文案;
  2. 翻译键应具有描述性,按模块.场景.字段层级组织,便于维护与检索;
  3. 新增翻译时确保中英文都有对应条目——en-US是参考语言与兜底语言,缺失键会从 en-US 回退,但显示内容可能不是目标语言;
  4. 不要直接修改i18n-keys.d.ts,它由scripts/generate-i18n-types.js自动生成,改完 JSON 后应重新运行生成脚本,仓库根目录另有 scripts/check-i18n.js 可用于检查语言包一致性;
  5. 不要绕过changeLanguage()直接调用 i18next 裸接口,否则会丢失配置持久化、localStorage 提示与 IPC 广播同步;
  6. 数字与日期格式化请走@/renderer/services/i18n/format,直接使用toLocaleString()系列会错误地依赖宿主系统语言;
  7. 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询