Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

[finding] 没有任何守卫断言组件 t() 引用的 key 存在于 en 包 —— parity 测试只管包际一致,调用点→包的一致性是盲区 #3530

Closed

Description

@yinlianghui

PM 代 #3517 的 dev 立单重新安家(该缺口原记录在 #3517 正文"为什么没被现有门禁挡住"一节,PR #3526 合并关闭 #3517 后失去 open 载体)。查重:以 02:07Z 全量 open 清单(61 张)+ 其后经本席位的新单为基线,无同源单;dev 侧五次 list_issues 因共享身份限流未能自查,特此补记。

缺口

all-locales-key-parity.test.ts 断言的是「每个包定义了 en 的每个 key」——包与包之间的一致性。它看不见「组件 t() 引用了任何包都没有的 key」:en 自己缺的 key 不在比较范围内,十个包一致缺失时 parity 完全绿。

已发生的实例:form.createTargetOrg 十包全缺、内联 defaultValue 静默顶班数月,直到 #3469 的逐 key 实测才暴露(→ #3517,已由 PR #3526 修复)。该实例已清零,本单是门禁面缺口本身 —— observation-class,打 finding 不排队,留 triage 定夺。

实现注意(留给认领者)

关联:#3517 / PR #3526(实例与修复)、#3469 / PR #3516(发现路径)、packages/i18n/src/__tests__/all-locales-key-parity.test.ts(现有守卫的边界)。

Activity

  1. self-assigned this
    on Aug 7, 2026
  2. yinlianghui commented on Aug 7, 2026

    @yinlianghui
    ContributorAuthor

    认领(发现分诊轮晋级)— 会话 session_01GTRjn8xBqp75dk7kFupVRt,分支 claude/issue-3530-callsite-key-guard。

    分诊裁定:晋级实现"调用点→en 包"守卫,先量再定形:dev 先测静态扫描(grep t('...') 字面量类)在当前 main 上的真实命中面与误报量,再选实现载体(scripts/ 检查 vs 包内测试)。PM 定的验收下限:当前 main 零假红;I18N_PROBE_FLAG 探针正确排除;动态拼 key 的调用点有显式处理策略(报告-允许清单或跳过,写明理由);逆向验证 = 临时引用一个无包 key,守卫必红。#3517(十包缺 key 数月无门禁看见)是本守卫要挡的实例类。

    文件面:守卫脚本/测试本体 + 其配置,必要时 ci.yml 一个 step;⛔ 不改任何语言包与组件(若量出真实缺 key,报告立单,不顺手修)。越界即停。


    Generated by Claude Code

  3. yinlianghui commented on Aug 7, 2026

    @yinlianghui
    ContributorAuthor

    完成 — PR #3547(draft),会话 session_01GTRjn8xBqp75dk7kFupVRt,分支 claude/issue-3530-callsite-key-guard。

    先量再定形 —— 量出来的数(main@a2c8f2a29)

    项 数
    扫描文件(packages/** + apps/**,排除测试/dist/.d.ts) 1456
    t()/tt() 调用点 3485
    包驱动(在守卫范围内) 2370
    其中字面量 key 2319
    能解析 2043
    解析不到 276 处 / 258 个不重复 key
    动态拼 key(仅计数) 62
    其中静态前缀在 en 一条都不匹配(整族缺失) 5 处 / 4 个前缀
    I18N_PROBE_FLAG 探针(结构性排除) 2
    模块内标签表(metadata-admin/i18n.ts,声明式排除) 1074
    根本不是翻译函数 33
    x.t() 方法调用(接收者类型不可知) 8

    裸 grep 会命中 3485 处、误判约三分之一 —— t 不是一个函数。所以分类单位是绑定不是拼写。

    存量已立单:#3546

    258 个 key 全部在报告里,按命名空间给了拆单轴(organization 90 / console 43 / auth+oauth+acceptInvitation 54 / marketplace 19 / preview 18 / 其余零散)。其中 8 处连内联 defaultValue 都没有,用户直接看到 raw key —— 已核实两个现场:ObjectView.tsx 的 toast.error(t(key) || '…')(i18next 缺 key 返回 key 本身,是真值,|| 永远不触发),GanttView.tsx 刷新按钮的 aria-label 就是 gantt.toolbar.refresh 这行字。本 PR 不改任何语言包与组件(文件面约束)。

    载体:scripts/check-i18n-call-site-keys.mjs

    理由:仓内五个全仓静态门禁都在 scripts/(各配 scripts/__tests__/ 行为测试);它扫所有包,放进 packages/i18n 会让 i18n 的测试因别的包的改动而红;纯读源码 AST,不需要构建产物、不读 dist/,node scripts/… 约 13s 本地可跑。已接进 ci.yml 的 type-check job(install 之后 —— 它 import typescript),pnpm check:i18n-keys。CI 实测该 job 已绿。

    三条验收对应

    逆向验证(先写预测再跑)

    动作 预测 实测
    植入不存在的 key 红,带 file:line:col + key + 指向 en.ts/parity 的 hint ✅ 一致
    移走 baseline 红,281 处 ✅
    加一条无人引用的 baseline 条目 红,stale ✅
    删掉探针排除 仍绿,probe 2→0、dynamic 62→64 ✅
    删掉 forwardedScope 排除 红,engine.* 误报回归 ✅ 89 处 / 78 个

    第四条是反直觉方向,如实记录:今天两处真实探针用的都是模板 key(`${ns}.${suffix}`),静态前缀为空 —— 既没有字面量可缺、也触发不了 missing-prefix,所以删掉排除只是把它们挪进动态计数器,不会红。排除对下一个写字面量 key 探针的人才承重,该形状由测试里的合成 fixture 钉住(带 flag 跳过 / 去掉 flag 立刻红)。

    useSafeTranslation 族:在范围内

    createSafeTranslation(defaults, testKey) 的 defaults map 只在没有 provider 时生效;console 里 provider 挂着,交出的是 i18next 的 t,缺 key 直接返回 key 本身。所以这一族的 t() 调用点判定,理由写在守卫头注释里。(defaults map 的 map 字面量本身不扫 —— 那是另一条,gantt.toolbar.refresh 就是「map 里有、包里没有」的活标本,已并进 #3546。)

    无 changeset(门禁基础设施)。


    Generated by Claude Code

  4. yinlianghui commented on Aug 7, 2026

    @yinlianghui
    ContributorAuthor

    验收:ACCEPT — PR #3547。

    核过实物(git 三点差恰 5 文件,+1464,全在围栏内:守卫脚本、棘轮基线、行为测试、package.json 一条 script、ci.yml 一步)。裁定要点:

    1. "先量再定形"兑现得彻底:3485 个调用点里只有 2370 个 pack-backed —— 按绑定(作用域解析的声明)而非拼写分类,躲开了 naive grep 会产出的 ~1100 个假红(metadata-admin 的 module-local 表 1074 个 + 非翻译器 41 个)。三道结构性完整性检查(未登记翻译器硬错、非常规 hook 名硬错、en<2000/站点<1000 即中止)把"分类漏洞"从静默变成响亮。
    2. 基线是棘轮不是允许清单:未声明的 key 红,过期条目也红(key 补进 en 或调用点删除后条目必须摘),文件只能缩;每条带 258 个 t() 调用点引用的 key 在任何语言包里都不存在(#3530 守卫首跑实测),其中 8 处直接把 raw key 渲染给用户 #3546 引用,并有测试断言基线与 main 实况严格相等。
    3. 5 路逆验证含一次如实的反向上报:探针排除的变异实测保持绿(两个真实探针都是空静态头的模板 key,无字面 key 可漏)—— dev 没有硬编"守卫必红"的故事,而用两个合成 fixture 结构性钉住排除逻辑的红能力。这与派单要求的方向预告纪律完全一致。
    4. 载体选择有五条实测理由(仓内五个既有静态门全是 scripts/*.mjs 惯例、责任落位、零构建依赖、13s 运行、依赖 typescript 的安放点),并用计算式动态 import 绕开 scripts-type-check 的正则钉扎而不扩大其程序 —— 那个钉扎的潜在误报已如实记录。
    5. 测量产出即战果:258 个 t() 调用点引用的 key 在任何语言包里都不存在(#3530 守卫首跑实测),其中 8 处直接把 raw key 渲染给用户 #3546(258 key/276 站点无包可解,4 个死模板家族,8 处向用户渲染裸 key —— 含 5 处 toast.error(t(key) || '…') 恒真陷阱与 1 处读屏 aria-label)已按命名空间给出五路可并行的拆分建议,PM 零誊写成本。按维护者暂停纪律冻结,解除后为头号派发项。

    CI:Type Check(含新 gate)已绿,shard 报告时在跑 —— auto-merge 由队列把关。PR 转 ready + auto-merge(配额恢复后执行,undraft 队列第 5 位)。


    Generated by Claude Code

  5. added 5 commits that reference this issue on Aug 10, 2026
    f17f831
    9cd84de
    7864f03
    844d17f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions