在 #3546 切片五(回填 marketplace + preview 命名空间)顺手量出,不在该 PR 范围内 。观察级:传进去的参数是休眠 的,按钮上的 Update 本身是个正常标签,今天没有任何东西坏掉。
现场
packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx:555(云端安装分支的主按钮):
label: installing
? t('marketplace.action.installing')
: cloudInstalledVersion
? (cloudUpdateAvailable
? t('marketplace.action.updateTo', { defaultValue: 'Update', version: latestVersion })
: t('marketplace.action.installed', { defaultValue: 'Installed' }))
: t('marketplace.action.installToCloud'),
version: latestVersion 被传进去,但 defaultValue 是 Update —— 没有 {{version}} 的洞。i18next 对没有对应洞的插值参数静默丢弃 ,所以这个参数从来没有渲染过任何东西。
对照:同一个文件 的 :852(环境下拉里每个环境后面的版本提示)确实渲染版本:
t('marketplace.install.updateTo', { defaultValue: 'Update → v{{version}}', version: latestVersion })
两个 key 名字都叫 updateTo、都传 version、都在同一个包详情页上,只有一个有洞。作者意图看着是"主按钮也该显示要升到哪个版本",但那只是推测。
全仓规模:1/1055,就是这一处
对全仓所有 t('key', { … }) 形态的调用点(key 在 en 能解析的,共 1055 个)做 AST 抽取,取出顶层参数名(剔除 i18next 保留名 defaultValue/count/context/ns/lng/… 与嵌套调用的参数),与 en 值里的 {{hole}} 名字集合比对:
checked 1055 pack-backed t(key, {…}) call sites whose key resolves in en
call sites passing an option with no matching {{hole}}: 1
distinct keys: 1
packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx:555
marketplace.action.updateTo inert=[version]
en = "Update"
这个类是单例 。第一遍扫描曾报 2 处,第二处 packages/fields/src/widgets/ImageField.tsx:105 是误报 —— 它的外层 t('fields.image.enlarge', { name: … }) 的参数对象里嵌了另一个 t() 调用 (t('fields.image.imageAlt', { index: idx + 1 })),我的正则把内层的 index: 当成了外层参数名。剔除嵌套括号跨度后复测为 1。记在这里是因为任何将来实现这道门禁的人会踩同一个坑:参数名提取必须先消掉嵌套调用。
#3546 切片五做了什么(以及为什么不修)
切片五把 marketplace.action.updateTo 补进十个语言包。硬纪律是 en 值与调用点内联 defaultValue 逐字节相同 (36/36),所以 en 落的是 Update,九包落各自的"更新 / Aktualisieren / Mettre à jour / …"(复用 form.update 既有译文行)。
没有 顺手给 en 加 {{version}},两个理由:
那会改变用户今天看到的字串 (Update → Update → v2.1.0),而这条纪律的整个意义就是回填不改变任何英文渲染;
该改不改是产品裁量 —— 主按钮要不要带版本号是设计问题,不是 i18n 问题。按钮宽度、与 Installed / Installing… 三态的视觉一致性都在里面。
切片五的 PR 里有一条断言把这个选择钉住 ,免得下一个读者以为是漏了:
expect ( src ) . toContain ( "t('marketplace.action.updateTo', { defaultValue: 'Update', version: latestVersion })" ) ;
for ( const lang of LANGS ) expect ( at ( builtInLocales [ lang ] , 'marketplace.action.updateTo' ) ) . not . toContain ( '{{' ) ;
expect ( at ( builtInLocales . en , 'marketplace.install.updateTo' ) ) . toContain ( '{{version}}' ) ;
也就是说:要么删掉那个休眠参数,要么给十包都加上洞 —— 无论哪条,这条断言都会红,迫使做决定的人正面处理。
三道 i18n 门禁为什么看不见
反向的错(值里有洞、调用点不传 参数 → 用户直接看到 {{name}} 花括号)同样没人管。本次扫描顺带确认了这一支目前为 0,但那是运气,不是保证。
收口方向(未裁决)
A. 删掉 :555 的 version 参数 。零风险,承认按钮就叫 Update。
B. 给十包加 {{version}} ,与 :852 对齐成 Update → v{{version}}。是产品改动,需要设计确认三态按钮宽度。
C. 加一道门禁 (建议,与 [finding] 8 处 auth 调用点的内联 defaultValue 与 en 包值不一致 —— key 存在故兜底是死代码,三道 i18n 门禁按设计都看不见这一类 #3810 选项 B 同一个位置):t(key, opts) 的非保留参数名集合必须与 en 值的 {{hole}} 名集合相等 ,双向都判 —— 传了没洞的参数红(本单),值里有洞没人传也红(声明即强制)。[finding] 没有任何守卫断言组件 t() 引用的 key 存在于 en 包 —— parity 测试只管包际一致,调用点→包的一致性是盲区 #3530 的守卫已经把调用点解析成 AST 了,增量接近零;存量按上面的实测是 1 处 + 0 处,不需要基线文件 ,可以直接落成硬门禁。
倾向 C 落门禁 + A 清存量 (A 而非 B,因为 B 是产品决定,不该由一道门禁的存量清理顺手带上)。若维护者认为主按钮该显示版本,那就是 B,并且门禁照样成立。
关联:#3546 (切片五量出本条并钉住选择)、#3810 (同一位置的另一道门禁提案:内联 defaultValue 必须等于 en 值;这两条判据应当同批实现)、#3530 (守卫本体,已有现成 AST)、#3650 (en-drift 门禁)。
在 #3546 切片五(回填
marketplace+preview命名空间)顺手量出,不在该 PR 范围内。观察级:传进去的参数是休眠的,按钮上的Update本身是个正常标签,今天没有任何东西坏掉。现场
packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx:555(云端安装分支的主按钮):version: latestVersion被传进去,但defaultValue是Update—— 没有{{version}}的洞。i18next 对没有对应洞的插值参数静默丢弃,所以这个参数从来没有渲染过任何东西。对照:同一个文件的
:852(环境下拉里每个环境后面的版本提示)确实渲染版本:两个 key 名字都叫
updateTo、都传version、都在同一个包详情页上,只有一个有洞。作者意图看着是"主按钮也该显示要升到哪个版本",但那只是推测。全仓规模:1/1055,就是这一处
对全仓所有
t('key', { … })形态的调用点(key 在en能解析的,共 1055 个)做 AST 抽取,取出顶层参数名(剔除 i18next 保留名defaultValue/count/context/ns/lng/… 与嵌套调用的参数),与en值里的{{hole}}名字集合比对:这个类是单例。第一遍扫描曾报 2 处,第二处
packages/fields/src/widgets/ImageField.tsx:105是误报 —— 它的外层t('fields.image.enlarge', { name: … })的参数对象里嵌了另一个t()调用(t('fields.image.imageAlt', { index: idx + 1 })),我的正则把内层的index:当成了外层参数名。剔除嵌套括号跨度后复测为 1。记在这里是因为任何将来实现这道门禁的人会踩同一个坑:参数名提取必须先消掉嵌套调用。#3546 切片五做了什么(以及为什么不修)
切片五把
marketplace.action.updateTo补进十个语言包。硬纪律是en值与调用点内联defaultValue逐字节相同(36/36),所以en落的是Update,九包落各自的"更新 / Aktualisieren / Mettre à jour / …"(复用form.update既有译文行)。没有顺手给
en加{{version}},两个理由:Update→Update → v2.1.0),而这条纪律的整个意义就是回填不改变任何英文渲染;Installed/Installing…三态的视觉一致性都在里面。切片五的 PR 里有一条断言把这个选择钉住,免得下一个读者以为是漏了:
也就是说:要么删掉那个休眠参数,要么给十包都加上洞 —— 无论哪条,这条断言都会红,迫使做决定的人正面处理。
三道 i18n 门禁为什么看不见
check-i18n-call-site-keys.mjs([finding] 没有任何守卫断言组件 t() 引用的 key 存在于 en 包 —— parity 测试只管包际一致,调用点→包的一致性是盲区 #3530):只问 key 在en存不存在。all-locales-key-parity.test.ts:比包与包的占位符形状(九包必须和 en 一样),从不比"调用点传的参数"和"值里的洞"。check-i18n-en-drift.mjs(i18n 门禁:en 文案变更时其余九包必须同批跟改(或显式挂账)——#3582/#3625 族缺陷缺的那道不变量 #3650):只在 en 值变化时动作。反向的错(值里有洞、调用点不传参数 → 用户直接看到
{{name}}花括号)同样没人管。本次扫描顺带确认了这一支目前为 0,但那是运气,不是保证。收口方向(未裁决)
:555的version参数。零风险,承认按钮就叫Update。{{version}},与:852对齐成Update → v{{version}}。是产品改动,需要设计确认三态按钮宽度。t(key, opts)的非保留参数名集合必须与en值的{{hole}}名集合相等,双向都判 —— 传了没洞的参数红(本单),值里有洞没人传也红(声明即强制)。[finding] 没有任何守卫断言组件 t() 引用的 key 存在于 en 包 —— parity 测试只管包际一致,调用点→包的一致性是盲区 #3530 的守卫已经把调用点解析成 AST 了,增量接近零;存量按上面的实测是 1 处 + 0 处,不需要基线文件,可以直接落成硬门禁。倾向 C 落门禁 + A 清存量(A 而非 B,因为 B 是产品决定,不该由一道门禁的存量清理顺手带上)。若维护者认为主按钮该显示版本,那就是 B,并且门禁照样成立。
关联:#3546(切片五量出本条并钉住选择)、#3810(同一位置的另一道门禁提案:内联 defaultValue 必须等于 en 值;这两条判据应当同批实现)、#3530(守卫本体,已有现成 AST)、#3650(en-drift 门禁)。