🌐 新增翻译完整性机械检查 (check:i18n)#1606
Conversation
PR scriptscat#1587 (pt-BR) 合并后发现 messages.json 与 monaco 编辑器语言遗漏,且当时无机制拦截; 本次新增 scripts/check-i18n.mjs 并接入 pnpm lint / lint:ci,机械校验: - src/locales/<locale>/*.json 各命名空间 key 是否与 en-US 一一对应(缺失/多余均报错) - src/locales/<locale>/index.ts 是否导出全部命名空间 - src/assets/_locales/<chrome-locale>/messages.json 与 en/messages.json 的 key 是否一致 (尚未创建该目录时仅提示,不阻塞) - docs/references/terminology-<locale>.md 是否存在(每个 locale 必须有,缺失即报错) - src/pkg/utils/monaco-editor/langs.ts 中 editorLangs 各 locale 的 key 是否与 en-US 一致 (尚未创建该 locale 条目时仅提示;已创建则 key 必须对齐) 已用 scriptscat/scriptcat 官方仓库的 PR scriptscat#1568(韩语)与 scriptscat#1587(pt-BR)实际验证: 正确通过完整、正确的翻译提交,也能在人为剔除 key / 文件时正确报错。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
用 PR scriptscat#1605 实测时发现:check-i18n.mjs 硬编码了单文件 src/pkg/utils/monaco-editor/langs.ts 路径,一旦该文件被拆分为 langs/<locale>.ts + langs/index.ts(如 scriptscat#1605 所做),检查会误报 "文件缺失"。改为先探测 langs/index.ts 是否存在,再退回单文件路径;并将 key 展开逻辑改为 按模块解析(parseModule/flattenNode),支持跨文件解析 import 绑定(如 index.ts 里 "pt-BR": ptBR 指向 ./pt-BR.ts 的 default export)与同文件 shorthand 属性 (如 pt-BR.ts 内的 grantValuePrompts,)。 已用 scriptscat#1605 的实际分支验证:新结构下 0 误报,人为在拆分后的 pt-BR.ts 中删除顶层 key 与 grantValuePrompts 子 key 均能正确报出具体路径。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CI 的 lint:ci 已经跑 check:i18n,但那只在 push/PR 之后才会看到红叉。 本次让本地 pre-commit 钩子在暂存区涉及翻译相关路径 (src/locales/、src/assets/_locales/、docs/references/terminology-*.md、 src/pkg/utils/monaco-editor/langs.ts 或 langs/ 拆分文件)时, 提前跑一遍 pnpm run check:i18n,不通过则直接拒绝提交, 不需要等到推送后在 CI 里才发现。 真正能拦住"不通过就不许合并"的还有一层:仓库 main 分支的 branch protection 需要把 Lint 设为 required status check, 这一步需要 scriptscat/scriptcat 的仓库管理员在 GitHub 设置里配置, 不是贡献者这边能做的。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
之前的触发范围包含 terminology-*.md、langs.ts/langs/* 等非 json 文件, 导致本 PR 自身改动 .husky/pre-commit、scripts/check-i18n.mjs 这类非 翻译内容的提交也可能被牵连(虽然本次未命中,但范围过宽)。收窄为只在 暂存区包含 src/locales/**/*.json 或 src/assets/_locales/**/*.json 时才 触发 pnpm run check:i18n,其余改动(含本 PR 自身)不受影响,仍可正常 提交推送。CI 侧的 lint:ci 不受影响,仍覆盖全部 5 类检查。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
manually tested |
|
复核了当前 head
建议先修复上述 false negative 和 staged-content 问题,再基于最新 main 重跑 |
CodFrm 在 PR scriptscat#1606 review 中指出: pre-commit 的 check:i18n 校验的是工作区而非 Git 暂存快照(暂存坏版本、工作区改回好版本可绕过检查,删除操作也不触发); check-i18n.mjs 从未与 src/locales/locales.ts 的实际注册(NS/resources/import) 交叉核对,index.ts 导出检查只是子串匹配可被注释字符串骗过;Monaco 自定义 AST evaluator 对 spread、计算属性、循环别名等无法解析的结构会静默折叠为空/叶子键集 而非报错;Chrome _locales 与 Monaco 覆盖面缺失只是 warning。 本次改动: - scripts/git-staged-snapshot.mjs: 用 `git checkout-index` 直接从索引物化暂存 快照,.husky/pre-commit 改为对该快照跑 check:i18n(--diff-filter 补上 D)。 - scripts/check-i18n.mjs: 新增 locales.ts 双向一致性检查(NS 数组 vs 命名空间 文件、import/resources vs 磁盘目录);index.ts 导出改用真实 AST 解析;Monaco AST evaluator 对 spread/计算属性/不支持的表达式/循环引用/语法错误一律报错 (fail-closed);Chrome _locales 与 Monaco 覆盖面缺失由 warning 升级为 error。 - 新增 scripts/check-i18n.test.mjs、scripts/git-staged-snapshot.test.mjs 覆盖 上述所有可复现场景。 - docs/translation.md 同步更新为当前 fail-closed 行为。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
@CodFrm 复核意见属实,已在 ffae9f1 修复,逐条对应如下:
验证: 麻烦再帮忙复核一下,如果还有遗漏的场景我再补。 |
check-i18n.mjs 与 git-staged-snapshot.mjs 都用
`import.meta.url === \`file://${process.argv[1]}\`` 判断是否被直接执行。
这个比较在两种情况下永不成立,main() / CLI 分支不执行,进程零输出 exit 0:
- `import.meta.url` 是 percent-encoded 的,而 argv[1] 是原始路径:仓库路径含
空格或非 ASCII 字符(如 ~/我的项目/scriptcat)时两者不等;
- `import.meta.url` 会解析软链而 argv[1] 不会:macOS 的 /tmp、/var 即是软链。
后果是本 PR 建立的机制整体失效且毫无迹象——`pnpm lint` / `lint:ci` 里这一格
永远绿,pre-commit 中两个脚本以 `&&` 串联、一起放行坏提交。实测在
~/…/中文目录/ 下删掉 zh-CN/common.json 的一个 key 后 `git commit` 直接落库,
而同样的破坏在 ASCII 路径下会被正确拦截。这恰好是脚本自身
"fail-closed by design" 承诺要杜绝的失败模式。
改为两边都归一化成真实文件路径再比对,同时覆盖编码与软链两种情形。
补充 CLI 入口回归测试:原有用例都直接调 runCheck(),绕过了 CLI 入口,
覆盖不到"脚本到底有没有被执行"这一层。新用例把脚本复制到含空格 /
非 ASCII 字符的目录下真实 spawn,断言干净树输出通过信息、缺 key 时
exit 1。因需拉起 node 子进程(实测 300ms+),显式放宽这些用例的超时,
不套用 vitest.config.ts 给单元测试定的 340ms 预算。
|
评审时发现一个 fail-open,已直接推到本分支(e34fd967),说明如下。 问题两个脚本都用这个判断是否被直接执行: if (import.meta.url === `file://${process.argv[1]}`) {这个比较在两种情况下永不成立,
后果是本 PR 建立的机制整体失效且毫无迹象: 复现(修复前)两个真实 clone,同一个破坏(删掉 修复两边都归一化成真实文件路径再比对,同时覆盖编码与软链: if (process.argv[1] && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) {修复后同一个 CJK 仓库:坏改动被拦截(HEAD 不动),合法改动正常放行( 补充的测试原有 23 个用例都直接调 因为要拉起 node 子进程(实测 300ms+,而 其余部分跑下来质量很高,没有别的阻塞项: 另:正文「已知限制」一节还停留在早期版本(说第 3、5 项只给 warning,实际现在是硬失败),也没提到 check 0( |
Checklist / 检查清单
背景
#1587(pt-BR 翻译)合并后,评审中发现该 PR 遗漏了
messages.json(chrome.i18n 商店信息)与 Monaco 编辑器语言(editorLangs),当时没有机制能自动拦截这类遗漏("we don't have a solid mechanism to prevent this")。本 PR 为此补上一个机械检查脚本,并在本地提交与 CI 两处强制执行。本次改动
新增
scripts/check-i18n.mjs,并通过新增的pnpm run check:i18n接入pnpm lint/pnpm lint:ci。针对src/locales/下的每个 locale,以en-US为基准做以下校验:src/locales/<locale>/*.json:每个命名空间文件的 key 是否与en-US一一对应,缺失或多余都会报错。src/locales/<locale>/index.ts:是否导出了en-US拥有的全部命名空间。src/assets/_locales/<chrome-locale>/messages.json:与en/messages.json的 key 是否一致(即 Brazilian Portuguese/pt-BR Translation #1587 遗漏的部分)。chrome 目录名通过实际扫描_locales/目录解析(如ko-KR→ko、zh-CN→zh_CN),而非写死的映射表,避免映射表本身过期失效。docs/references/terminology-<locale>.md:每个 locale 必须有对应术语规范文件,缺失即报错。src/pkg/utils/monaco-editor/langs.ts(或未来拆分后的langs/<locale>.ts+langs/index.ts):editorLangs(悬浮提示、脚本头字段提示,即 Brazilian Portuguese/pt-BR Translation #1587 遗漏的另一部分)用 TypeScript 编译器 API 解析。它是手写的const对象而非 JSON,部分属性引用了同文件内其他顶层常量(如grantValuePrompts: grantValuePromptsEnUS)或跨文件import绑定(拆分后每个 locale 一个文件,index.ts里"pt-BR": ptBR指向./pt-BR.ts的export default),解析时都会展开后再与en-US比对 key。同时:
docs/translation.md新增"机械检查:遗漏翻译"一节并更新翻译检查清单。.husky/pre-commit中新增一段:只要暂存区包含src/locales/**/*.json或src/assets/_locales/**/*.json,本地提交前就会先跑一遍pnpm run check:i18n,不通过直接拒绝提交——不用等推送后才在 CI 里看到红叉。范围刻意收窄为 json 文件本身(不含terminology-*.md、langs.ts/langs/),这样改动检查脚本本身或调整 terminology/langs 这类周边文件时不会被连带拦下;这些周边文件的完整性仍由 CI 侧的lint:ci(覆盖全部 5 类检查)兜底。已知限制
pt-BR的_locales目录、pt-BR/tr-TR的editorLangs条目),本次检查仅给出提示(warning),不会导致失败——这两处目前是可选/尽力而为的内容(参见docs/translation.md),与项目现状一致。但只要该 locale 已经创建了对应条目,其 key 就必须与en-US保持一致,否则报错。这个折衷是为了不让本 PR 自身因为与本次改动无关的历史遗留缺口(tr-TR/pt-BR的editorLangs)而在合并后立即变红。docs/translation.md与对应的terminology-<locale>.md。pnpm run check:i18n已经接入pnpm lint:ci(.github/workflows/test.yml的Lintjob),失败时 PR 上会有红叉,本地pre-commit也会提前拦截;但真正能拦住合并按钮的,是仓库管理员在main分支的 branch protection 里把Lint设为 required status check——这一步需要 scriptscat/scriptcat 的仓库管理员在 GitHub 仓库设置里配置,贡献者/本 PR 都无法代为完成,只能建议。建议审查重点
src/pkg/utils/monaco-editor/langs.ts的 TypeScript AST 解析逻辑(parseModule/flattenNode):是否正确处理了对象嵌套、同文件顶层常量引用、跨文件import绑定与 shorthand 属性(grantValuePrompts,)。src/assets/_locales目录名解析逻辑(findChromeDirName):是否能正确覆盖未来新增的 locale,而不需要手动维护映射表。.husky/pre-commit只在 json 文件改动时触发是否合适:好处是不会拦住本 PR 自身、或调整 terminology/langs 文件时的提交;代价是本地提交阶段不会拦截"新增 locale 但漏配 terminology 文件/langs 条目"这类问题,这类问题仍要等 CI 才会报出来。验证
pnpm run check:i18n:在当前main上通过,仅有两条提示性 warning(pt-BR尚无_locales目录、pt-BR/tr-TR尚无editorLangs条目),均为历史遗留、非阻塞。pnpm run lint:ci:全部通过(prettier、tsc、check:i18n、eslint)。_locales/ko/messages.json与editorLangs["ko-KR"]均完整。langs.ts拆分为langs/<locale>.ts):零问题通过,已在 #1605 的评论里贴出结果;期间发现脚本最初硬编码单文件路径、对拆分后的目录结构会误报"文件缺失",已在本 PR 中修复(自动探测langs/index.ts,支持跨文件import解析)。langs.ts/langs/<locale>.ts中删除/新增 key,删除messages.json、删除terminology-<locale>.md)均能正确以 exit code 1 报出具体缺失/多余的 key 路径,还原后重新通过;pre-commit钩子在暂存区包含 json 翻译文件的坏改动时也会正确拒绝提交,同时确认了它不会拦截本 PR 自身对.husky/pre-commit、scripts/check-i18n.mjs等非 json 文件的提交。关联