news 2026/9/7 19:12:09

Dify 前端国际化(i18n)系统实战:i18n-config 模块的职责划分、语言包扩展与 i18n:check 校验

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Dify 前端国际化(i18n)系统实战:i18n-config 模块的职责划分、语言包扩展与 i18n:check 校验

Dify 前端国际化(i18n)系统实战:i18n-config 模块的职责划分、语言包扩展与 i18n:check 校验

【免费下载链接】difyBuild Agentic workflows, RAG pipelines, with rich AI model and tool support on one collaborative workspace. Deploy on cloud, VPC, or self-hosted, so teams move from prototype to production without rebuilding the stack.项目地址: https://gitcode.com/GitHub_Trending/di/dify

本文以 Dify 仓库中 web/i18n-config/README.md 文档为主体,系统讲解 Dify Web 端国际化体系的落地方式:en-US源语言包、扁平化 key 设计、各文件的职责边界、新增 locale 与命名空间的完整流程,以及pnpm i18n:check校验工具与自动化翻译工作流的机制。读完后,你将能够在不破坏多语言一致性校验的前提下,独立完成语言包扩展、文案修改与 CI 级校验。

一、核心设计决策:源语言、扁平 key 与 i18next 配置

web/i18n-config/README.md 开篇就确立了整套国际化体系的三条基石规则,理解它们是后续一切操作的前提:

  1. web/i18n/en-US/下的英文 JSON 文件是唯一源语言(source locale)。其他所有 locale 目录必须保持相同的扁平 key 与占位符(interpolation variables 和 markup placeholders)。
  2. i18next 使用keySeparator: false,即 key 中的点号.是 key 本身的组成部分,而不是嵌套对象的层级分隔符。这一决定可以从 web/i18n-config/settings.ts 中得到印证:
export function getInitOptions(): InitOptions { return { // We do not have en for fallback load: 'currentOnly', fallbackLng: 'en-US', partialBundledLanguages: true, defaultNS, enableSelector: 'optimize', keySeparator: false, // 点号属于 key 本身,不做嵌套解析 ns: namespaces, interpolation: { escapeValue: false, }, } }
  1. 各 locale 目录与en-US的 key 集合必须严格对齐,由 web/scripts/check-i18n.js 在提交前强制校验(该脚本以targetLanguage = 'en-US'为基准对比其他语言包)。

从源码结构看,这种“源语言 + 扁平 key + 提交前校验”的组合,使得 24 个受支持 locale(见下文languages.ts)之间的 key 漂移能够被脚本精确捕获,而不依赖开发者人工记忆。

二、文件职责划分(Owners):每个文件只负责一件事

README 中最值得反复引用的部分是 “Owners” 一节——它把i18n-config目录拆分为职责单一的若干文件,避免任何人把语言注册表复制到文档或代码的其他位置(README 明确要求 “Do not copy the language registry into documentation. Read the current source files when adding a locale or namespace.”)。

文件职责源码佐证
web/i18n-config/languages.ts受支持 Web locale 的唯一事实来源(source of truth)导出languages数组,每项含value/name/prompt_name/example/supported字段
web/i18n-config/language.tslocale 归一化与产品特定的 locale 映射;拥有I18nText契约定义LanguagesSupportedgetLanguagelocaleMapgetDocLanguage
web/i18n-config/resources.ts带类型的命名空间(namespace)注册表;文件名用 kebab-case,命名空间用 camel-caseapp-debug.jsonappDebug,通过kebabCase(ns)转换
web/i18n-config/locale-resources/<locale>.ts单一 locale 的**懒加载(lazy loading)**入口如 web/i18n-config/locale-resources/zh-Hans.ts 仅两行动态 import
web/i18n-config/settings.ts共享的 i18next 初始化选项getInitOptions()返回InitOptions
web/scripts/check-i18n.jslocale key 校验与多余 key 的移除pnpm i18n:check的执行体

当前web/i18n-config/locale-resources/目录下共存在 24 个<locale>.ts文件(ar-TN、de-DE、en-US、es-ES、fa-IR、fr-FR、hi-IN、id-ID、it-IT、ja-JP、ko-KR、lo-LA、nl-NL、pl-PL、pt-BR、ro-RO、ru-RU、sl-SI、th-TH、tr-TR、uk-UA、vi-VN、zh-Hans、zh-Hant),与 web/i18n/ 下的 24 个语言目录一一对应。

2.1 语言注册表:languages.ts 的结构

languages.ts 是一个as const的静态数据文件,每个语言条目长这样:

{ value: 'zh-Hans', name: '简体中文', prompt_name: 'Chinese Simplified', example: '你好,Dify!', supported: true, },
  • value:程序使用的 locale 标识(en-USzh-Hansja-JP……);
  • name:面向用户的本地化显示名;
  • prompt_name:英文短名(用于提示语等场景);
  • example:该语言的“Hello, Dify!”问候示例,常用于模型提示词或 UI 预览;
  • supported:是否启用。language.ts 中的LanguagesSupported正是通过languages.filter((item) => item.supported).map((item) => item.value)派生而来——这意味着新增或停用语言只需改这里,下游类型与运行时自动收敛

I18nText类型也由该列表直接生成,要求后端下发的多语言对象必须覆盖所有受支持 locale:

export type I18nText = Record<(typeof LanguagesSupported)[number], string>

2.2 locale 归一化:language.ts 的映射层

language.ts 除了持有LanguagesSupported,还承担三类映射职责:

  • getLanguage:将zh-Hans/ja-JP转换为下划线形式(zh_Hans/ja_JP),其余 locale 一律回退到LanguagesSupported[0](即en_US)。这里的下划线形式是与后端历史字段(如en_USzh_Hansja_JP)兼容的产物,因此Locale类型显式并入了这三个 legacy 值。
  • localeMap:locale → 短码(enzh-cnzh-twja……)的双写法映射,en-USen_US都指向en,供依赖短码的第三方库使用。
  • 产品特定映射getDocLanguage(文档站只服务zh/ja/en三种语言,默认en)与getAccessControlTemplateLanguage(访问控制模板语言的zh/ja/en映射),体现了“一个 locale 体系、多个消费方按各自能力取子集”的设计。

该文件末尾的NOTICE_I18N常量则是I18nText契约的一个真实用例:全站公告的titledescen_US/zh_Hans等下划线 key 为全部 24 个 locale 提供文案。

三、命名空间注册表:resources.ts 的类型安全体系

resources.ts 是 README 中 “namespaces use camel case, file names use kebab case” 规则的落点,其工作方式分四层:

  1. 37 个类型化命名空间:从web/i18n/en-US/*.json逐一import typeappappDebugdatasetworkflowbilling……共 37 个,如app-debug.jsonappDebugagent-v-2.jsonagentV2),聚合为RawResources类型。由于只是import type,这些 import 不会进入运行时 bundle。
  2. PluralBaseResources复数键桥接:i18next 的复数选择器(count)会让 TypeScript 难以静态推断可用 key,因此该类型以类型级声明把复数基础键(如common.members.seatsRemainingworkflow.nodes.iteration.error)显式暴露出来。源码中的注释解释了动机:“This type-only bridge exposes runtime plural base keys; selector types cannot require callers to pass count.”
  3. defaultNS = 'app':未指定命名空间时默认落在app
  4. 文件名 ↔ 命名空间的双向推导namespacesInFileName = namespaces.map((ns) => kebabCase(ns)),由 web/i18n-config/load-resource.ts 在运行时用kebabCase(namespace)完成 camelCase → kebab-case 的动态文件名换算。

这套设计让 TS 编译器能够校验“某命名空间下是否存在某个 key”,命名空间注册表同时是 resources.ts#L146-L184 中namespaces数组(传给 i18nextns选项)的类型来源,二者通过satisfies ReadonlyArray<keyof Resources>强绑定,新增命名空间时若忘记登记会直接报错。

四、运行时链路:i18next 实例、懒加载与 Cookie 切换

README 没有逐行描述运行时,但源码链路完整且清晰,可按“资源加载 → 实例创建 → 语言切换”三步理解。

4.1 locale 懒加载

每个locale-resources/<locale>.ts只做一件事——动态 import 该 locale 的 JSON,例如 zh-Hans.ts:

export const loadResource = (fileNamespace: string) => import(`../../i18n/zh-Hans/${fileNamespace}.json`)

load-resource.ts 中的loadLocaleResources再按 locale 动态选择模块:

const loadLocaleResources = (locale: Locale): Promise<LocaleResourceModule> => { const normalized = normalizeLocale(locale) return import(`./locale-resources/${normalized}.ts`) }

normalizeLocale在这里完成第二道防线:把 legacy 写法en_US/ja_JP/zh_Hans映射回连字符形式,不在LanguagesSupported中的 locale 一律回退到en-USdefaultLocale = 'en-US' satisfies Locale)。结合settings.tsload: 'currentOnly'fallbackLng: 'en-US'的配置(注释说明 “We do not have en for fallback”),运行时只会加载当前 locale 的当前命名空间,这正是两级动态 import(locale 模块 → JSON 文件)实现按需分包的基础。

4.2 客户端实例创建

client.ts 展示了标准的 i18next + react-i18next 组装方式:

export function createI18nextInstance(lng: Locale, resources: Resource) { const instance = createInstance() instance .use(initReactI18next) .use( resourcesToBackend((language, namespace) => loadI18nResource(language, namespace), ), ) .init({ ...getInitOptions(), lng, resources }) return instance }

i18next-resources-to-backend把自定义的loadI18nResource挂为后端,i18next 缺什么命名空间就向它要,而它最终落到 4.1 的动态 import。changeLanguage(lng)则通过react-i18nextgetI18n()委托给全局实例,保证语言切换只触发对应资源的懒加载而不重打全部包。

4.3 SSR/CSR 双入口与语言切换交互

web/package.json 的 exports 字段暴露了#i18n包内别名,按运行环境分发到不同实现:

"#i18n": { "react-server": "./i18n-config/lib.server.ts", "default": "./i18n-config/lib.client.ts" }

即 React Server Components 环境使用 lib.server.ts,浏览器端使用 lib.client.ts;另有 server.ts / client.ts 分别承担 SSR 与 CSR 侧的具体装配。

面向交互的入口在 web/i18n-config/index.ts:

export const i18n = { defaultLocale: 'en-US', locales: LanguagesSupported, } as const export const setLocaleOnClient = async (locale: Locale, reloadPage = true) => { Cookies.set(LOCALE_COOKIE_NAME, locale, { expires: 365 }) await changeLanguage(locale) if (reloadPage) location.reload() }

切换语言的完整闭环是:写 Cookie(有效期 365 天,键名为LOCALE_COOKIE_NAME)→changeLanguage异步加载新 locale 资源 → 默认整页 reload 保证服务端渲染与客户端状态一致。同文件的renderI18nObject则解决反向问题——渲染后端直接下发的多语言对象:优先取obj[language],其次回退obj.en_US,最后取第一个非空值,这恰好与I18nText的下划线 key 约定呼应。

五、新增一个 locale:README 五步流程

README 的 “Add a locale” 一节给出了五步操作,结合源码可以逐步展开:

  1. 在 languages.ts 中添加语言元数据value/name/prompt_name/example/supported: true)。LanguagesSupportedI18nText会随之自动扩展,未同步填充新 locale 的I18nText用例会编译报错,相当于类型级提醒。
  2. 创建web/i18n/<locale>/目录,放入全部源命名空间对应的 JSON 文件(当前为 web/i18n/en-US/ 下的 37 个文件,如app-debug.jsondataset-pipeline.jsonworkflow.json……),key 必须与en-US完全对齐。
  3. 新建locale-resources/<locale>.ts(照抄 zh-Hans.ts 的两行动态 import 即可),并在 language.ts 中补上所需映射——至少是localeMap中的短码条目;如该 locale 需服务文档站或访问控制模板,还要登记DOC_LANGUAGE/ACCESS_CONTROL_TEMPLATE_LANGUAGE
  4. 同步后端语言注册表:当该 locale 会被后端 API 接受时,需保持 api/constants/languages.py 与前端languages.ts对齐。
  5. 提交前运行完整的 i18n 校验pnpm i18n:check(下一节详述)。

README 特别提醒:language.ts同时拥有“被接受的 locale 拼写”与I18nText契约,新增 locale 时二者必须同步维护,否则会出现“运行时认得但类型不认”或反向的错位。

六、新增或修改文案与 i18n:check 校验

6.1 文案修改规则

README 的 “Add or change copy” 一节规则很直白:

  • 先改英文 key,再更新所有受支持 locale
  • 精确保留插值变量与 markup 占位符{{name}}{{count}}以及内嵌标签),不得在翻译中增删或改名——因为占位符是前后端渲染契约的一部分。

6.2 i18n:check 命令详解

web/目录下运行(脚本定义见 web/package.json:"i18n:check": "tsx ./scripts/check-i18n.js"):

pnpm i18n:check pnpm i18n:check --file app billing --lang zh-Hans ja-JP
  • 不带参数:全量校验所有受支持 locale 与en-US的 key 一致性;
  • --file <...> --lang <...>:把校验范围收窄到指定命名空间与语言。两个 flag 的取值都是空格分隔的(脚本源码中显式拒绝逗号:--file expects space-separated values. Example: --file app billing),且都至少需要一个值;
  • --auto-remove仅当确实要删除多余 locale key 时使用,否则校验只会报告而不改动文件。

从 check-i18n.js 的源码结构看,它直接import data from '../i18n-config/languages'并以supported: true的条目为语言全集——又一次印证了 “languages.ts是唯一事实来源”:校验器、运行时、SSR 入口消费的是同一份注册表,不存在第二份语言清单。

同目录还有两个配套脚本(web/package.json中定义):i18n:migrate-selectors(选择器迁移)与i18n:prune-unused(清理未使用 key),属于维护性工具,日常文案工作以i18n:check为主。

七、自动化翻译工作流

README 最后描述了仓库内置的自动翻译机制,两条触发路径:

  1. 自动触发:当main分支上web/i18n/en-US/*.json发生变更时,触发 scoped translation workflow。工作流languages.ts推导目标 locale(而不是硬编码语言列表),只翻译发生变化的命名空间与 key,随后用i18n:check验证翻译结果,仅在确实产生翻译变更时开一个 pull request。
  2. 手动触发:使用Translate i18n Files with Claude Codeworkflow dispatch 执行手动 scoped sync;Full 模式要求显式提供文件列表。

这套机制与前述设计自洽:由于 key 以en-US为源且校验器以en-US为基准,自动翻译只需对 diff 的 key 做最小化同步,i18n:check则作为 PR 前的最终一致性闸门。

八、关键路径速查

内容路径
本文主体文档web/i18n-config/README.md
语言注册表(唯一事实来源)web/i18n-config/languages.ts
locale 归一化与I18nText契约web/i18n-config/language.ts
命名空间类型注册表(37 个)web/i18n-config/resources.ts
i18next 共享初始化选项web/i18n-config/settings.ts
locale 懒加载模块(24 个)web/i18n-config/locale-resources/
locale 归一化 + 资源动态加载web/i18n-config/load-resource.ts
客户端实例创建 / 语言切换web/i18n-config/client.ts
交互入口(Cookie、renderI18nObjectweb/i18n-config/index.ts
英文源语言包web/i18n/en-US/
key 校验脚本web/scripts/check-i18n.js
后端语言注册表(需对齐)api/constants/languages.py

适用前提与限制:以上机制均针对 Dify Web 前端(Next.js + i18next + react-i18next 技术栈);i18n:check需在web/目录下通过 pnpm 运行;--auto-remove会真实删除 locale 文件中的多余 key,误用会造成文案丢失。扩展 locale 或命名空间时,务必以当前源码文件为准(README 明确告诫不要将语言注册表抄入文档),并保证前后端两份语言注册表同步。

【免费下载链接】difyBuild Agentic workflows, RAG pipelines, with rich AI model and tool support on one collaborative workspace. Deploy on cloud, VPC, or self-hosted, so teams move from prototype to production without rebuilding the stack.项目地址: https://gitcode.com/GitHub_Trending/di/dify

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 19:12:09

LeetCode 167 两数之和 II 有序数组双指针解法详解

1. 题目拆解与核心难点 1.1 题目到底在问什么——条件即线索 LeetCode 167这道题&#xff0c;全称是“两数之和 II - 输入有序数组”&#xff0c;说白了就是经典“两数之和”的进阶版。基础版题目给的是一个无序数组&#xff0c;你需要找到两个数&#xff0c;使它们的和等于目…

作者头像 李华
网站建设 2026/9/7 19:06:39

Git分支管理实战:从工作流选型到冲突解决全指南

1. 分支管理&#xff0c;先从“它到底在管什么”说起如果你去问刚接触 Git 的人&#xff0c;分支管理到底是什么&#xff0c;十有八九会得到一句“就是创建分支、合并分支呗”。这句话没错&#xff0c;但它把一个本来应当成为团队协作底座的事情&#xff0c;说窄了。我做了这么…

作者头像 李华
网站建设 2026/9/7 19:06:06

IsaacLab启动Segmentation Fault排查:xcb库冲突与headless失效的根治方案

如果你也遇到 IsaacLab 安装完成后&#xff0c;打开终端跑第一个训练脚本&#xff0c;满心期待看到环境初始化动画&#xff0c;结果屏幕上只有一行冷冰冰的Segmentation fault (core dumped)&#xff0c;并且补上--headless再试依然原地崩溃&#xff0c;那么这篇文章大概率能帮…

作者头像 李华