feat(ci): assert every t() call-site key exists in the en locale pack - #3547
Merged
Conversation
`all-locales-key-parity.test.ts` compares packs to EACH OTHER, so ten packs identically missing a key is full parity — and full parity is green. Nothing asserted the other direction: that a key a component actually calls `t()` with exists anywhere at all. objectui#3517 lived in that blind spot for months (`form.createTargetOrg`, missing from all ten packs, covered by an inline `defaultValue`), and the new gate's first full run over `main` found 258 more keys in the same place. The gate classifies by BINDING, not by spelling, because `t` is not one function here: of 3485 `t()`/`tt()` call sites, 2370 reach i18next, 1074 reach the module-local `engine.*` table in `metadata-admin/i18n.ts`, and 41 are not translators at all. Only the first group is judged; the other two are skipped by declaration and by shape, with the counts printed on every run so the unchecked surface stays visible. The 258 keys `main` still owes ship as a ratcheted baseline (objectui#3546): an undeclared key fails, and so does a declared one whose defect is gone, so the file can only shrink. Fixes #3530
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Spreading a 2.6k-entry Set once per candidate head is the same shape that made `all-locales-key-parity` quadratic (7.51s -> 25ms once hoisted). Behaviour is unchanged: same 0 findings, same counters.
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 #3530
缺口
packages/i18n/src/__tests__/all-locales-key-parity.test.ts断言的是「每个包定义了en的每个 key」——包与包之间。它按构造看不见另一个方向:组件t()引用了一个任何包都没有的 key。十个包一致地缺同一个 key 就是完全一致,完全一致就是绿。运行期也不会漏出来:
fallbackLng: 'en'配上内联t(key, { defaultValue: 'English' }),这一处调用点照常渲染正确英文,而这条字符串在十种语言里都翻不了。唯一的信号是i18n.ts里那个 dev-only 的 missing-key 警告,而 CI 从不看浏览器控制台。#3517(form.createTargetOrg)就是一个实例,藏了数月;本守卫首跑在main上又量出 258 个。先量再定形:为什么答案不是「所有
t(」裸 grep
t('...')在本仓命中 3485 处,其中约三分之一判错——因为t不是一个函数:use*Translation/use*Translate/use*T钩子(经 i18next 到语言包)import { t }自packages/app-shell/src/views/metadata-admin/i18n.ts(模块内engine.*标签表,不是 i18next 包),含它传给下游组件的那一跳useCallback结果、Date差值、`链、别的对象的.t()`)所以分类单位是绑定,不是拼写。守卫对每个
t(...)/tt(...)解析该位置作用域内t的声明(最近作用域优先,同作用域取使用点之前最后一次声明),再判族。实测过:把forwardedScope那条排除删掉,metadata-admin树里立刻多出 89 处(78 个不重复 key)误报,无一是真问题。两类失败
missing-key—— 字面量 key 在en里没有叶子。i18next 复数后缀(_one/_other…)算定义了基 key;带returnObjects: true的 key 允许指向子树。missing-prefix—— 模板 key(t(`marketplace.category.${c}`))的静态前缀在en里一条都不匹配。那么插值取任何值都是缺的。这是对动态 key 唯一能在静态期成立的判断,它多捞出 4 个整族缺失(console.ai.group.、gantt.linkEnd.、marketplace.disclosure.runtime.、organization.invitations.status.)。动态 key 的显式策略
不是字面量的 key 静态解析不了。这些调用点不判定、不报红,但计数,并且每次运行都打印出来,让不可分析面是可见的而不是被静默吸收(62 处)。
I18N_PROBE_FLAG探针与被跳过的绑定族同样处理。探针排除是结构性的
useObjectLabel的约定 key 探针本来就该 miss——它回落到服务端给的 label。守卫按[I18N_PROBE_FLAG]这个标记排除,不按路径,所以写在哪里的探针都排除得掉,而useObjectLabel.ts里非探针的调用照样判。逆向验证的方向在这里是反直觉的,如实记录:把
isProbe那段提前返回删掉,守卫仍然绿,只是probeSites2 → 0、dynamicKeySites62 → 64。因为今天两处真实探针用的都是模板 key(`${ns}.${suffix}`),静态前缀为空,既没有字面量可缺、也触发不了missing-prefix。排除对下一个写字面量 key 探针的人才是承重的,所以这个形状由测试里的合成 fixture 钉住:带 flag 的字面量探针被跳过,同一处去掉 flag 立刻报missing-key。载体选择:
scripts/*.mjs,不是包内测试check-doc-links/check-spec-symbol-derivation/check-type-check-coverage/check-lint-coverage/check-control-bytes五个全仓静态门禁都在scripts/,每个配一份scripts/__tests__/行为测试。packages/i18n会让 i18n 的测试因为 plugin-grid 的改动而红——责任放错地方。node scripts/…约 13s 直接可跑,本地和 CI 一样。dist/,所以不会有构建产物带来的假红。en的 key 集是解析 AST 得到的(所以不需要构建),这买来了第二个真相源;测试里第一个describe就是防漂移的钉子:解析出的集合必须与 vitest 真正求值的模块逐 key 相等(2641 = 2641)。存量:棘轮式 baseline,不是允许清单
scripts/i18n-call-site-key-baseline.json记下 gate 落地时main已欠的 258 个 key + 4 个前缀,每条带 issue 号(#3546)。它只能变小:不在其中的 key 报红,而条目对应的缺陷已经修好却还留着,同样报红。修的方式是把 key 补进packages/i18n/src/locales/en.ts—— 补进去之后all-locales-key-parity.test.ts立刻要求另外九包补齐,这正是正确的顺序。⛔ 加defaultValue不是修复,那正是让这 258 个藏了几个月的机制。本 PR 不改任何语言包与组件(#3530 的文件面约束)。存量已立 #3546 跟踪,其中 8 处连内联兜底都没有、用户直接看到 raw key(如
ObjectView.tsx的toast.error(t(key) || '…')—— i18next 缺 key 返回 key 本身,是真值,||永远不触发;GanttView.tsx刷新按钮的aria-label就是gantt.toolbar.refresh这行字)。逆向验证(先写预测,再跑)
close-label.tsx植入t('common.closeButtonLabelZzz')missing-key,带 file:line:col + key + 指向en.ts与 parity 测试的 hintforwardedScopeengine.*/perm.cel.*误报回归跑过的验证
一处顺带的发现,记在这里:
scripts/__tests__/scripts-type-check.test.ts那条「scripts 项目不 import workspace 包」的钉子按正则找from '@object-ui/…',分不清代码、字符串和注释。本测试的 fixture 源码里必须出现这个 specifier(那是被分析的文本),所以改成插值写法,并在注释里说明——注释本身也得避开该模式,否则同样命中。没有去改那条钉子(不在本单文件面内)。无 changeset:门禁基础设施,非用户可见变更。
🤖 Generated with Claude Code
https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
Generated by Claude Code