news 2026/9/20 17:04:36

Readest 国际化标签重命名工作流:Key-as-Content 架构下的多语言键迁移与命令面板同步

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Readest 国际化标签重命名工作流:Key-as-Content 架构下的多语言键迁移与命令面板同步
  • 桌面应用
  • 跨平台
  • 前端

【免费下载链接】readest

Readest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.

项目地址:https://gitcode.com/gh_mirrors/re/readest
点击查看免费下载

导读

Readest 采用 "key-as-content"(键即内容)的国际化设计:设置面板上的英文标签文本本身就是翻译键,因此重命名一个 UI 标签等同于重命名全部语言翻译文件中的键。本文档总结的这套工作流(源自仓库内 i18n-label-rename-workflow.md 记忆文档)解决了三个核心问题:如何不靠猜测地为每个语言推导新翻译、如何防止设置搜索面板(Command Palette)因漏改而静默失同步、以及如何在不产生 diff 噪音的前提下部分回滚。读完本文,你将掌握一套可复现的、经过真实 PR(#5287、#5301)验证的标签重命名流程。

一、先理解 Readest 的 Key-as-Content 国际化架构

1.1 键就是内容:源码里写的是英文文本本身

在 Readest 的源码中,UI 文案的引用方式不是t('settings.font.overrideBookFont')这种语义键,而是直接把英文文本当作键。例如设置面板中:

  • LayoutPanel.tsx 中label={_('Additional Margin (%)')}
  • commandRegistry.ts 中labelKey: _('Additional Margin (%)')

这里的_是 misc.ts 导出的stubTranslation存根,可以推断其实现为"返回传入的键字符串本身"——它只在模块初始化阶段作为占位符,真正渲染时由react-i18nextt函数根据当前语言查找翻译。也就是说,翻译键与英文默认文案是同一条字符串public/locales/<code>/translation.json中的 JSON key 就是界面显示的文字。

1.2 翻译键在哪些文件中流转

  • 键的提取pnpm i18n:extract(定义于 package.json)调用i18next-scanner,扫描src/**/*.{js,jsx,ts,tsx}中的_('...')调用,把新键写入各语言文件。
  • 语言清单:i18n-langs.json 列出全部可翻译语言(en是源语言,不在此列,见 i18n.ts 的注释与SUPPORTED_LNGS = ['en', ...translatableLngs])。
  • 翻译资源:public/locales/ 下每个语言目录各有一份translation.json,由浏览器端i18next-http-backend/locales/{{lng}}/{{ns}}.json加载。

1.3 关键结论:重命名标签 = 重命名键

正因为 key-as-content,重命名一个 UI 标签会改变它在所有语言翻译文件中的 JSON key。那些只改界面英文、不同步 30 多种语言文件的 PR 是必然不完整的;反过来,像 #5287(从 Header & Footer 行中删掉 "Show")和 #5301("Column Gap" 改为 "Additional Margin")这种"看似只改文案"的 PR,本质上是跨语言文件的 i18n PR。这一点决定了后续所有操作都要以"全语言键迁移"的视角来执行。

二、不要重新翻译:从每个语言的旧值机械推导新值

2.1 直觉上的错误做法

拿到新标签后最直觉的做法是:把英文新文本丢给译者/机器翻译,得到各语言的新翻译。文档明确警告这不可取——它会把译者自己的术语习惯替换成你的用词,破坏既有译文的一致性。例如德语社区习惯用 "Verbleibende Zeit",若你按英文 "Remaining Time" 重新翻译,可能得到风格不一致的结果。

2.2 正确做法:git show恢复旧值并剥离被改动的词

因为仓库是 git 管理的,旧键及其翻译仍完整保存在历史提交中。用下面的命令从基准提交恢复某个语言的旧 translation.json:

git show <base>:apps/readest-app/public/locales/<code>/translation.json

<base>是重命名前的基准提交(通常是改动前的分支点或合并基)。拿到旧文件后,找到旧键(如"Show Remaining Time"),把被删掉的词从对应翻译中机械剥离,即得到新键的翻译。

#5287 的真实案例(文档原例):

语言旧键与旧值(来自 git show)新键与新值(机械剥离后)
德语"Show Remaining Time": "Verbleibende Zeit anzeigen""Remaining Time": "Verbleibende Zeit"

这种"剥离"是纯机械操作,无需任何翻译猜测;对全部语言逐一执行,一致性由构造过程本身保证。当前仓库 de/translation.json 中已能看到重命名后的结果"Remaining Time": "Verbleibende Zeit",与此工作流的产出完全吻合。其余语言(如 ar"الوقت المتبقي"、es"Tiempo restante"、fr"Temps restant"、bn"বাকি সময়"、el"Υπολειπόμενος χρόνος")同样只保留了剥离后的核心词,验证了这套推导逻辑在真实仓库中的落地效果。

提示:如果个别语言在剥离后出现语法不完整或词序问题,只对该语言做最小修正,而不是推倒重译,以保持与其他语言的推导结果同构。

三、先查重:新键可能已经存在

在动手修改所有语言文件之前,先确认新键是否已经存在于翻译文件中#5287中 "Remaining Time" 是已存在的键(Reading Progress相关区域已有类似条目),提取器在扫描到它时会直接复用已有键,于是你少写一个字符串——旧值 "Show Remaining Time" 下的翻译直接并入既有键,无需为每个语言新增条目。

实操建议:在动手前对public/locales/en/translation.json和任一语言文件 grep 新键文本:

grep -n '"Remaining Time"' apps/readest-app/public/locales/en/translation.json

若命中,直接复用即可;若未命中,才需要走第二节的旧值剥离流程。

四、别漏掉 commandRegistry.ts:设置搜索面板的镜像同步

4.1 labelKey 机制与失同步风险

Readest 的设置搜索面板(Command Palette)由 commandRegistry.ts 驱动,它镜像了设置面板中的一部分标签,作为CommandItem.labelKey。该文件的labelKey值必须与 LayoutPanel.tsx 等面板中使用的_('...')保持一致,否则面板与设置界面显示不同的文案,搜索也会失准。例如当前commandRegistry.ts中的:

  • labelKey: _('Additional Margin (%)')(L295),对应 #5301 的重命名结果;
  • labelKey: _('Show Header')labelKey: _('Show Footer')(L319、L325)等 Header & Footer 相关条目。

漏改commandRegistry.ts是静默的:不会有编译错误,面板上的标签已更新,但命令面板仍然引用旧键,导致面板与设置页显示不一致,且搜索旧名/新名都可能失败。

4.2 keywords 数组是独立命名的,旧搜索词可以保留

每个CommandItemkeywords数组与labelKey相互独立。例如段落间距相关条目的keywords: ['paragraph', 'margin', 'spacing', 'gap']。这意味着当标签从 "Show Remaining Time" 改为 "Remaining Time" 后,保留 keywords 中的'show'并不会被新标签污染——搜索面板仍能通过旧词show命中该条目。getSearchableText(L41-L52)把localizedLabellabelKeypanelpanelLabelsectionkeywords全部拼入 fzf 的搜索文本,所以 keywords 相当于给用户保留了"老用户习惯的搜索词",这是重命名时应当刻意保留的兼容性设计。

4.3 相关的搜索与渲染链路

  • 搜索:searchCommandsfzf(smart-case、normalize、limit 50)对组合文本检索,命中位置再映射回标签区间用于高亮;
  • 渲染:CommandPalette.tsx 通过_(item.labelKey)实时取当前语言的翻译并高亮命中字符。

对这条链路的回归测试参见 command-registry-extended.test.ts,其中通过stubTranslation: (key) => key让测试聚焦于注册表结构与搜索逻辑本身。

五、部分回滚而不制造 diff 噪音

5.1 问题:i18n:extract只追加,不回写原位置

i18next-scanner的配置见 i18next-scanner.config.cjs:

{ sort: false, // 不按键名排序,保持文件既有顺序 removeUnusedKeys: true, // 清理源码中不再引用的键 defaultValue: '__STRING_NOT_TRANSLATED__', keySeparator: false, nsSeparator: false, func: { list: ['_'], extensions: ['.js', '.jsx', '.ts', '.tsx'] }, // ... }

关键点在于sort: false重新提取只会把"重新出现的键"append 到文件末尾。如果你把重命名过的键又改回去,直接跑pnpm i18n:extract会得到"键被移到文件底部"的 diff——键内容没变,位置变了,在代码评审里表现为一次毫无意义的 move,污染 diff。

5.2 正确做法:按基准提交的键顺序重建 JSON

文档给出的无损重建流程(建议用脚本执行):

  1. 读取基准提交<base>)中各语言的translation.json,拿到该语言文件的键顺序;
  2. 逐键构造新文件:存活下来的键取当前值要恢复的键取基准值(即撤销重命名带来的改动);
  3. 把真正新增的键 append 到文件末尾;
  4. 重新运行pnpm i18n:extract并确认结果是no-op(没有任何改动)——这一步证明手工重建的文件与扫描器将要产出的文件完全一致,是最有说服力的自检。

5.3 为什么"no-op"能证明正确性

因为 scanner 的写入逻辑是确定性的(removeUnusedKeys决定删哪些、sort: false决定不重排),若重建文件经过提取后 diff 为空,就说明:所有应存在的键都在、位置正确、不应存在的键(如已被移除的旧键)已清理。反之若 diff 非空,对照差异即可定位是漏了键还是多写了键。

六、收尾验证清单

合并前建议按以下顺序自查,全部通过即可视为重命名完成:

  1. 键覆盖grep新键确认存在于所有可翻译语言文件(apps/readest-app/public/locales/*/translation.json),旧键不再残留;
  2. 面板镜像:确认src/services/commandRegistry.ts中对应labelKey已改为新键,且keywords保留旧搜索词;
  3. 提取无副作用:运行pnpm i18n:extractgit status干净(或 diff 仅含预期改动);
  4. 翻译质量门禁:运行pnpm check:translations(package.json)——它扫描各语言文件中是否残留__STRING_NOT_TRANSLATED__占位符(scanner 的defaultValue),确保没有漏翻的键混入;
  5. 视觉回归:设置面板与命令面板分别打开,确认文案一致、搜索命中正常(仓库 e2e 与 Playwright 测试目录apps/readest-app/e2e/可作参考)。

七、工作流总结

这套工作流可以浓缩为四条纪律:

  1. 重命名即全量迁移——key-as-content 决定了改标签就是改 30 多种语言文件的键,不存在"只改英文"的捷径;
  2. 推导优于翻译——用git show <base>:apps/readest-app/public/locales/<code>/translation.json恢复旧值、机械剥离被改词,而不是重新翻译;
  3. 先复用再新增——新键若已存在,提取器会复用,省去全部语言的新增工作;
  4. 镜像与回归并举——同步commandRegistry.tslabelKey(保留keywords兼容旧词),回滚时按基准键序重建文件并让pnpm i18n:extract验证为 no-op。

它已在 #5287 与 #5301 两次真实重命名中验证过,且与仓库内其他国际化记忆(如 i18n 提取键清理、反馈翻译中的复数处理、基于 Playwright 的设置面板截图)共同构成 Readest 的 i18n 维护方法论。

  • 桌面应用
  • 跨平台
  • 前端

【免费下载链接】readest

Readest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.

项目地址:https://gitcode.com/gh_mirrors/re/readest
点击查看免费下载

相关推荐

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

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

大模型本地部署全指南:硬件选型、工具实战与避坑手册

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 17:04:14

别找临时中转:用 TaoToken 给 Roo Code 做 OpenAI 兼容通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 17:02:55

AC-AC变换电路并联运行:均流控制与环流抑制设计要点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 17:01:24

方程式赛车尾翼优化:空气动力学基础与CFD仿真落地实践

简介&#xff1a;这是一份关于方程式赛车尾翼优化设计的专业资料&#xff0c;面向车辆工程专业学生、FSC车队成员及空气动力学入门者&#xff0c;重点讲解尾翼下压力提升与人工攻角调整等工程问题。文档从汽车扰流器概念入手&#xff0c;用机翼剖面图说明气流速度与压强的关系&…

作者头像 李华
网站建设 2026/9/20 17:01:09

2025保密教育知识题库高效备考指南:避开误区吃透核心考点

简介&#xff1a;这份2025最新保密教育知识题库与答案文档&#xff0c;面向机关单位保密干部、涉密人员及参加保密教育培训的学员&#xff0c;用于系统复习保密法律法规、国家安全教育和密码安全知识。内容以选择题与判断题为主&#xff0c;覆盖全民国家安全教育日、涉密会议管…

作者头像 李华
网站建设 2026/9/20 16:59:03

Element 3D v2.2.2安装教程:AE三维插件从下载到出片全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华