- 桌面应用
- 跨平台
- 前端
【免费下载链接】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.
导读
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-i18next的t函数根据当前语言查找翻译。也就是说,翻译键与英文默认文案是同一条字符串,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 数组是独立命名的,旧搜索词可以保留
每个CommandItem的keywords数组与labelKey相互独立。例如段落间距相关条目的keywords: ['paragraph', 'margin', 'spacing', 'gap']。这意味着当标签从 "Show Remaining Time" 改为 "Remaining Time" 后,保留 keywords 中的'show'并不会被新标签污染——搜索面板仍能通过旧词show命中该条目。getSearchableText(L41-L52)把localizedLabel、labelKey、panel、panelLabel、section、keywords全部拼入 fzf 的搜索文本,所以 keywords 相当于给用户保留了"老用户习惯的搜索词",这是重命名时应当刻意保留的兼容性设计。
4.3 相关的搜索与渲染链路
- 搜索:
searchCommands用fzf(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
文档给出的无损重建流程(建议用脚本执行):
- 读取基准提交(
<base>)中各语言的translation.json,拿到该语言文件的键顺序; - 逐键构造新文件:存活下来的键取当前值,要恢复的键取基准值(即撤销重命名带来的改动);
- 把真正新增的键 append 到文件末尾;
- 重新运行
pnpm i18n:extract并确认结果是no-op(没有任何改动)——这一步证明手工重建的文件与扫描器将要产出的文件完全一致,是最有说服力的自检。
5.3 为什么"no-op"能证明正确性
因为 scanner 的写入逻辑是确定性的(removeUnusedKeys决定删哪些、sort: false决定不重排),若重建文件经过提取后 diff 为空,就说明:所有应存在的键都在、位置正确、不应存在的键(如已被移除的旧键)已清理。反之若 diff 非空,对照差异即可定位是漏了键还是多写了键。
六、收尾验证清单
合并前建议按以下顺序自查,全部通过即可视为重命名完成:
- 键覆盖:
grep新键确认存在于所有可翻译语言文件(apps/readest-app/public/locales/*/translation.json),旧键不再残留; - 面板镜像:确认
src/services/commandRegistry.ts中对应labelKey已改为新键,且keywords保留旧搜索词; - 提取无副作用:运行
pnpm i18n:extract后git status干净(或 diff 仅含预期改动); - 翻译质量门禁:运行
pnpm check:translations(package.json)——它扫描各语言文件中是否残留__STRING_NOT_TRANSLATED__占位符(scanner 的defaultValue),确保没有漏翻的键混入; - 视觉回归:设置面板与命令面板分别打开,确认文案一致、搜索命中正常(仓库 e2e 与 Playwright 测试目录
apps/readest-app/e2e/可作参考)。
七、工作流总结
这套工作流可以浓缩为四条纪律:
- 重命名即全量迁移——key-as-content 决定了改标签就是改 30 多种语言文件的键,不存在"只改英文"的捷径;
- 推导优于翻译——用
git show <base>:apps/readest-app/public/locales/<code>/translation.json恢复旧值、机械剥离被改词,而不是重新翻译; - 先复用再新增——新键若已存在,提取器会复用,省去全部语言的新增工作;
- 镜像与回归并举——同步
commandRegistry.ts的labelKey(保留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.
相关推荐
kbar国际化支持:多语言命令面板的实现方法
kbar国际化支持:多语言命令面板的实现方法 想要为你的网站或应用添加一个功能强大且支持多语言的命令面板吗?kbar 提供了完美的解决方案!作为一款快速、轻量级
前端UI组件OBS Studio插件开发与配置:进阶用户完整指南
OBS Studio插件开发与配置:进阶用户完整指南 OBS Studio作为开源直播录制软件的标杆,其强大的插件系统是功能扩展的核心。本文将深入探讨OBS插件
音视频直播屏幕录制桌面应用视频终极Lucide图标国际化指南:从零开始的多语言语义化命名实践
终极Lucide图标国际化指南:从零开始的多语言语义化命名实践 Lucide作为一款由社区打造的精美且一致的图标工具包,是Feather Icons的衍生开源项
前端UI组件设计系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考