LibreChat 多语言本地化:新增语言全流程与 i18n 底层机制深度解析
本篇以 LibreChat 仓库中的 本地化指南 为主体,完整讲解为 LibreChat 新增一种语言的操作流程(Locize 平台接入、语言选择器注册、翻译键添加、i18n 配置与回退设置),并结合 i18n 核心实现、语言状态持久化与测试用例,剖析语言代码归一化、懒加载资源包、并发切换竞态处理等底层机制。读完后可独立完成新语言接入,并理解语言偏好如何持久化、回退到英文的完整链路。
本地化系统总览
LibreChat 的前端国际化基于 i18next + react-i18next,所有语言资源位于 client/src/locales/ 目录下,每个语言对应一个子目录(如 en/、fr/、zh-Hans/),内含一个 translation.json 翻译文件。当前仓库已支持 42 种语言/变体,从 i18n.ts 导出的 supportedLocales 常量数组可以看到完整清单:en、zh-Hans、zh-Hant、pt-BR、pt-PT、bo(藏语)、uk(乌克兰语)、ug(维吾尔语)、he(希伯来语)等。
一个关键的架构细节:从源码看,只有英文是静态打包的——i18n.ts 顶部只有 import translationEn from './en/translation.json' 这一个静态导入,resources 对象(L55-L57)也只注册了 en。其余 41 种语言全部通过 localeLoaders 映射表(L59-L103)以动态 import() 方式懒加载,配合 i18next 的 partialBundledLanguages: true 与 load: 'currentOnly'(L288-L306),每种语言的翻译 JSON 被打成独立 chunk,用户切换到某语言时才下载,从而控制首屏体积。
新增语言:官方指南的完整步骤
以下内容继承自 client/src/locales/README.md,并针对当前仓库实现做了补充。
第 1 步:在 Locize 项目中添加语言
LibreChat 的翻译托管在 Locize 平台(项目地址见官方指南)。进入项目总览页,点击 "Start to translate" 卡片 "..." 菜单中的 "ADD LANGUAGE" 按钮即可添加新语言。
第 2 步:更新语言选择器组件
编辑 client/src/components/Appearance/Selectors.tsx,在 LangSelector 组件的 languageOptions 数组中加入新语言选项:
{ value: 'language-code', label: localize('com_nav_lang_language_name') },
指南给出的示例:
{ value: 'bo', label: localize('com_nav_lang_tibetan') },
{ value: 'uk-UA', label: localize('com_nav_lang_ukrainian') },
语言代码格式要点(来自指南,且与源码行为一致):
- 无区域变体的语言使用简单代码,如
bo; - 需要区分区域变体时使用带区域代码的格式,如
uk-UA、pt-BR、zh-Hans。
当前 Selectors.tsx 中的 languageOptions 实际以「auto」项开头({ value: 'auto', label: localize('com_nav_lang_auto') },对应英文 "Auto detect"),其后是带区域标签的变体值(如 en-US、de-DE)。选择器使用带搜索的 Dropdown 组件,并在语言包下载期间显示加载 Spinner(由 Recoil 的 languageLoading 状态驱动)。
第 3 步:添加本地化键(语言自身名称)
在 client/src/locales/en/translation.json 中添加该语言名称的键值,最佳实践是使用目标语言的本地文字(native script)作为值。以指南示例为例,当前英文翻译文件中已有:
"com_nav_lang_tibetan": "བོད་སྐད་",
"com_nav_lang_ukrainian": "Українська",
(见 translation.json 对应行)
第 4 步:创建翻译文件目录
mkdir -p client/src/locales/[language-code]
然后创建 client/src/locales/[language-code]/translation.json,初始内容为空 JSON 对象:
{}
即新增 client/src/locales/bo/translation.json、client/src/locales/uk/translation.json 等。
第 5 步:注册到 i18n
指南要求更新 client/src/locales/i18n.ts:导入新的翻译文件并加入 resources 对象:
import translationLanguageCode from './language-code/translation.json';
export const resources = {
// ... existing languages
'language-code': { translation: translationLanguageCode },
} as const;
结合当前源码的补充说明:仓库现有实现已演进为懒加载模式,因此新语言的实际注册点有三处,均位于 i18n.ts:
localeLoaders = {
// ...
'language-code': () => import('./language-code/translation.json'),
};
- 若语言代码需要归一化映射(例如选择器里用
uk-UA而资源目录是uk),确保localeAliases表(L113-L151)中存在对应条目,如'uk-ua': 'uk'、'zh-cn': 'zh-Hans'。该表由normalizeLocale统一消费,因此无论用户在 UI 中选择uk-UA还是浏览器报告uk,最终都解析到同一资源。
第 6 步:配置回退语言(可选)
如果某语言缺失翻译时应回退到特定语言,更新 i18n.ts 中 i18next 初始化的 fallbackLng 配置。指南的写法:
fallbackLng: {
'language-variant': ['fallback-language', 'en'],
// ... existing fallbacks
},
当前实现(L288-L306)中的配置为:
fallbackLng: {
'zh-TW': ['zh-Hant', 'en'],
'zh-HK': ['zh-Hant', 'en'],
zh: ['zh-Hans', 'en'],
default: ['en'],
},
即所有未特别配置的语言最终都回退到英文;中文繁体变体(zh-TW/zh-HK)优先回退到 zh-Hant 再回退英文。
底层机制:语言代码归一化与资源加载
normalizeLocale:一切语言标识的入口
changeLanguageSafely、detectInitialLanguage 都依赖 normalizeLocale(L197-L216),其处理顺序为:
- 若传入
'auto',取navigator.language(浏览器语言); - 将下划线替换为连字符并整体小写(
zh_CN→zh-cn); - 先查小写化的
supportedLocales精确表(localeByLowercase); - 再查
localeAliases别名表; - 最后截取基础语言代码再查一次,全部失败则回退
'en'。
Translation.spec.ts 对这套归一化做了断言:normalizeLocale('uk-UA') 应为 'uk'、normalizeLocale('pt-BR') 应保持 'pt-BR'、normalizeLocale('en-US') 应为 'en'——这正是指南第 2 步「区域代码在 UI 中书写、基础代码在资源中注册」设计的验证。
ensureLocale:懒加载、去重与失败回退
ensureLocale 负责按需加载语言资源,包含三个从测试用例可确认的行为:
- 在途请求去重:同一语言的并发加载共享一个 Promise,loader 只调用一次(测试
should reuse an in-flight locale load验证了这一点,Translation.spec.ts); - 加载失败回退英文:chunk 加载失败时打印
[i18n] Failed to load locale "..."并返回'en',且下一次调用可以重试(测试should retry a locale load after a transient failure,L106-L128); - 已注册检测:若 i18next 中已存在该语言的 resource bundle,直接复用而不再触发网络请求。
加载完成后通过 i18n.addResourceBundle(locale, 'translation', module.default, true, true) 将翻译合并进 i18next 实例。
changeLanguageSafely:并发切换竞态处理
changeLanguageSafely(L308-L329)用自增的 languageRequestId 保证只有最后一次语言切换生效:若某次切换在等待 chunk 下载期间被更新的切换超越,它会直接返回当前 i18n 语言;若旧请求晚于新请求完成,代码会主动重新加载并切换到 latestRequestedLocale。对应测试 should only apply the newest rapid language switch 与 should restore the newest language if an older change finishes late(Translation.spec.ts)用三个延迟解析的 Promise 精确模拟了「快速连续切换」场景。
syncDocumentLanguage:RTL 与无障碍同步
每次切换成功后,syncDocumentLanguage(L279-L286)会同步 document.documentElement.lang 与 dir 属性——后者由 i18n.dir(locale) 计算,因此希伯来语(he)、阿拉伯语(ar)等会自动获得 dir="rtl" 的整页右到左排版。
语言偏好的持久化与 UI 同步链路
用户选择语言后的数据流如下,各环节均有源码可查:
- Recoil 状态 + localStorage:client/src/store/language.ts 中
lang是一个atomWithLocalStorage('lang', ...),默认值优先读langCookie,其次 localStorage,最后取navigator.language; - 副作用同步:client/src/components/System/LanguageSync.tsx 监听
store.lang变化,若与当前i18n.language不一致则置languageLoading为 true 并调用changeLanguageSafely(lang),完成后关闭 Loading; - i18next 应用翻译 + 文档属性同步,即上一节的
changeLanguageSafely链路。
启动时则由 initializeI18n(L331-L335)执行 detectInitialLanguage()——按「Cookie → localStorage → 浏览器语言」的顺序探测并归一化后完成首次切换。
翻译键的类型安全
client/src/hooks/useLocalize.ts 中定义了:
export type TranslationKeys = keyof typeof translationEn;
export default function useLocalize() {
const { t } = useTranslation();
return useCallback(
(phraseKey: TranslationKeys, options?: TOptions) => t(phraseKey, options),
[t],
);
}
由于 TranslationKeys 直接派生自英文翻译文件的键,任何在英文 translation.json 中不存在的新键都会在 TypeScript 编译期报错。这正是指南第 3 步「先改英文文件」的强制性原因:英文是唯一事实来源(single source of truth),其他语言的 translation.json 只需保持键集合与其一致。
翻译管理流程:只手工维护英文
指南「Translation Process」一节的规则与仓库脚本相互印证:
- 新建语言的翻译文件以空对象
{}起步,后续由 LibreChat 的自动化翻译平台(Locize)填充; - 只有
en翻译文件应当手工更新; - 其他语言的翻译在外部平台管理,下载后通过仓库根目录的 scripts/merge-locize-download.mjs 与现有翻译做深度合并——该脚本会递归比较基线(base)与当前(current)JSON 树,恢复平台导出中缺失的键并统计恢复数量,防止新键丢失。
语言代码标准(指南原文要点)
- 大多数语言使用 ISO 639-1 代码(
en、fr、de); - 需要区分变体时组合区域/写法代码(
pt-BR、zh-Hans、zh-Hant); - 藏语使用
bo(Bodic),乌克兰语使用uk或带区域的uk-UA。
注意 UI 选择器值与资源目录代码可以不同(如选择器写 uk-UA、资源目录是 uk),只要 localeAliases 中登记了 'uk-ua': 'uk' 这类映射即可——归一化逻辑会把两者统一。
验证新语言是否生效
指南给出的验证步骤,结合源码与测试补充如下:
- 重启开发服务器;
- 进入 Settings > General,确认新语言出现在语言下拉框(该下拉框即
LangSelector组件渲染,带搜索功能); - 选择该语言,确认 UI 语言随之切换——若翻译文件为空,界面会按
fallbackLng回退到英文文案(测试should fallback to English for an invalid language code验证了无效代码的回退行为,Translation.spec.ts); - 可运行 client/src/locales/Translation.spec.ts 中的测试套件,覆盖:有效键在英文/法文/西班牙文下的取值、无效键原样返回(
i18n.t('invalid-key')返回'invalid-key')、占位符插值(如com_endpoint_default_with_num传入{ 0: 'John' }得到default: John)、以及上文所述的归一化/去重/竞态场景。
最佳实践(指南 Notes 与补充)
- 语言下拉选项保持按字母顺序排序,便于用户查找;
- 下拉框中的语言名一律使用本地文字(如
བོད་སྐད་、Українська),保证母语用户可识别; - 任何缺失的翻译都会以英文兜底,因此新语言即使翻译不完整也可安全上线;
- 从源码结构看,若新增语言的 UI 选择器值带区域后缀(如
xx-XX),记得在localeAliases中补充小写化的别名条目,否则normalizeLocale只能靠「基础语言」兜底解析,无法精确命中带变体的资源。
以上流程完整覆盖了 本地化指南 的全部六个接入步骤,并给出了每个步骤在 i18n.ts、Selectors.tsx、store/language.ts 与测试文件中的对应实现,可直接作为新语言接入的操作手册。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00