Folo(Follow)i18n 协作实战:新增语言、维护多语言资源与通过 ESLint 校验的完整指南
【免费下载链接】follow🧡 Folo is the AI RSS Reader项目地址: https://gitcode.com/GitHub_Trending/fol/follow
Folo 是一个"在同一个地方订阅一切"的 AI RSS 阅读器(仓库根目录 package.json 中描述为Follow everything in one place),其桌面端、移动端、Web 端共享同一套国际化资源。wiki/contribute-i18n.md 是一份面向贡献者的国际化协作指南,本文以该文档为主线,结合仓库中locales/的真实资源结构、类型定义、ESLint 校验规则与提交钩子,深入讲解:如何新增一种语言、如何维护已有翻译、必须遵守的翻译规范,以及如何利用仓库内置的自动化工具确保提交的 JSON 与英文基准完全对齐。
读完本文,你将掌握在 Folo 中新增/更新翻译的完整可执行流程,并理解locales/目录背后"命名空间 × 语言"的二维组织方式、占位符与复数规则、以及 CI/commit 阶段自动校验的实现原理,从而贡献出能直接合并的翻译提交。
1. 先读懂locales/的多语言资源结构
要高效地贡献翻译,第一步是理解资源目录的组织方式。仓库根目录的 locales/ 按命名空间目录 → 语言文件两级组织:
locales/ ├── ai/ ai 功能相关文案 ├── app/ 桌面端主应用界面文案(最大的命名空间) ├── common/ 各端通用文案 ├── errors/ 错误提示文案 ├── external/ 外部服务/第三方相关文案 ├── lang/ 各语言的语言自名(如 English / 简体中文) ├── mobile/ │ └── default/ 移动端默认文案命名空间 ├── native/ 桌面端主进程/原生层文案 ├── settings/ 设置页文案 └── shortcuts/ 快捷键文案其中除mobile/default为二级目录外,其余每个命名空间目录下均按语言代码存放 JSON,当前仓库已包含 5 个语言文件,例如 locales/app 下有:
en.json—— 英文基准(唯一的事实来源,所有校验都以它为参照)zh-CN.json—— 简体中文zh-TW.json—— 繁体中文ja.json—— 日语fr-FR.json—— 法语
语言代码遵循 BCP-47 风格(zh-CN、zh-TW、fr-FR),带连字符;新增语言时文件名必须使用与现有文件一致的大小写与连字符格式。
1.1 类型定义中"注册"语言
语言不只是 JSON 文件,还会被 TypeScript 类型定义引用,例如桌面端渲染层的 default-resource.electron.ts 逐个import各命名空间的各语言 JSON,resources.ts 则负责主进程的native命名空间。这也是文档中"脚本会更新必要配置文件"所指的配置之一。
1.2 别名@locales的解析
各端代码统一通过@locales/xxx/en.json形式引用资源,例如 electron.vite.config.ts 与 vite.render.config.ts 中均有:
"@locales": resolve("../../locales")也就是说,locales/目录是所有端共享资源的"唯一真源"(single source of truth),在哪个应用里改文案都是改同一份 JSON。
2. 新增一种语言前的准备
wiki/contribute-i18n.md 在 "Pay Attention" 一节强调:如果打算贡献一种全新语言,动工前必须先检查仓库 Issues 中是否已存在i18n标签下的相关认领/讨论(在仓库 Issues 页面按i18n标签筛选即可);若没有任何先例,应先提交一个新 issue 认领,避免多人同时翻译同一语言造成重复劳动与合并冲突。
新增语言的核心约定:英文en.json是结构基准,目标语言文件必须与它保持完全一致的 key 集合与嵌套层级。
3. 新增语言的标准操作流程
3.1 使用文档记载的生成命令
按原文档的记载,在仓库根目录执行:
npm run generator:i18n-template接着从列表中选择目标语言代码,脚本会自动完成两件事:
- 为目标 locale创建新的资源文件(即
locales/<namespace>/<lang>.json骨架); - 更新必要的配置文件(即上文提到的类型定义注册文件等)。
然后打开编辑器进入locales/目录,即可看到新生成的语言 JSON 文件。
仓库现状提示:在本工作区快照的根目录 package.json 中,
scripts实际注册的是dedupe:locales、lint、format等脚本,并未包含generator:i18n-template。因此若你检出的版本没有该命令,可直接采用 3.2 的等价手工方案——最终产物一致:在locales/<namespace>/下新增<lang>.json,并在第 3.3 节的注册点补上语言代码。
3.2 手工新增语言的等价步骤
- 为每个命名空间复制一份
en.json作为骨架(或先只做app、common等应用实际加载的命名空间); - 将骨架中的所有 value 翻译为目标语言,key 一律保持与
en.json完全一致; - 语言自名:在 locales/lang 对应文件中填上该语言的本地名称(例如新增简体中文时
lang/zh-CN.json应为{ "name": "简体中文" }); - 若目标语言在应用界面的语言选择器中需要启用,还要把语言代码补进各端"已注册语言"常量。
3.3 各端已注册语言与回退链
仓库中实际"启用"语言由源码中的常量数组决定,目前各端注册情况如下(均经源码核实):
| 端/进程 | 注册语言 | 源码位置 |
|---|---|---|
| 桌面端主进程(原生层) | en, zh-CN, zh-TW, ja | constants.ts |
| 桌面端渲染层 | en, zh-CN, zh-TW, ja, fr-FR | constants.ts |
| 移动端 | en, ja, zh-CN, zh-TW, fr-FR | constants.ts |
iOS 本地化的CFBundleLocalizations声明同样维护在 app.config.base.ts。新增语言时若希望某端真正可选,需同步更新对应常量。
另外要注意回退(fallback)链:桌面端渲染层 i18n.ts 中zh-TW → zh-CN → 默认语言,主进程 lib/i18n.ts 中zh-TW → zh-CN → en。这意味着繁体中文某 key 缺失时会先回退到简体中文再回退英文,不会直接显示裸 key;但作为贡献者仍应以各语言文件完整为目标,不要依赖回退机制。
4. 更新已有翻译:定位语言文件并翻译
维护/优化已有翻译相对直接:
- 进入 locales/ 目录;
- 找到目标语言的 JSON 文件(例如要改进法语,就编辑各命名空间下的
fr-FR.json); - 修改对应 key 的 value 即可,不要增删 key、不要改变 key 的拼写。
4.1 JSON 中的占位符、复数与富文本——必须原样保留
locales/中的文案并非纯文本,含三类必须保留的结构(都以英文文件为基准),以 locales/app/en.json 为例:
- 变量占位符
{{variable}}:翻译时整体移动位置、保留拼写,例如"discover.import.quota_warning": "You have {{remaining}} feeds remaining in your quota.""entry_actions.copied_notify": "{{which}} copied to clipboard."
- 复数/上下文后缀
_one、_other、_zero:i18next 依赖这些后缀区分复数形式,翻译时不能合并或删除,例如discover.search.results_one/discover.search.results_other、sidebar.category_unsubscribe_dialog.success_one/..._other/..._zero等成组 key; - 行内 HTML 标签(如
<br />、<b>、<span>):例如subscription_limit_warning的值中包含<br /><b>{{feedCount}}/{{feedLimit}} feeds</b>,翻译时标签与占位符都不得破坏。
翻译成简体中文后的正确示例:
{ "discover.import.quota_warning": "您的配额中还剩 {{remaining}} 个订阅源。", "entry_actions.copied_notify": "已将{{which}}复制到剪贴板。" }key 采用扁平点号路径写法(如sidebar.feed_actions.open_site_in_browser),值可以自由调整语序,但 key 本身在任何语言文件中都必须逐字节一致。
5. 翻译规范要点(来自原文档 + 仓库实现)
原 wiki/contribute-i18n.md 的 "Translation Guidelines" 给出了如下要求,仓库内另有自动化规则强制其中的"结构一致":
- 结构与 key 与英文版保持一致——不仅是"约定",更是仓库的硬性校验(见第 6 节);
- 译文要符合目标文化、贴合上下文,避免生硬的逐字直译;
- 尽量使用中性化、无性别倾向的表达;
- 保留
{{variable}}等占位符,以及第 4.1 节所述的复数后缀与行内标签。
5.1 命名空间内 key 语义隔离
翻译时可以借助命名空间判断文案上下文:settings/管设置项、shortcuts/管快捷键、errors/管报错、ai/管 AI 面板、app/管主体界面。同名相近 key(如两处都有 "Continue with {{provider}}")也要按各自语境分别翻译。
6. 质量保障:ESLint 规则与提交钩子
"非英文文件不得多出 en.json 中没有的 key、key 结构必须合法",这些不是口头要求,而是根目录 eslint.config.mjs 对locales/**/*.json启用的三条error级规则,实现在 plugins/eslint/eslint-check-i18n-json.js 与 plugins/eslint/eslint-recursive-sort.js:
| 规则 | 作用 | 出错时可自动修复 |
|---|---|---|
check-i18n-json/valid-i18n-keys | 校验 JSON key 集合合法:点号路径与上层前缀 key 不得互相冲突 | 否 |
check-i18n-json/no-extra-keys | 每个非en语言文件与同命名空间的en.json比对,不允许存在 en.json 中没有的 key | 是(自动删除多余 key) |
recursive-sort/recursive-sort | 强制 JSON 嵌套 key 按规则递归排序,保证多语言文件 diff 干净 | 是 |
其中no-extra-keys的实现值得注意:它对每个非en文件,自动定位到同一命名空间的en.json(path.dirname(filename)上溯后取en.json),把当前文件多出的 key 直接报告并支持--fix删除。这也解释了为什么新增 key 的正确姿势是"先改en.json,再同步各语言":向某个翻译文件添加en.json中没有的 key 会直接被校验拒绝。
6.1 提交即自动整理:lint-staged 钩子
根目录 package.json 中lint-staged对locales/**/*.json配置了特殊钩子:
"locales/**/*.json": [ "npm run dedupe:locales", "git add locales" ]dedupe:locales即eslint --fix locales/**。也就是说,只要 commit 触碰了 locales 下的 JSON,提交钩子就会自动对全部语言资源跑一次修复(删除多余 key、排序、修正格式)并把结果重新纳入暂存区。借助它,翻译文件的排序/多余 key 问题能在提交瞬间被"自动治好",无需手工整理。
6.2 手动执行校验
在仓库根目录随时可手动运行:
pnpm dedupe:locales # 自动修复并格式化 locales/**/*.json(等价于 eslint --fix locales/**) pnpm lint # 全仓库 lint(eslint),包含上述 i18n 规则仓库
packageManager为pnpm@10.17.0,脚本同时兼容npm run <script>写法。
6.3 移动端资源同步:copy-translation.ts
移动端的 locales/mobile/default 并不直接编辑全部 key,而是由 scripts/copy-translation.ts 把app命名空间中移动端复用的若干 key(如feed_form.*等)同步到mobile/default,并在目标已有该 key 时跳过覆盖。翻译app命名空间中与订阅表单相关的文案后,如需移动端生效,应运行该脚本或在移动端资源中补齐对应 key。
7. 本地验证译文效果
按原文档建议,改完翻译后应运行应用实测:
- 在本地运行对应应用(桌面端为
apps/desktop、移动端为apps/mobile,仓库使用 pnpm workspace 与 turbo 编排,pnpm dev:*相关命令见根目录 package.json); - 打开设置把界面语言切换到你所翻译的语言;
- 在应用内遍历你改动涉及的功能页面,核对文案在真实上下文中的显示是否自然、是否出现未翻译的英文或裸 key。
验证时特别留意:占位符替换后语序是否通顺、复数句式的翻译在数量为 0/1/多时是否都读得通、含<br />/<b>的富文本渲染是否正常。
8. 提交与合并建议
完成翻译并自测通过后,按原文档的建议提交:
- 为改动新建一个独立分支(避免直接在主干提交翻译);
- 提交信息清晰描述改动,例如
feat(i18n): add Korean translations或fix(i18n): improve French strings in app namespace; - 打开 Pull Request,并在描述中写明本次涉及的语言与命名空间/功能分区,方便维护者快速 review。
由于提交钩子会自动整理 locales 文件,PR 的 diff 通常非常干净(key 顺序与 en.json 完全一致、无多余 key),这也让维护者能一眼看出哪些 key 是你真正翻译/改动的部分。
9. 贡献翻译快速清单
动手前最后过一遍以下清单,可大幅降低返工概率:
- 新语言:已在 Issues 用
i18n标签检索并(如有必要)提交认领 issue; - 所有改动都以
en.json为结构基准,key 零增删、零拼写差异; {{variable}}占位符、_one/_other/_zero复数后缀、<br />等行内标签完整保留;- 译文贴合语境、自然通顺、尽可能使用无性别化表达;
- 已把新语言代码补进对应端的
langs常量(如需在界面可选)与 app.config.base.ts 的CFBundleLocalizations; - 本地
pnpm dedupe:locales通过(无 valid-i18n-keys / no-extra-keys / recursive-sort 报错); - 已运行应用切换到目标语言实测关键页面;
- 独立分支提交、PR 描述注明语言与命名空间。
Folo 的全栈多端产品形态决定了其 i18n 复杂度:一份英文基准、跨桌面/移动/Web 十个命名空间、外加 eslint + lint-staged 的强约束。遵循本文流程贡献翻译,你的改动就能与仓库既有的自动化体系无缝衔接,成为一份"可一键合并"的高质量贡献。
【免费下载链接】follow🧡 Folo is the AI RSS Reader项目地址: https://gitcode.com/GitHub_Trending/fol/follow
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考