feat(scripts): en 文案改动必须由九个译文包同批跟改的门禁 (#3650) - #3659
Merged
Conversation
新增 scripts/check-i18n-en-drift.mjs:以 merge-base 为基线比对十个语言包的 值,en 某 key 的值变了而其余九包该 key 的值没变即失败并逐 key 点名。这是 #3582/#3625 一族缺陷缺的那道不变量 —— 现有三道门禁全部只读键集合,对「值 的语义漂移」天生失明,而 #3625 证明连「值是英文」「非拉丁包里有 ASCII」这 类判据也看不见它(八包存的是地道译文,只是译了一句 en 已废弃的话)。 挂账文件 scripts/i18n-en-drift-baseline.json 落地为空,条目须逐字转写新的 en 文案 —— 因此只能由已经做出该改动的人写,只豁免那一句,且下次该 key 再 变时自动过期。数量方向由测试里的 WAIVER_CEILING 钉住。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 7, 2026 17:54
This was referenced Aug 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3650
新增
scripts/check-i18n-en-drift.mjs:以git merge-base HEAD origin/main为基线比对十个语言包的值,en某 key 的值变了而其余九包该 key 的值没变即失败并逐 key 点名。挂账文件scripts/i18n-en-drift-baseline.json落地为空。⛔ 未碰语言包本体、
scripts/check-doc-links.mjs及其测试(在途 #3622)、既有守卫的任何行为(只加了两处头注互认注释)。为什么是「事件」而不是「状态」
#3625才是定设计的那一案。八个包存的是地道译文(ja 是日文、ru 是西里尔、ar 是阿拉伯文),互不相同、文字系统正确、键集合完整、占位符一致 —— 关于那个值本身的每一条机械判据都是绿的,而且会一直绿。没有门禁能判断一句日文是否还等于英文现在的意思,那是在要一个译者。能判的是事件:这个 PR 把
en的这个 key 改了,九个译文没跟。这条不变量只在改en的那个 PR 里可判 —— 那时作者还知道这句话该说什么;晚一天就是考古。需要斟酌的三点(实测后裁,取舍如下)
1. en 纯排版不豁免
Save→Save.与整句重写同等对待。理由按顺序:先量后裁:把门禁回放到
en.ts进入本仓以来的全部 21 个 commit,只有 1 个 commit 改过en的值(0e50440e8,3 个 key),它的 27 个包/key 对全部跟改了 —— 绿,且正确。也就是说在这段历史上,「不豁免」的可观测代价是 0 个 commit:根本没有纯排版的抖动需要被豁免。2. 粒度:key 级
不比语义、不按命名空间、不按文件。两条推论都是有意的:
en新增/删除 key 不归本门禁,那是键集合的事,all-locales-key-parity.test.ts双向都会红。在这里再报一遍等于一个缺陷两处报、两套说辞。console.ai.*出站消息 key 就是有意只存在于 en/zh;要求另外八包「跟改」一个它们不该定义的 key,就是本门禁与管键集合的那道门禁直接打架。3. 与 all-locales-key-parity 的分工写进两者头注(互相指认)
三道门禁各自的盲区已写进
check-i18n-call-site-keys.mjs与all-locales-key-parity.test.ts的头注:all-locales-key-parity.test.tscheck-i18n-call-site-keys.mjs(#3530)en里存不存在check-i18n-en-drift.mjs(本单)en值变了,九包跟没跟parity 头注里同时写明:它在 #3582/#3625 上全绿是正确的,不该试图让它变红 —— 那是在要一个键集合测试去判含义。
落地即绿 —— 为什么不是空绿(正文第 5 点的诚实答复)
正文第 5 点要求「先全量扫一遍确认没有第三案存量」。这条对本门禁不适用,原因要写清楚:
本门禁判的是 diff 里的事件,不是树里的状态。它没有「存量」可扫 —— 「今天 ja 那句话是否还等于今天 en 那句话」正是那个没有门禁能回答的语义问题,而假装能回答正是 #3625 得以全绿的原因。所以挂账文件落地为空,不是「债还没算」,是这道门禁在构造上就没有债可记。
能做的那部分我做了:把门禁回放到历史上。
为什么全绿 —— 这是本 PR 最该被读到的一条实测:两案的肇事 commit 根本不在本仓历史里。十个语言包是在
30ac2e1ee一次性整体进仓的(11 files, +30270),而就在那个 commit 上:两案都是带着漂移进的仓 —— 改
en的那次动作发生在这些文件被搬进来之前。这恰恰是这一族缺陷只能靠人肉逐 key 读邻行才被发现的原因,也说明本门禁不可能被历史验证。因此验收语料是植入式的,不是观察来的:
scripts/__tests__/check-i18n-en-drift.test.ts在一次性 git 仓里重建 #3625 那个本应存在的 commit(真实的十句译文、真实的八个包),断言门禁点名view.readonlyTooltip与全部八个包。这一条,加上下面的退出码验证,才是「落地即绿是一次测量而不是空比较」的证据。挂账机制:为什么它不会退化成 allowlist
条目形如:
逐字转写新 en 文案是整个机制,不是手续:
en现值核对(与 diff 无关),所以该 key 下次再改时条目自动失配 → 红,直到有人删掉或按新句子续签。挂账自己会过期,不会沉淀成 allowlist。有意不做的一条:「本次没命中任何 finding 的挂账」不算错。挂账 PR 合入后,之后每一次运行看到的都是空 diff、都是这个形状;若判红,等于把一个 PR 的逃生口变成后面所有 PR 的红灯。数量方向改由测试里的
WAIVER_CEILING钉住 —— 加一条挂账就得同时抬那个数字,评审看得见。逆向验证(先写预测,后跑;六条)
预测在跑之前就写死在这里,其中第 6 条预测的是「不红」,如实记。
en改值,九包不动view.readonlyTooltip/unchanged in: zh, ja, ko, de, fr, es, pt, ru, ar9 pack value(s) followed1 finding(s) waived by the ledgeren里不存在的 keystale-waiverview.noSuchKey: this key no longer exists in the en packstale-waiver,且不豁免原 findingen再改一次the en text this waiver covers is gone. Ledger says "…read-only.", en now says "…. Read-only."(a) 的真实输出:
第 6 条:棘轮 —— 派发口径那一句需要拆成三种形状
派发口径写的是「临时增一条无对应 en 变更的挂账 → 棘轮测试红」。结论成立,但抓住它的机制随形状而不同,照单一句写会把读者引偏,所以拆开如实记:
en里stale-waiver)en现值不符stale-waiver)en现值、本次 diff 无漂移d3 是惰性挂账:脚本判绿是设计,不是漏 —— 挂账 PR 合入后每一次运行都是这个形状。而它永远不可能豁免未来的改动:该 key 下次一改,转写就与
en现值不符 →stale-waiver→ 红(即第 e 条)。在静态时抓住它的是测试里的数量天花板,不是脚本。实测(把一条 d3 形状的挂账临时塞进真挂账文件):
只有那一条红,方向与预测一致。植入项已还原(
waivers: {})。实现取舍
解析方式:TypeScript AST(不是 import,也不是正则)
「改动前」那一侧是 git 里的 blob,不是磁盘上的文件,十个包又是 1.5 MB TypeScript。用
git show REF:PATH拿到文本,TS AST 转成key到string的映射:不需要构建、不需要 loader、不需要临时文件 —— 与check-i18n-call-site-keys.mjs的collectEnKeys同一招、同一理由。正则在多行字符串/转义/嵌套花括号上不可靠,不考虑。解析器读不懂的形态一律 throw,不跳过 —— 悄悄丢掉一棵子树会让门禁在它不再读的那些 key 上恰好全绿。这条第一次运行就兑现了:它当场炸在
objectActions.resetPackageSetConfirm,那句 en 是'…' + '…'跨行拼接(全仓仅此一处)。i18next 服务的是拼接后的字符串,所以比较的也必须是拼接后的字符串;已加折叠并钉了测试。(隔壁collectEnKeys把这个节点当作不透明叶子,对它是对的 —— 它只要 key。)CI 挂接:ci.yml 的 type-check 作业(与 #3547 同款)
#3547 的
check:i18n-keys就挂在这个作业里,本门禁紧挨其后 —— 复用同一次 install(它 importtypescript),不需要构建。changeset-guard/control-bytes/check-doc-links全是在 HEAD 上全树扫描),所以按派发口径「没有先例就 fetch」办:给该作业的 checkout 加fetch-depth: 0。actions/checkout 默认深度 1,git merge-base在浅克隆里什么也找不到 —— 而门禁把「基线解析不出来」判为硬失败而不是跳过(diff 门禁找不到 diff 却报绿,正是这一族门禁存在的理由),所以这一行掉了是响亮的红,不是静悄悄的过。测试钉住了这一行。ci.yml的paths-ignore是**/*.md、content/**、docs/**、apps/site/**、.changeset/**—— 没有一条能匹配packages/i18n/src/locales/*.ts或挂账文件,所以改 en 文案的 PR 一定会启动这个 workflow。测试把这条也钉住了:任何新增的paths-ignore条目只要触及packages/scripts/locales就红。基线解析顺序
--base→OS_I18N_DRIFT_BASE→merge-basewithorigin/$GITHUB_BASE_REF→origin/main→main。用 merge-base 而不是origin/main顶端:一个分支只该为它自己改的en负责,不该为它分叉之后落到 main 上的改动负责。--root/--head/--min-keys三个开关都有注释说明用途:--root指向别的 checkout(worktree,或测试建的一次性仓),--min-keys是「防空比较」下限(默认 2000,CI 从不传;测试里有一条故意不传、驱动 CLI 去证明这道下限还在响)。测试
新文件
scripts/__tests__/check-i18n-en-drift.test.ts,32 条。18 个文件里包含
ci-cd-pipeline-doc.test.ts(证明新增 CI 步骤没有破坏那页的钉子)、check-i18n-call-site-keys.test.ts(头注改动安全)、scripts-type-check.test.ts。测试自身的两处防空绿:
en模块,而且逐值比对而不只是逐键 —— 只比键的话,一个把文本读错的解析器会去比两个错字符串,漂移可以照样漏掉。--min-keys跑一次 CLI,让「包小得不可能是真包」这条守卫真的被执行过,而不是一行没人碰的代码。无 changeset
纯 CI 门禁改动,不改任何 npm 包的运行时行为、非用户可见特性 —— 与 PR #3589(
check-doc-links扫描面扩张)同款判断。未触碰content/docs/releases/。顺带发现,未在本 PR 修改(越界)
#3653(observation-class,
finding标签,未指派):content/docs/guide/ci-cd-pipeline.md:69的type-check作业行漏了pnpm check:i18n-keys(#3547 落的那一步),而ci-cd-pipeline-doc.test.ts的判定粒度是「作业」不是「步骤」 —— 它双向钉住 job 表与每个 workflow 的小节,但没有任何一条读「一个作业里跑了哪些步骤」,所以往既有作业里加run:步骤而不改文档,全套钉子照绿。本 PR 会让这一栏再少报一步(check:i18n-drift),有意未改 —— 派发口径的文件面不含content/docs/,且要把这行改对必须连带补上 #3547 欠的那一步(不是本单的账)。修 #3653 时两步一起补即可。范围
scripts/check-i18n-en-drift.mjs、scripts/i18n-en-drift-baseline.json(空)、scripts/__tests__/check-i18n-en-drift.test.ts.github/workflows/ci.yml(fetch-depth: 0+ 一个步骤)、package.json(一条 script)、check-i18n-call-site-keys.mjs与all-locales-key-parity.test.ts的头注分工互认(纯注释)scripts/check-doc-links.mjs及其测试、既有守卫的任何行为、content/docs/、content/docs/releases/🤖 Generated with Claude Code
https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt