Readest 国际化键提取陷阱:`pnpm i18n:extract` 误删翻译键的成因与零干扰提交流程
发布时间:2026/9/20 23:54:45来源:尧图网络
桌面应用跨平台前端【免费下载链接】readestReadest 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点击查看免费下载本指南聚焦 Readestapps/readest-app一个基于 Next.js/Tauri 的多平台电子书阅读器的 i18n 开发工作流剖析其核心命令pnpm run i18n:extracti18next-scanner在功能分支上静默删除约 30 个合法翻译键、制造约 1000 行无关 diff的经典事故并给出完整的规避方案如何只提交新键、如何用零额外 diff 的方式手工写入 34 个语言文件、以及如何在提交前验证翻译覆盖完整性。读完你将掌握 Readest 特有的key-as-content翻译模型的正确维护姿势避免把扫描器的误删带进无关 PR。背景Readest 的key-as-content国际化模型要理解i18n:extract的删除行为必须先理解 Readest 的翻译模型源码中的字符串字面量本身就是翻译键key英语文案即是键本身这与键名 英语值分离的传统 i18n 方案截然不同。这一模型体现在三个关键文件上文件角色apps/readest-app/i18n-langs.json可翻译语言清单当前共 34 个语言码de、ja、es、fa、fr、it、el、ko、uk、nl、sl、sv、pl、pt、ru、tr、hi、id、vi、ms、he、ar、th、bo、bn、ta、si、ro、hu、uz、ka、pt-BR、zh-CN、zh-TW不包含enapps/readest-app/public/locales/ /translation.json各语言的翻译资源键与源字符串一一对应apps/readest-app/src/i18n/i18n.tsi18next 初始化SUPPORTED_LNGS [en, ...translatableLngs]即en是源语言关键机制在翻译调用侧apps/readest-app/src/hooks/useTranslation.ts 中的 hook 以t(key, { defaultValue: key, ...options })调用 i18next——任何未在en/translation.json中显式登记的字符串渲染出的英语就是键本身。因此 apps/readest-app/public/locales/en/translation.json 可以保持极小规模当前仓库中仅约 120 行只包含两类条目复数变体base_one/base_other及个别语言需要的_few/_many等 CLDR 形式专有名词覆盖如字体名LXGW WenKai GB Screen其英语值与键不同。en文件之所以从不进 extract 的 diff是因为i18next-scanner.config.cjs的lngs数组直接读取i18n-langs.json不含en扫描器只为列出的语言生成占位符en永远不被触碰。事故现场一次 extract 制造 ~1000 行无关改动在功能分支上运行提取命令见 apps/readest-app/package.json 中定义的脚本cd apps/readest-app pnpm run i18n:extract其底层是i18next-scanner --config i18next-scanner.config.cjs。在i18next-scanner.config.cjs的options中removeUnusedKeys: true见 apps/readest-app/i18next-scanner.config.cjs意味着扫描器会把在当前扫描范围内找不到源码引用的键全部删除。典型后果是每个非en语言文件被删除约 30 个看起来完全合法的键例如Sync History、downloaded {{n}} book(s)、Match Whole Words整体 diff 出现约 1000 行增减/-与本 PR 的功能改动毫无关系。这些键之所以被误删是因为已提交的语言文件可能领先于当前分支的源码要么键对应的功能尚未合入当前基线分支要么字符串由动态拼接或在未被扫描的模块如非src/**下的生成代码、worker、部分插件中构建静态扫描器根本无法发现它们。根因静态扫描的边界与removeUnusedKeysapps/readest-app/i18next-scanner.config.cjs 的完整关键配置如下const options { debug: false, sort: false, func: { list: [_], // 只识别 _(...) 形式的调用 extensions: [.js, .jsx, .ts, .tsx], }, lngs, // 来自 i18n-langs.json不含 en ns: [translation], defaultValue: __STRING_NOT_TRANSLATED__, // 新键占位值 resource: { loadPath: ./public/locales/{{lng}}/{{ns}}.json, savePath: ./public/locales/{{lng}}/{{ns}}.json, jsonIndent: 2, // 输出恰好是 JSON.stringify(obj, null, 2) lineEnding: \n, }, keySeparator: false, nsSeparator: false, allowDynamicKeys: true, removeUnusedKeys: true, // ← 误删的根源 };配置文件还自行解析了输入 globresolveInput扫描src/**/*.{js,jsx,ts,tsx}并排除*.test.*文件这一细节进一步说明扫描范围的边界只有静态出现在这些被扫描源文件里的_(...)字面量才会被认定为在用。于是当分支源码落后于已提交语言文件时removeUnusedKeys就会把找不到引用的键当作废弃键批量清除。顺带一提defaultValue: __STRING_NOT_TRANSLATED__与 CI 校验直接相关apps/readest-app/package.json 中的check:translations会 greppublic/locales/*里是否残留__STRING_NOT_TRANSLATED__一旦有未翻译占位符pnpm check:all即失败。也就是说漏翻译会被 CI 拦住而误删除不会被拦住——这正是需要靠流程来兜底的原因。安全提交流程为新增功能添加少量字符串的标准做法对于为一个功能新增若干字符串的改动核心原则是绝不把扫描器的删除结果提交进无关 PR。推荐流程如下。第 1 步运行 extract可选仅用于确认新键cd apps/readest-app pnpm run i18n:extract此时非en语言文件会同时出现新增键 误删键。运行它的目的只是确认哪些键是真正新增的占位值为__STRING_NOT_TRANSLATED__。第 2 步丢弃全部 churn只保留手工改动git checkout -- apps/readest-app/public/locales这条命令把public/locales下的所有变更包括误删与占位符全部还原保证分支回到干净状态。务必在动手编辑语言文件之前执行否则会覆盖你自己已写入的翻译。第 3 步手工向每个语言文件追加新键只把你自己的新键逐个语言写入。由于配置文件规定输出格式精确为JSON.stringify(obj, null, 2) \n对应 i18next-scanner.config.cjs 的jsonIndent: 2与lineEnding: \n用一段 Node 脚本JSON.parse→ 追加新键 → 原样重写即可得到零额外 diff的结果且插入顺序被保留JSON 对象键序在解析重写后维持原序新键落在末尾。示意如下// 在 apps/readest-app 目录下运行的示意脚本请勿直接提交进仓库 import { readFile, writeFile } from node:fs/promises; import langs from ./i18n-langs.json with { type: json }; const NEW_KEYS { de: { Sync History: Verlauf }, zh-CN: { Sync History: 同步历史 }, // ...为每个语言补充真实翻译跳过 en }; for (const lng of langs) { if (lng en) continue; // en 是 key-as-content不写 const file ./public/locales/${lng}/translation.json; const obj JSON.parse(await readFile(file, utf8)); for (const [key, value] of Object.entries(NEW_KEYS[lng] ?? {})) { if (!(key in obj)) obj[key] value; // 已存在则跳过避免重复 } await writeFile(file, JSON.stringify(obj, null, 2) \n); }翻译时应匹配各语言既有的术语先在对应语言文件里 grep 一个相关键例如Export Annotations/Annotations复用该语言已经确立的领域名词而不是凭英文重新意译出另一个正确但不同的同义词——后者在 UI 上会表现为用词不一致。第 4 步验证覆盖完整grep -rn Your Key apps/readest-app/public/locales | wc -l结果应等于语言数量当前i18n-langs.json为 34en只含复数/专有名词覆盖普通键不会出现在en文件中因此精确匹配计数即非en语言数。每个新键都应逐一验证。进阶复制键重命名与部分还原的零 churn 技巧Readest 的 key-as-content 模型还有一个连带效应重命名 UI 标签等于重命名翻译键需要同步 34 个语言文件相关经验记录见 apps/readest-app/.claude/memory/i18n-label-rename-workflow.md。正确做法不是从零重新翻译新标签而是从基线提交取回每个语言的旧值再机械地去掉被改动的词git show base:apps/readest-app/public/locales/code/translation.json例如Show Remaining Time: Verbleibende Zeit anzeigen在去掉 Show 后变成Remaining Time: Verbleibende Zeit全程无需猜词。若新键已经存在于文件中如Reading Progress本就存在extract 会直接复用还能少写一个字符串。重命名还有一个容易被遗漏的镜像点apps/readest-app/src/services/commandRegistry.ts 用labelKey镜像了部分设置项标签用于设置搜索面板command palette。漏掉它会导致面板与设置页文案静默脱节而其keywords数组是独立的保留旧词可让搜索继续命中。另一个技巧用于部分还原场景pnpm i18n:extract只会把重新加回的键追加到文件末尾diff 中会显示为移动产生不必要噪声。更干净的做法是遍历基线提交的键顺序保留当前值仍在的键、用基线值还原被还原的键再追加真正的新键随后重跑一次pnpm i18n:extract若输出为 no-op即证明文件与扫描器将生成的结果完全一致。复数键与术语一致性的两条铁律两条与本文直接相关的团队铁律分别记录在apps/readest-app/.claude/memory/feedback_en_plurals_manual.md非复数字符串严禁写入en/translation.json键即英语值只有复数字符串需要手工在en中补base_one/base_other变体——否则count: 1会回退到裸键渲染出 1 days 这类错误。约定是_one里保留{{count}}插值并把(s)占位换成真正的单数名词如Are you sure to delete {{count}} selected book(s)?_one→Are you sure to delete {{count}} selected book?。可编写审计脚本遍历src/中带count的_(...)调用核对每个 base 键的_one/_other是否齐全。apps/readest-app/.claude/memory/i18n-match-established-locale-terms.md填写__STRING_NOT_TRANSLATED__占位符前先查该语言既有的领域名词并复用例如用jq -r .[Highlights], .[Notes], .[Bookmarks] public/locales/l/translation.json摸底再用小写化 词干匹配的脚本跨全部语言审计少数语言内部本身就无统一术语如bo的 Highlight/Highlights 不一致时宁可跳过并询问也不要凭猜。提交前自检清单新键是否在全部 34 个非en语言文件中有真实翻译非__STRING_NOT_TRANSLATED__占位git checkout -- apps/readest-app/public/locales已清除所有扫描器误删的 churndiff 中只有你的新键复数键是否在en/translation.json中补齐了_one/_other若涉及设置项标签重命名commandRegistry.ts 的labelKey是否同步pnpm check:translationsgrep__STRING_NOT_TRANSLATED__与pnpm check:all通过。这套流程的价值在于它把扫描器是全量删除的、静态的、只认当前分支源码这一特性纳入掌控让每个 PR 的 locales diff 严格等于功能所需的新键既不误删既有翻译也不污染无关改动。赞分享桌面应用跨平台前端【免费下载链接】readestReadest 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点击查看免费下载相关推荐focus.nvim高级技巧如何利用方向键实现智能窗口分割与导航focus.nvim高级技巧如何利用方向键实现智能窗口分割与导航 作为一名 Neovim 用户你是否厌倦了手动调整窗口大小的繁琐操作是否希望在多个分割窗口Umi-OCR 国际化翻译全流程实战lupdate 提取、Linguist 翻译与 lrelease 编译Umi OCR 国际化翻译全流程实战lupdate 提取、Linguist 翻译与 lrelease 编译 本篇指南完整拆解 Umi OCR 的 Qt/QMLOCR桌面应用Readest 的 Apple App Store 与 TestFlight 提交fastlane lanes 设计及三个关键工程陷阱Readest 的 Apple App Store 与 TestFlight 提交fastlane lanes 设计及三个关键工程陷阱 导读 本文基于 Rea桌面应用跨平台前端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网