fix(i18n): 回填棘轮最后 17 个缺失语言 key 与两个模板 key 家族,调用点欠账清零(#3546 切片七) - #3885
Conversation
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 一并删掉,理由写在原处。
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
订正一处围栏措辞(正文写在 #3844 落地之前)。 正文末段写"与在飞 #3844 / #3875(es 既有值敬称)零相交"。开 PR 后
Generated by Claude Code |
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
|
✅ 验收(PM,session 实物核验:头 裁定要点:
转 ready 并挂 auto-merge。落 main 后 #3546 达成 DoD(基线双清零,258 key 存量七个切片还清)—— 由 PM 两读法确认合并后执行关单;#3837 同时解锁即派。越界 #3876/#3877/#3878/#3879/#3880 归分诊席;pt 介词缩合(#3877)dev 荐方向 A,已把当前渲染钉成「当前真相」,不阻塞。 Generated by Claude Code |
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()实测:common.retry3 处;common.record/common.editInStudio/detail.add/layout.systemNav.datasources各 2 处missing-prefixgantt.linkEnd.1、organization.invitations.status.2)切片二"预测 90 实为 93"的教训在这里派上了用场:如果按棘轮的 17 行去数调用点,会漏掉 6 处。
守卫读数,前后对照(同一命令):
2320/2320 —— 整个调用点面第一次全解析。解析数 +23(= 23 个调用点)、en key 数 +24,与清单精确相等。棘轮 ASSERT:
棘轮按行删除而非重序列化(切片四量过:重序列化会把 diff 变成全文件重排)。最终 diff
7 insertions(+), 24 deletions(-):删掉 19 个条目行,两个空对象各收成一行,并在note里加四行说明 —— 文件保留而不删除,空是它的终态且承重:对空基线来说任何新增未解析 key 都算unexpected并使构建失败。严重度:全部"英文可见、十语不可译",且英文兜底有三种形态
正文点名的 8 处裸 key 站点是切片一(PR #3583)的;
detail.viewSource(en.ts:994)、wizard.missingRequired、gantt.toolbar.refresh三个 key 从那时起就已在 en 解析 —— 本切片派发说明里"高严重度残余"那一段的前提已过期,实测本切片零裸 key 站点。但英文兜底不是一种形态,而是三种,所以逐字节比对拆成三块:
t(key, { defaultValue: 'English' })。dashboard.loading用useSafeTranslate的位置参数形式tt(key, 'Loading…')—— 只 grepdefaultValue:的普查会把这处判成"没有兜底"并且判错。organization.invitations.status.传{ defaultValue: tab }(枚举成员本身,由元素的 CSScapitalize首字母大写);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 等价关系
多站点 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.—— 取值域是同文件的封闭联合与切片四的
console.ai.group.同类(权威在同一文件),不同于切片五的marketplace.disclosure.runtime.(权威在姐妹仓packages/spec)。targetEnd可为 null,调用点用?? 'start'收敛,所以调用点上的取值域也确实是两个。"key 可达性 vs 值判定"分开断言(切片五写下的判据):
en的gantt.linkEnd子对象键集合 ===['start','end'],十包同集合。第三种端点一旦加入,这条红 —— 正是棘轮里那条 prefix 条目原本的职责,从棘轮搬进测试。en值与useGanttTranslation的兜底表逐字节相同('gantt.linkEnd.start': 'start'),并断言该 hook 仍是 host-first / 逐 key 的形态。这两条一起才说明"包在时 i18next 答、包不在时兜底表答,用户不能分辨跑的是哪条"。organization.invitations.status.—— 取值域是同文件的StatusFilterall只出现在页签,其余四个也是AuthInvitation.status的取值。逐字节比对结构上不可能(默认值是变量),取的等价关系是:两个元素都带 CSScapitalize,所以用户看到的是Pending而不是pending,于是en值必须恰好等于首字母大写后的枚举成员。差一个字母,本切片就改动了用户今天看到的英文。capitalize类名与五成员联合都作为前提断言了;并且断言五个成员都是单词(CSScapitalize会把每个词首字母大写,将来出现两词状态时这个模型不再成立,那条断言就是必须回头看的地方)。packages/auth/src/types.ts:404把status声明成开放string,枚举只写在文档注释里,由此长出statusBadgeVariant的default:兜底与defaultValue: inv.status(未知状态把 wire 原值当界面文案渲染进徽章)。与 #3846 同类。九包实译:逐包邻键取证
home.welcomeAdminDescriptionNoAi逐包点名这个菜单(zh 写「管理」菜单);empty.objectNotFoundDescription用全角引号“{{name}}”;本包——45 处对单—25 处;detail.deleteConfirmation用「此记录」管理、此界面页引用了 “{{name}}”,但该来源不可用。、此应用尚不可用 —— 可能仍在发布中。请稍后重试。、此记录help.onlineDocs=オンラインドキュメント;empty.objectNotFoundDescription用「{{name}}」;organization.settings.deleted=組織を削除しました;kanban.addColumn=カラムを追加(不是table.columns的列)ドキュメント、「{{name}}」を参照していますが、レコードを削除しました、カラムdetail.history=기록(不是 히스토리)、detail.noCommentsYet=아직 댓글이 없습니다;全包 0 处全角冒号;preview.history.items舍量词只留单位词아직 기록이 없습니다、열、Studio에서 편집home.welcomeAdminDescriptionNoAi说Verwaltungsmenü;gantt.readOnlyHint=Die Bearbeitung ist in dieser Ansicht deaktiviert.;gantt.linkType.fs用Anfang(而gantt.column.start是Start);concurrentUpdateDescription的洞在von之后 → 第三格Verwaltung、… ist auf dieser Instanz deaktiviert.、Anfang/Ende、diesem DatensatzInvitation acceptée阴性n'est pas encore disponible、Acceptée/Refusée/Annulée/Toutesempty.*邻域一致 usted(Verifique su configuración);Invitación aceptada阴性Vuelva a intentarlo en un momento.、Aceptada/TodasConvite aceito阳性(与 es/fr 相反)Aceito/Recusado/Cancelado/TodosapprovalsInbox.recallUnavailable把 instance/deployment 译作среда;версию支配第二格;preview.history.items=элементов(第二格复数)публикация ещё идёт、в этой среде、этой записи、колонокالتطبيقات/المستخدمون);connectAgent.disabled.title=MCP معطّل في هذا النشر;gantt.linkType.fs用无冠词نهاية → بدايةالوثائق/الإدارة/مصادر البيانات、في هذا النشر、بداية/نهايةen 同串且邻键译文在此语法可用时复用既有行,6 处,双向钉住(先验证两边 en 真的同字符串,再验证十包同值):
common.done←view.done;common.record←home.recentApps.itemType.record;common.retry←lookup.retry;dashboard.loading←lookup.loading;detail.add←report.editor.fieldPickerAdd;organization.invitations.status.pending←grid.import.jobStatus.pending。四个邀请状态形容词刻意不复用,理由是语法而非口味:
approvals*家族那几行与各包的「请求」一词性数一致,而这里描述的是邀请。最清楚的一条是 ru ——approvalsInbox.statusRejected是阳性Отклонён(запрос),而 приглашение 是中性要Отклонено。照搬会落一个"键名 parity 全绿、反抄袭集合也全绿"的语法错误。fr/es 取阴性(invitation / invitación)、pt 取阳性(convite),都从同命名空间的 toast 里取 —— 那是各包已经对性别做过承诺的地方。all也随同一性别(它量化的名词就是"邀请"):frToutes、esTodas、ptTodos。canceled是组织撤回而非受邀人拒绝,所以 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不是独立标签:ConcurrentUpdateDialog把detail.concurrentUpdateDescription按{{field}}切开,粗体渲染在缺口里,所以包值要迁就洞前的格位 —— de 第三格(von之后,与兄弟键detail.deleteConfirmation的第四格不同)、ru 第二格(версию之后)。合成后的整句逐包断言。de + este必须缩合成deste,而de在外层既有句子里(本切片不改既有值)。已立 #3877,并在测试里把当前渲染(mais recente de este registro)钉成"当前真相"而不是留给下一个读者当疏漏,那条断言就是该单落地时必须一起改的地方。反抄袭断言:精确集合恰 2 元,三条防空转
实测"与 en 逐字节相同"的
lang :: key对:2 / 216。两处都是法语与英语确实同拼的词,且都紧邻
layout.systemNav.configuration—— 该键 fr 在本切片之前就是Configuration。第三个同形一旦出现,这条集合相等会红,必须像切片二(12)、三(18)、四(1)那样在同一行上给理由。集合相等最容易空转(切片二 B1 的教训:包为空时同样绿),所以配三条:
it.each);Verwaltung/ ruАдминистрирование/ deDokumentation/ arالوثائق),且外来词确实在(jaStudio で編集、koStudio에서 편집、deDatenquellen)并仍不与 en 逐字节相同;另加本切片特有的形状断言:插值恰 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)且等于包内值;此界面页引用了 “crm_lead”,但该来源不可用。);设计评审 (结束) → 上线 (开始));反向验证:六处方向跑前先判,六处全中(第 6 处是一次误操作变成的额外方向,如实记下)
方向在跑之前就定了。本切片是"包里没有 → 补上",没有被删的死枝、没有计数类下游门禁、没有 canonical-first 的
??链,所以不存在先例里的"诊断变多"或"反转";第 3、4、5、6 条的预判是守卫绿而别的东西红,那是 #3530 记录的门禁分工,不是漏检。GUARD EXIT=1;六个 3546 家族测试的总数钉全红,含organization-那条刚反转的not.toContain。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 行。zh.ts(en 保留)→ 守卫仍然绿(它只看 en),parity 与本切片测试红,失败文本就是把藏了这些 key 数月的机制摆出来 —— zh 缺 key 时t()返回的是 en 的值,不是 key、不报错:StatusFilter加第六个成员('expired'),不加 key —— 本切片特有的一条,预判为"守卫绿,只有本测试红"。命中,而且这一条是本 PR 最值得记的反向验证:守卫看不见它 —— 模板静态头此刻已能匹配(en 里有这个家族),而动态 key 分支是 report-only,所以一个未枚举的成员一条 finding 也不产生。棘轮里那条 prefix 条目从来也做不到这件事;只有测试里钉住的联合原文能做到。grid.import.jobStatus.pending的 en 值改成小写 —— 这条是误操作变成的方向,如实记下:第一次脚本用count=1替换pending: 'Pending',,而该邻键在 en.ts 里同缩进且行号更小,于是改到了它。结果恰好验证了复用断言是双向钉住的 —— 邻键动了,新键没动,一样红:en值Pending改成小写pending(第 5 条原本要做的事,定位到status: {之后再替换)→ 预判"守卫绿(key 在)、drift 红(en 值变了九包没跟)、本测试的capitalize等价关系红"。三道门禁三种判决,全中:六个方向跑完后工作树恢复干净,六个 3546 家族测试重跑 225 passed (225)。
顺手量出、未在本 PR 修的事
„与 ASCII"配成一对(正确配对的 21 个),德语用户看到的收尾引号是错的;search.resultsCount结尾是连着两个直引号。三道 i18n 门禁按设计都看不见(不读引号 / 只判 key 在不在 / 只在 en 值变化时触发,而这些值从落地起就错)。detail.concurrentUpdateDescription洞前留了裸de,插入名词短语时渲染de este registro(葡语必须缩合成deste);修法在外层句子或调用点,给了两个选项与各自代价。finding) —— en 包 32 个值以 ASCII...结尾、113 个以…结尾,九包逐值跟随;统一到…会触发 drift 门禁的 32 × 10 = 320 处机械改动,需独立 PR + 一条门禁。finding) ——AuthInvitation.status声明成开放string而文档注释只枚举四个成员;与 [finding] marketplaceApi 把插件信任层级 runtime 声明为开放 string,而生产方 spec 的 PluginRuntimeSchema 是三成员封闭枚举 —— 消费侧比契约宽松,PluginDisclosure 的 as any 与开放兜底表都由此而来 #3846 同类的 contract-first 反面。finding) —— "同一 en 字符串必须同一译文"这条落值纪律不能做成门禁:281 组共享 en 值里 164 组至少有一个包刻意分开译(Design in Studio/Clear/Save/Delete/{{count}} records都是正当分歧),硬门禁会产生 164 个假红。顺带点出其中两组像是笔误(esDone的Hecho/Listo、deLoading...的Laden.../Wird geladen...),两者都属改既有值,与在飞 es 语言包的 preview.draftBar 同一条横幅两种敬称 —— messageClean 用 usted、message 用 tú,西语用户按「有无待发布变更」看到不同人称 #3844 / es 的 preview.empty.notReadyDescription 用 tú,与同屏的 preview.draftBar(usted)混用 —— 草稿预览未就绪时一屏两种敬称 #3875 有相交风险故未动。验证
all-locales-key-parity.test.ts含在上面 33 个文件内,已绿(它是补完 en 后立刻要求九包补齐的那道门禁)。scripts/__tests__/check-i18n-call-site-keys.test.ts的applyBaseline断言对空基线同样成立(unexpected与stale都是空数组),不需要改。@object-ui/i18n无工作区内依赖,自身不需要先建依赖;@object-ui/react的TranslationKeys = 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 与missingPrefixes2 → 0 —— 那个计数器每切片动一次且只降;organization-那条toContain反转成not.toContain,console-与marketplace-两条对空集恒真的not.toContain删掉并写明理由)、changeset(@object-ui/i18npatch)。零组件改动。与在飞 #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