Skip to content

fix(i18n): 回填棘轮最后 17 个缺失语言 key 与两个模板 key 家族,调用点欠账清零(#3546 切片七) - #3885

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3546-slice7-final-residue
Aug 9, 2026
Merged

fix(i18n): 回填棘轮最后 17 个缺失语言 key 与两个模板 key 家族,调用点欠账清零(#3546 切片七)#3885
yinlianghui merged 1 commit into
mainfrom
claude/issue-3546-slice7-final-residue

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes-part-of: #3546 — 切片七,终章。本单不写 Fixes:棘轮虽已清零,关单由 PM 核验后执行。

分支自 origin/main@39136cccb69cd33482fe6886e7142def87e4493c 显式 sha 切出(先 git fetch origin main,不用 FETCH_HEAD)。该 sha 含切片六合并点 e64a52ec3,已用 git merge-base --is-ancestor 实测确认。

分母:脚本实测,不手数

node scripts/check-i18n-call-site-keys.mjs(#3530 的守卫)的 analyze() 实测:

实测
不重复缺失 key 17
对应调用点 23 —— 本系列第一次两数明显不等
多站点 key 5:common.retry 3 处;common.record / common.editInStudio / detail.add / layout.systemNav.datasources 各 2 处
属本切片的 missing-prefix 2 个家族 / 3 个调用点(gantt.linkEnd. 1、organization.invitations.status. 2)
棘轮内条目 19(17 key + 2 prefix),与实测清单双向精确相等,无差集
en 新增 key 路径 24(17 叶子 + 2 + 5 家族成员)
--- multi-site keys ---
common.record                    AppContent.tsx:417:71, AppContent.tsx:446:73
common.retry                     AppContent.tsx:573:16, InvitationsPage.tsx:135:12, MembersPage.tsx:119:12
common.editInStudio              PageView.tsx:108:22, PageView.tsx:109:27
detail.add                       RelatedList.tsx:1130:31, RelatedList.tsx:1300:31
layout.systemNav.datasources     AppSidebar.tsx:354:39, UnifiedSidebar.tsx:352:41
--- ratchet vs measured ---
ratchet keys: 17   measured: 17
in ratchet not measured: []
measured not in ratchet: []
ratchet prefixes: ["gantt.linkEnd.","organization.invitations.status."]
measured prefixes: ["gantt.linkEnd.","organization.invitations.status."]
prefix sets equal: true

切片二"预测 90 实为 93"的教训在这里派上了用场:如果按棘轮的 17 行去数调用点,会漏掉 6 处。

守卫读数,前后对照(同一命令):

前: 2372 pack-backed (2297/2320 literal keys resolve) … en pack (2894 keys)
后: 2372 pack-backed (2320/2320 literal keys resolve) … en pack (2918 keys)
Every in-scope call-site key resolves against the en pack (2918 keys).   GUARD EXIT=0

2320/2320 —— 整个调用点面第一次全解析。解析数 +23(= 23 个调用点)、en key 数 +24,与清单精确相等。棘轮 ASSERT:

before: 17 after: 0 removed: 17
ASSERT removed===17 -> true
ASSERT missingKeys empty -> true {}
ASSERT missingPrefixes 2->0 -> true ["gantt.linkEnd.","organization.invitations.status."] => []
ASSERT file still valid JSON -> true (parsed)
remaining by namespace: {}

棘轮按行删除而非重序列化(切片四量过:重序列化会把 diff 变成全文件重排)。最终 diff 7 insertions(+), 24 deletions(-):删掉 19 个条目行,两个空对象各收成一行,并在 note 里加四行说明 —— 文件保留而不删除,空是它的终态且承重:对空基线来说任何新增未解析 key 都算 unexpected 并使构建失败。

严重度:全部"英文可见、十语不可译",且英文兜底有三种形态

正文点名的 8 处裸 key 站点是切片一(PR #3583)的;detail.viewSource(en.ts:994)、wizard.missingRequiredgantt.toolbar.refresh 三个 key 从那时起就已在 en 解析 —— 本切片派发说明里"高严重度残余"那一段的前提已过期,实测本切片零裸 key 站点。

但英文兜底不是一种形态,而是三种,所以逐字节比对拆成三块:

  1. 22 站点 / 16 keyt(key, { defaultValue: 'English' })
  2. 1 站点 dashboard.loadinguseSafeTranslate位置参数形式 tt(key, 'Loading…') —— 只 grep defaultValue: 的普查会把这处判成"没有兜底"并且判错。
  3. 两个家族的默认值根本不是字面量:organization.invitations.status.{ defaultValue: tab }(枚举成员本身,由元素的 CSS capitalize 首字母大写);gantt.linkEnd. 不传默认值,靠 useGanttTranslation逐 key 兜底表(表里两个成员都有)。

zh 会话下用户实际看到的英文:app 还在发布时落地的 App not available 空态(整句说明 + Retry 按钮)、界面页"来源不可用"、系统导航的 Administration 分组头与 Datasources / Documentation 两项、工作区对话框"本实例已禁用创建新组织"、邀请列表五个状态标签(筛选页签 + 每行徽章)、甘特依赖拖拽提示里指出落点端的 start / end、记录详情的 Add / Record deleted / No history yet 与并发更新对话框的 "this record"、看板空板的列计数、仪表板部件的读屏 Loading…、页面编辑器 "Edit in studio" 的 title 与 aria-label。

en ↔ 内联英文默认值:16 逐字节 + 1 位置参数 + 7 等价关系

slice call sites: 23  (literal defaultValue: 22, positional fallback: 1, none: 0)
en value === inline defaultValue: 16/16   (per key; 5 keys have 2-3 sites, all spelling the same default)

多站点 key 的每一处拼写都断言了(PageView 同一个按钮的 title + aria-label 两处、RelatedList 两个入口、AppContent 两处 —— 计数各钉 2),否则一个 key 两种默认值会让"逐字节相同"在一处成立、在另一处不成立而无人察觉。

第 2 形态(dashboard.loading)取的等价关系相同 —— 包值 === 该站点原本会渲染的 fallback —— 并额外断言 useSafeTranslate 确实是 t(keyOrKeys, fallback) 且逐 key 判定。顺带说明:common.loading 是更老的 'Loading...'(三个 ASCII 句点)拼写,是不同的字符串,所以刻意不复用(字形分歧另立 #3878)。

第 3 形态的两组等价关系见下。

两个前缀家族:封闭枚举,不通配

gantt.linkEnd. —— 取值域是同文件的封闭联合

packages/plugin-gantt/src/GanttView.tsx:1369/1373   sourceEnd: 'start' | 'end';  targetEnd: 'start' | 'end' | null;
packages/plugin-gantt/src/GanttView.tsx:4577        const endLabel = (e: 'start' | 'end') => t(`gantt.linkEnd.${e}`);

与切片四的 console.ai.group. 同类(权威在同一文件),不同于切片五的 marketplace.disclosure.runtime.(权威在姐妹仓 packages/spec)。targetEnd 可为 null,调用点用 ?? 'start' 收敛,所以调用点上的取值域也确实是两个。

"key 可达性 vs 值判定"分开断言(切片五写下的判据):

  • key 面:engantt.linkEnd 子对象键集合 === ['start','end'],十包同集合。第三种端点一旦加入,这条红 —— 正是棘轮里那条 prefix 条目原本的职责,从棘轮搬进测试。
  • 值面:两个 en 值与 useGanttTranslation 的兜底表逐字节相同('gantt.linkEnd.start': 'start'),并断言该 hook 仍是 host-first / 逐 key 的形态。这两条一起才说明"包在时 i18next 答、包不在时兜底表答,用户不能分辨跑的是哪条"。

organization.invitations.status. —— 取值域是同文件的 StatusFilter

packages/app-shell/src/console/organizations/manage/InvitationsPage.tsx:30
  type StatusFilter = 'all' | 'pending' | 'accepted' | 'rejected' | 'canceled';
:161  t(`organization.invitations.status.${tab}`, { defaultValue: tab })          ← 筛选页签
:206  t(`organization.invitations.status.${inv.status}`, { defaultValue: inv.status })  ← 每行徽章

all 只出现在页签,其余四个也是 AuthInvitation.status 的取值。逐字节比对结构上不可能(默认值是变量),取的等价关系是:两个元素都带 CSS capitalize,所以用户看到的是 Pending 而不是 pending,于是 en 值必须恰好等于首字母大写后的枚举成员。差一个字母,本切片就改动了用户今天看到的英文。capitalize 类名与五成员联合都作为前提断言了;并且断言五个成员都是单词(CSS capitalize 会把每个词首字母大写,将来出现两词状态时这个模型不再成立,那条断言就是必须回头看的地方)。

⚠️ 该家族的宽松处没有在本 PR 修,已立 #3879:packages/auth/src/types.ts:404status 声明成开放 string,枚举只写在文档注释里,由此长出 statusBadgeVariantdefault: 兜底与 defaultValue: inv.status(未知状态把 wire 原值当界面文案渲染进徽章)。与 #3846 同类。

九包实译:逐包邻键取证

邻键证据(实测,非印象) 本切片落值举例
zh home.welcomeAdminDescriptionNoAi 逐包点名这个菜单(zh 写「管理」菜单);empty.objectNotFoundDescription 用全角引号 “{{name}}”;本包 —— 45 处对单 25 处;detail.deleteConfirmation 用「此记录」 管理此界面页引用了 “{{name}}”,但该来源不可用。此应用尚不可用 —— 可能仍在发布中。请稍后重试。此记录
ja help.onlineDocs = オンラインドキュメント;empty.objectNotFoundDescription「{{name}}」;organization.settings.deleted = 組織を削除しました;kanban.addColumn = カラムを追加(不是 table.columns) ドキュメント「{{name}}」を参照していますがレコードを削除しましたカラム
ko detail.history = 기록(不是 히스토리)、detail.noCommentsYet = 아직 댓글이 없습니다;全包 0 处全角冒号;preview.history.items 舍量词只留单位词 아직 기록이 없습니다Studio에서 편집
de home.welcomeAdminDescriptionNoAiVerwaltungsmenü;gantt.readOnlyHint = Die Bearbeitung ist in dieser Ansicht deaktiviert.;gantt.linkType.fsAnfang(而 gantt.column.startStart);concurrentUpdateDescription 的洞在 von 之后 → 第三格 Verwaltung… ist auf dieser Instanz deaktiviert.Anfang / Endediesem Datensatz
fr 直角撇号 U+0027(420 : 18);Invitation acceptée 阴性 n'est pas encore disponibleAcceptée / Refusée / Annulée / Toutes
es 全包 usted 占优,empty.* 邻域一致 usted(Verifique su configuración);Invitación aceptada 阴性 Vuelva a intentarlo en un momento.Aceptada / Todas
pt Convite aceito 阳性(与 es/fr 相反) Aceito / Recusado / Cancelado / Todos
ru ё 163 处;approvalsInbox.recallUnavailable 把 instance/deployment 译作 среда;версию 支配第二格;preview.history.items = элементов(第二格复数) публикация ещё идётв этой средеэтой записиколонок
ar 导航类复数标签带定冠词(التطبيقات / المستخدمون);connectAgent.disabled.title = MCP معطّل في هذا النشر;gantt.linkType.fs 用无冠词 نهاية → بداية الوثائق / الإدارة / مصادر البياناتفي هذا النشربداية / نهاية

en 同串且邻键译文在此语法可用时复用既有行,6 处,双向钉住(先验证两边 en 真的同字符串,再验证十包同值):common.doneview.done;common.recordhome.recentApps.itemType.record;common.retrylookup.retry;dashboard.loadinglookup.loading;detail.addreport.editor.fieldPickerAdd;organization.invitations.status.pendinggrid.import.jobStatus.pending

四个邀请状态形容词刻意不复用,理由是语法而非口味:approvals* 家族那几行与各包的「请求」一词性数一致,而这里描述的是邀请。最清楚的一条是 ru —— approvalsInbox.statusRejected 是阳性 Отклонён(запрос),而 приглашение 是中性要 Отклонено。照搬会落一个"键名 parity 全绿、反抄袭集合也全绿"的语法错误。fr/es 取阴性(invitation / invitación)、pt 取阳性(convite),都从同命名空间的 toast 里取 —— 那是各包已经对性别做过承诺的地方。all 也随同一性别(它量化的名词就是"邀请"):fr Toutes、es Todas、pt Todoscanceled组织撤回而非受邀人拒绝,所以 de 取本包 cancel toast 的 zurückgezogen 而不是泛用的 Abgebrochen

按概念找邻键(切片五的盲区 / 切片六的新规)本切片有六处,每处的前提都与落值并排断言:

  • Administration —— 全切片最强的一条:兄弟键 home.welcomeAdminDescriptionNoAi 在一整句里逐包点名了这个菜单(zh「管理」菜单 / de Verwaltungsmenü / ru «Администрирование» 菜单)。
  • Datasources —— 单数概念早已译好(report.editor.objectName),而兄弟导航项都是复数,故取复数。
  • Documentation —— help.allDocs / help.onlineDocs 已有。
  • Record deleted —— 结构孪生 organization.settings.deleted(「名词 + deleted」成功 toast)。
  • No history yet —— detail.noCommentsYet 的"No X yet"家族,加各包自己的 History 词。
  • kanban.columns —— 裸单位词,取 preview.history.items 的形状(组件拼数字与空格,包只供单位词),词取 kanban 自己的列词。en 只有复数形也是安全的:空态只在 boardColumns.length > 1 时渲染,计数永不为 1,所以不需要复数族(这一条前提也断言了,守卫那个 > 1 一旦改动,断言先红)。

detail.concurrentUpdateRecordLabel 不是独立标签:ConcurrentUpdateDialogdetail.concurrentUpdateDescription{{field}} 切开,粗体渲染在缺口里,所以包值要迁就洞前的格位 —— de 第三格(von 之后,与兄弟键 detail.deleteConfirmation 的第四格不同)、ru 第二格(версию 之后)。合成后的整句逐包断言。

⚠️ pt 是唯一一个合成后不地道且叶子值无法救的包:葡语 de + este 必须缩合成 deste,而 de 在外层既有句子里(本切片不改既有值)。已立 #3877,并在测试里把当前渲染(mais recente de este registro)钉成"当前真相"而不是留给下一个读者当疏漏,那条断言就是该单落地时必须一起改的地方。

反抄袭断言:精确集合恰 2 元,三条防空转

实测"与 en 逐字节相同"的 lang :: key 对:2 / 216

fr :: layout.systemNav.administration     ("Administration")
fr :: layout.systemNav.documentation      ("Documentation")

两处都是法语与英语确实同拼的词,且都紧邻 layout.systemNav.configuration —— 该键 fr 在本切片之前就是 Configuration。第三个同形一旦出现,这条集合相等会红,必须像切片二(12)、三(18)、四(1)那样在同一行上给理由。

集合相等最容易空转(切片二 B1 的教训:包为空时同样绿),所以配三条:

  1. presence:十包 × 24 路径 = 240 个值全部是非空字符串(独立一条 it.each);
  2. 反面对照:空/小集合也可能因为"译者绕开了所有像英文的词"而成立。这几条证明那两处不是复制粘贴 —— 能译的包都译了(de Verwaltung / ru Администрирование / de Dokumentation / ar الوثائق),且外来词确实在(ja Studio で編集、ko Studio에서 편집、de Datenquellen)并仍不与 en 逐字节相同;
  3. 每包一条用户可见面的具名钉子(见下 provider 抽样)。

另加本切片特有的形状断言:插值恰 1 个路径带洞(empty.interfacePageSourceMissing{{name}},其余 23 个十包一律不许有洞 —— 与切片六的 11 个正相反);em dash 十包为 、zh 为该包自己的 ——;引号风格随同族empty.objectNotFoundDescription(zh 全角、ja 角括号、其余 ASCII);fr 直角撇号全量 24 路径;ru ё;ar 的"RTL 句首不落拉丁 token"全量 24 路径且插值后仍成立。

de 的引号是一处刻意分歧:同族既有值把德语开引号 与 ASCII 直引号 " 配成一对(全包实测 20 处这样、21 处正确配对),那是笔误而不是约定,所以本切片写正确配对的 „{{name}}“,并把错配单独立单 #3876 而不是照抄。测试里两条断言并排,一条钉邻键的错配现状、一条钉本切片的正确配对。

provider-mounted 抽样钉扎(discrimination check)

十六个组件文件的 t 绑定逐个断言(不是假设):九个是裸 useObjectTranslation();RecordDetailView@object-ui/react 取(带 language);plugin-detail 三个走 useDetailTranslation;kanban 走自己的 createSafeTranslation(探针键 kanban.noCards 在包里,所以挂 provider 时 i18next 胜出,且其 defaults 表没有 kanban.columns —— 那是无 provider 方向的 #3865);dashboard 走 useSafeTranslate;gantt 走逐 key 的 useGanttTranslation

  • en / zh 各解析 9 个代表 key(每个 owner 一个),断言 not.toBe(key) 且等于包内值;
  • zh 具名八条(含插值:此界面页引用了 “crm_lead”,但该来源不可用。);
  • 两个家族按真实模板调用形态在 en/zh 两路把全部 7 个成员各渲染一遍,并按 GanttView 的拼法合成拖拽提示(设计评审 (结束) → 上线 (开始));
  • 另八包各钉一条用户可见面,横跨四种书写系统;
  • ar 的"RTL 句首不落拉丁 token"全量 24 路径(且插值后仍不以拉丁开头);fr 直角撇号全量 24 路径。

反向验证:六处方向跑前先判,六处全中(第 6 处是一次误操作变成的额外方向,如实记下)

方向在跑之前就定了。本切片是"包里没有 → 补上",没有被删的死枝、没有计数类下游门禁、没有 canonical-first 的 ?? 链,所以不存在先例里的"诊断变多"或"反转";第 3、4、5、6 条的预判是守卫绿而别的东西红,那是 #3530 记录的门禁分工,不是漏检。

  1. 只把棘轮 19 行放回(包保留)→ 守卫报 19 条 stale,GUARD EXIT=1;六个 3546 家族测试的总数钉全红,含 organization- 那条刚反转的 not.toContain
    19 baseline entries are stale — the defect is gone, so the entry must go too (this file is a ratchet; it only shrinks):
      missingKeys: common.done …(17 条)
      missingPrefixes: gantt.linkEnd.
      missingPrefixes: organization.invitations.status.
    GUARD EXIT=1
    
    AssertionError: expected 17 to be +0        ×4(auth / console / marketplace-preview / perm-home)
    AssertionError: expected [ 'gantt.linkEnd.', …(1) ] to not include 'organization.invitations.status.'
      ← organization 那条 not.toContain 排在 toBe(0) 之前,所以它先红
    AssertionError: expected [ 'common.done', …(16) ] to deeply equal []
    Test Files  6 failed (6) | Tests  6 failed | 219 passed (225)
    
  2. 只回退 en.ts(棘轮保持已删)→ 守卫报 26 条 unexpected / 19 distinct,GUARD EXIT=1,解析数回落 2320 → 2297,en key 数 2918 → 2894;parity 报九包各有 24 个 en 没有的 key。26 = 23 个 missing-key 站点 + 3 个 missing-prefix 站点;19 distinct 正是棘轮那 19 行。
    2372 pack-backed (2297/2320 literal keys resolve)
    26 call sites reference a key the en pack does not define (19 distinct):
      …InvitationsPage.tsx:161:14  [missing-prefix]  organization.invitations.status.
      …GanttView.tsx:4577:60  [missing-prefix]  gantt.linkEnd.
      …KanbanImpl.tsx:673:52  [missing-key]  kanban.columns
    GUARD EXIT=1
    
    AssertionError: zh has 24 key(s) absent from en …(九包各一条)
    Test Files  2 failed (2)
    
  3. 只回退 zh.ts(en 保留)→ 守卫仍然绿(它只看 en),parity 与本切片测试红,失败文本就是把藏了这些 key 数月的机制摆出来 —— zh 缺 key 时 t() 返回的是 en 的值,不是 key、不报错:
    守卫: Every in-scope call-site key resolves against the en pack (2918 keys).  GUARD EXIT=0
    AssertionError: zh is missing 24 key(s)
    AssertionError: expected 'Edit in studio' to be '在 Studio 中编辑'        ← 这就是那个机制
    AssertionError: zh gantt.linkEnd.start: expected 'start' to be undefined
    AssertionError: zh common.done diverged from view.done: expected undefined to be '完成'
    Test Files  2 failed (2) | Tests  12 failed | 51 passed (63)
    
  4. StatusFilter 加第六个成员('expired'),不加 key —— 本切片特有的一条,预判为"守卫绿,只有本测试红"。命中,而且这一条是本 PR 最值得记的反向验证:守卫看不见它 —— 模板静态头此刻已能匹配(en 里有这个家族),而动态 key 分支是 report-only,所以一个未枚举的成员一条 finding 也不产生。棘轮里那条 prefix 条目从来也做不到这件事;只有测试里钉住的联合原文能做到。
    守卫: Every in-scope call-site key resolves against the en pack (2918 keys).  GUARD EXIT=0
    × the key surface is exactly StatusFilter
    AssertionError: StatusFilter moved: expected '/**\n * InvitationsPage …' to contain "type StatusFilter = 'all' | 'pendi…"
    Test Files  1 failed (1) | Tests  1 failed | 42 passed (43)
    
  5. 只把邻键 grid.import.jobStatus.pending 的 en 值改成小写 —— 这条是误操作变成的方向,如实记下:第一次脚本用 count=1 替换 pending: 'Pending',,而该邻键在 en.ts 里同缩进且行号更小,于是改到了它。结果恰好验证了复用断言是双向钉住的 —— 邻键动了,新键没动,一样红:
    守卫 GUARD EXIT=0    drift RED(en 值变了九包没跟)
    AssertionError: en organization.invitations.status.pending vs grid.import.jobStatus.pending:
                    expected 'Pending' to be 'pending'
    Test Files  1 failed (1) | Tests  1 failed | 42 passed (43)
    
  6. 把本家族自己的 enPending 改成小写 pending(第 5 条原本要做的事,定位到 status: { 之后再替换)→ 预判"守卫绿(key 在)、drift 红(en 值变了九包没跟)、本测试的 capitalize 等价关系红"。三道门禁三种判决,全中:
    守卫 GUARD EXIT=0
    drift: en now: "pending"   unchanged in: zh, ja, ko, de, fr, es, pt, ru, ar   DRIFT EXIT=1
    × en renders exactly what the CSS-capitalised wire value rendered
    AssertionError: en status.pending: expected 'pending' to be 'Pending'
    Test Files  1 failed (1) | Tests  2 failed | 41 passed (43)
    

六个方向跑完后工作树恢复干净,六个 3546 家族测试重跑 225 passed (225)

顺手量出、未在本 PR 修的事

  1. de 语言包 20 个值把德语开引号 „ 和 ASCII 直引号 " 配成一对(正确配对的只有 21 个),德语用户看到的收尾引号是错的 #3876(新立,concrete) —— de 包 20 个值把 与 ASCII " 配成一对(正确配对的 21 个),德语用户看到的收尾引号是错的;search.resultsCount 结尾是连着两个直引号。三道 i18n 门禁按设计都看不见(不读引号 / 只判 key 在不在 / 只在 en 值变化时触发,而这些值从落地起就错)。
  2. pt 的 detail.concurrentUpdateDescription 在洞前留了裸 "de",插入名词短语时渲染出 "de este registro"(葡语必须缩合成 deste)—— 叶子值无论怎么写都不可能对 #3877(新立,concrete) —— pt 的 detail.concurrentUpdateDescription 洞前留了裸 de,插入名词短语时渲染 de este registro(葡语必须缩合成 deste);修法在外层句子或调用点,给了两个选项与各自代价。
  3. [finding] en 包 32 个值用 ASCII "..." 结尾、113 个用 U+2026 "…" —— 同一屏上省略号两种字形,且九包各自跟着抄 #3878(新立,finding) —— en 包 32 个值以 ASCII ... 结尾、113 个以 结尾,九包逐值跟随;统一到 会触发 drift 门禁的 32 × 10 = 320 处机械改动,需独立 PR + 一条门禁。
  4. [finding] AuthInvitation.status 声明成开放 string,而它自己的文档注释只枚举四个成员 —— 未知状态会把 wire 原值当界面文案渲染进邀请徽章 #3879(新立,finding) —— AuthInvitation.status 声明成开放 string 而文档注释只枚举四个成员;与 [finding] marketplaceApi 把插件信任层级 runtime 声明为开放 string,而生产方 spec 的 PluginRuntimeSchema 是三成员封闭枚举 —— 消费侧比契约宽松,PluginDisclosure 的 as any 与开放兜底表都由此而来 #3846 同类的 contract-first 反面。
  5. [finding] 「同一 en 字符串必须同一译文」不能做成门禁:281 组共享 en 值里 164 组至少有一个包刻意分开译 —— 记下分母,并点出其中两组像是笔误 #3880(新立,finding) —— "同一 en 字符串必须同一译文"这条落值纪律不能做成门禁:281 组共享 en 值里 164 组至少有一个包刻意分开译(Design in Studio / Clear / Save / Delete / {{count}} records 都是正当分歧),硬门禁会产生 164 个假红。顺带点出其中两组像是笔误(es DoneHecho/Listo、de Loading...Laden.../Wird geladen...),两者都属改既有值,与在飞 es 语言包的 preview.draftBar 同一条横幅两种敬称 —— messageClean 用 usted、message 用 tú,西语用户按「有无待发布变更」看到不同人称 #3844 / es 的 preview.empty.notReadyDescription 用 tú,与同屏的 preview.draftBar(usted)混用 —— 草稿预览未就绪时一屏两种敬称 #3875 有相交风险故未动。

验证

node scripts/check-i18n-call-site-keys.mjs
  → 2372 pack-backed (2320/2320 literal keys resolve)
    Every in-scope call-site key resolves against the en pack (2918 keys).      EXIT=0
node scripts/check-i18n-en-drift.mjs
  → 0 en value(s) changed (24 key(s) added, 0 removed) — No en value changed in this range.   EXIT=0
node scripts/check-control-bytes.mjs
  → OK (scanned 3777 tracked text file(s); skipped 85 binary)                   EXIT=0
node scripts/check-changeset-presence.mjs
  → 15 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)   EXIT=0
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]' 全部 18 个改动/新增文件                 → 零命中(gate 之外的自扫)
git log --format=%B origin/main..HEAD | grep -ci fable                          → 0
  (另查 co-authored-by / claude-session / opus / sonnet / haiku                  → 0)

flock -w 7200 /tmp/os-heavy-verify.lock -c 'NODE_OPTIONS=--max-old-space-size=4096 \
  pnpm exec vitest run packages/i18n \
  scripts/__tests__/check-i18n-call-site-keys.test.ts \
  scripts/__tests__/check-i18n-en-drift.test.ts --maxWorkers=2'
  → Test Files 33 passed (33) | Tests 628 passed (628)      (585 → 628,新增 43 条)

# 消费半径扫尾:本切片零组件改动,但这些包是语言包的消费方(按规则的消费半径扫,
# 不按被编辑的包扫 —— 本切片跨 app-shell / plugin-detail / plugin-dashboard /
# plugin-kanban / plugin-gantt 五个包的调用点)
flock … 'pnpm exec vitest run packages/plugin-kanban packages/plugin-dashboard \
  packages/plugin-gantt packages/app-shell/src/layout \
  packages/app-shell/src/console/organizations packages/app-shell/src/preview --maxWorkers=2'
  → Test Files 100 passed (100) | Tests 763 passed (763)

pnpm --workspace-concurrency=2 --filter @object-ui/i18n type-check   → tsc --noEmit 通过
pnpm exec turbo run type-check --concurrency=2                       → 78 successful, 78 total
                                                                       (CI 同命令,末次编辑后全仓)
npx eslint 本 PR 改动的 16 个 i18n 文件                              → 0 problems

all-locales-key-parity.test.ts 含在上面 33 个文件内,已绿(它是补完 en 后立刻要求九包补齐的那道门禁)。scripts/__tests__/check-i18n-call-site-keys.test.tsapplyBaseline 断言对基线同样成立(unexpectedstale 都是空数组),不需要改。@object-ui/i18n 无工作区内依赖,自身不需要先建依赖;@object-ui/reactTranslationKeys = typeof en 只是转出口,加 key 不窄化任何消费方 —— 全仓 turbo type-check 已实跑覆盖。

测试命令按 AGENTS.md §9 的唯一正确写法(仓根 pnpm exec vitest run 加相对仓根的路径),不用 pnpm --filter 包名 test;--workspace-concurrency 一律在 --filter 之前。

围栏

18 文件:packages/i18n/src/locales/*.ts(十包,仅新增本切片 24 个路径)、scripts/i18n-call-site-key-baseline.json(删 19 条 + 两个空对象收行 + note 加四行)、packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx(新增,43 条)、auth- + console- + marketplace-preview- + organization- + perm-home-namespace-3546.test.tsx(棘轮总数 17 → 0 与 missingPrefixes 2 → 0 —— 那个计数器每切片动一次且只降;organization- 那条 toContain 反转成 not.toContain,console-marketplace- 两条对空集恒真的 not.toContain 删掉并写明理由)、changeset(@object-ui/i18n patch)。零组件改动

与在飞 #3844 / #3875(es 既有值敬称)零相交:本切片只新增 key,不改任何既有语言包值,es 新增行也不在 preview.draftBar 邻近。与 #3848(packages/core)、#3837(AiChatPage 与 console-namespace-3546.test.tsx 里的 convZh 字节钉 —— 实测本 PR 该文件 diff 里 convZh 命中 0)零相交。未动 content/docs/releases/**


Generated by Claude Code

Fixes-part-of: #3546

守卫 scripts/check-i18n-call-site-keys.mjs 实测:17 个不重复 key / 23 个调用点
(五个 key 多站点:common.retry 3 处,common.record、common.editInStudio、
detail.add、layout.systemNav.datasources 各 2 处),外加两个整族缺失前缀的 3 个
调用点。九个命名空间散落在 app-shell / plugin-detail / plugin-dashboard /
plugin-kanban / plugin-gantt —— 都不够大到单独成片的长尾。

棘轮 17 → 0,missingPrefixes 2 → 0;守卫读数从 2297/2320 变成 2320/2320,
en key 数 2894 → 2918(+24 = 17 叶子 + 2 + 5 家族成员)。基线文件保留而不删除:
空是它的终态且承重 —— 对空基线来说任何新增未解析 key 都算 unexpected 并使构建失败。

英文一字未改,分三种形态各自取等价关系:16 个 key 与内联
t(key, { defaultValue: '…' }) 逐字节相同;dashboard.loading 的英文兜底是
useSafeTranslate 的第二个位置参数 tt(key, 'Loading…')(只 grep defaultValue:
的普查会误判这处没有兜底);gantt.linkEnd.* 与 useGanttTranslation 的逐 key 兜底
表逐字节相同;五个邀请状态标签等于元素 CSS capitalize 后的 wire 原值。

两个家族按封闭枚举回填而不是补通配前缀,棘轮里 missingPrefixes 那条的职责搬进
测试:gantt.linkEnd. 取 GanttView 自己的 'start' | 'end'(linkDrag 状态类型),
organization.invitations.status. 取 InvitationsPage 的 StatusFilter
(all/pending/accepted/rejected/canceled)。

九包译文逐包取邻键证据。en 同串且邻键译文在此语法可用时复用既有行(view.done /
home.recentApps.itemType.record / lookup.retry / lookup.loading /
report.editor.fieldPickerAdd / grid.import.jobStatus.pending 六处);四个邀请
状态形容词刻意不复用 approvals 家族 —— 那些与各包的「请求」一词性数一致
(ru 阳性 Отклонён),而邀请需要自己的一致(ru 中性 Отклонено)。
按概念找邻键:layout.systemNav.administration 取自
home.welcomeAdminDescriptionNoAi(该句逐包点名这个菜单),kanban.columns 取
preview.history.items 的裸单位词形状加 kanban 自己的列词(ja カラム 而非 列)。

detail.concurrentUpdateRecordLabel 会被嵌进 concurrentUpdateDescription 的洞里,
所以按洞前的格位选值:de 第三格(von 之后)、ru 第二格(версию 之后)。

零组件改动。五个既有 3546 家族测试的棘轮总数钉子与 missingPrefixes 钉子同步同降
一次(17 → 0、2 → 0);其中两处对空集恒真的 not.toContain 一并删掉,理由写在原处。
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 8, 2026 11:50pm

Request Review

@github-actions github-actions Bot added the tests label Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

订正一处围栏措辞(正文写在 #3844 落地之前)。

正文末段写"与在飞 #3844 / #3875(es 既有值敬称)零相交"。开 PR 后 main 又前进三个提交,其中 #3844 已合入(b750823f0),所以准确说法是:

#3875(es preview.empty.notReadyDescription 的 tú)仍在飞,同样是既有值,本切片不碰。


Generated by Claude Code

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-CjQhjOao.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.66KB 3.13KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 482.39KB 106.34KB
core (index.js) 2.96KB 1.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 139.61KB 35.99KB
fields (index.js) 230.82KB 56.70KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.53KB 10.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.98KB 12.37KB
plugin-charts (index.js) 61.04KB 17.31KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 117.21KB 30.27KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 236.17KB 58.82KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 112.10KB 27.10KB
plugin-gantt (index.js) 162.55KB 39.57KB
plugin-grid (index.js) 187.63KB 49.66KB
plugin-kanban (index.js) 48.30KB 13.28KB
plugin-list (index.js) 105.12KB 25.48KB
plugin-map (index.js) 16.81KB 5.24KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 25.76KB 7.33KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 2.71KB 1.34KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

✅ 验收(PM,session session_01GTRjn8xBqp75dk7kFupVRt)—— #3546 切片七(终章)

实物核验:头 fa1f75251,18 文件 +1273/−76;棘轮终态实读 —— missingKeys {} / missingPrefixes {} 双空,且注记把「空是终态、任何新欠账对空基线即红」写成文件自述,基线文件保留的裁定采纳;十包 + en 各 28 行、六个 3546 家族测试总数钉同降一次(两条对空集恒真的 not.toContain 删除、一条反转,理由在原处);trailer 0。
CI 终态(独立复核):20 检查全部 completed,18 success + 2 skipped,零失败。

裁定要点:

  • 分母纪律再次救场:17 key 实为 23 调用点(五个多站点 key),按棘轮行数数会漏 6 处。
  • 前缀族按封闭枚举回填(gantt linkDrag 的 'start'|'end'、InvitationsPage 的 StatusFilter 五成员),prefix 职责搬进测试 —— R4 反向验证(加第六个成员守卫仍绿、唯测试红)证明这不是仪式。
  • 派发前提订正采纳:「高严重度三处无 defaultValue」早在切片一已解决,本切片零裸 key 站点 —— 如实报告而非硬凑证据。
  • 四个邀请状态形容词刻意不复用 approvals 家族(ru 语法性:приглашение 中性要 Отклонено)—— parity 与反抄袭集合都看不见的语法错误被人工判掉,九包取证质量高。
  • 反向验证六方向全中(含误操作意外变成的 R5,恰好验证复用断言双向钉住;R6 三道门禁三种判决)。
  • es 语言包的 preview.draftBar 同一条横幅两种敬称 —— messageClean 用 usted、message 用 tú,西语用户按「有无待发布变更」看到不同人称 #3844 的 es.ts 同文件零行相交有 merge-tree 实证,PR 正文措辞已自行订正。

转 ready 并挂 auto-merge。落 main 后 #3546 达成 DoD(基线双清零,258 key 存量七个切片还清)—— 由 PM 两读法确认合并后执行关单;#3837 同时解锁即派。越界 #3876/#3877/#3878/#3879/#3880 归分诊席;pt 介词缩合(#3877)dev 荐方向 A,已把当前渲染钉成「当前真相」,不阻塞。


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants