Skip to content

fix(i18n): 回填 marketplace + preview 两命名空间 37 个缺失语言 key 及 marketplace.disclosure.runtime. 前缀家族,十包补齐 (#3546 切片五) - #3847

Merged
yinlianghui merged 3 commits into
mainfrom
claude/issue-3546-slice5-marketplace-preview
Aug 8, 2026
Merged

fix(i18n): 回填 marketplace + preview 两命名空间 37 个缺失语言 key 及 marketplace.disclosure.runtime. 前缀家族,十包补齐 (#3546 切片五)#3847
yinlianghui merged 3 commits into
mainfrom
claude/issue-3546-slice5-marketplace-preview

Conversation

@yinlianghui

@yinlianghui yinlianghui commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Fixes-part-of #3546 — 切片五。本单不关闭 #3546:回填后仍余 31 个缺失 key + 2 个整族缺失前缀。

基线 origin/main@d3e738af8(merge-base;main 其后前进一个提交 #3841,与本围栏零文件相交,已实测)。

修订记录(三个提交,后两个是自查改正,细节在评论里)

  • e8a738d4e 回填本体。
  • e01dff775 ja disclosure.grantsIntro 尾冒号改半角 —— 该包的约定是有条件的(谓语结尾用半角 10:2、名词标签用全角 4:1),不是本文档初版所写的"既有分歧"。下表 ja 行已按此更正。
  • c3e30870c preview.history.items 九包改用 common.itemCount 的既有单位词,不再自造括号复数记法 —— 并暴露了本切片取证方法的一个盲区(逐字节搜邻键看不见"同一概念、不同英文写法"的邻键)。下表 pt 行已按此更正。

en 值三次提交一字未改,所以下面的分母、36/36、棘轮、前缀家族与三方向反向验证全部不受影响

分母:脚本实测,不手数

node scripts/check-i18n-call-site-keys.mjs(#3530 的守卫)的 analyze() 实测本切片两个命名空间:

实测
不重复缺失 key 37(marketplace 19 + preview 18)
对应调用点 37 —— 本切片罕见地 1:1,无一 key 多站点
属本切片的 missing-prefix 1(marketplace.disclosure.runtime.,棘轮三个前缀家族里唯一属这两个命名空间的)
棘轮内本切片条目 38(37 key + 1 前缀,与实测清单双向精确相等,无差集)
en 新增 key 40(37 实测 + 前缀家族枚举出的 3)

多站点 key 数 = 0。切片二"预测 90 实为 93"的教训这次同样先量了 —— 结论恰好是 1:1,所以脚本实测在这里的价值不是抓多站点,而是证明没有:

--- multi-site keys ---
(空)
--- ratchet vs measured ---
ratchet slice keys: 37   measured: 37
in ratchet not measured: []
measured not in ratchet: []
ratchet slice prefixes: [ 'marketplace.disclosure.runtime.' ]   measured prefixes: [ 'marketplace.disclosure.runtime.' ]

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

前: 2372 pack-backed (2246/2320 literal keys resolve) … en pack (2836 keys)
后: 2372 pack-backed (2283/2320 literal keys resolve) … en pack (2876 keys)
Every in-scope call-site key resolves against the en pack (2876 keys).

解析数 +37(= 37 个调用点)、en key 数 +40(37 + 前缀家族 3),与清单精确相等。

棘轮 ASSERT 输出:

before: 68 after: 31 removed: 37
ASSERT removed===37 -> true
ASSERT entry lines deleted===38 (37 keys + 1 prefix) -> true
ASSERT no marketplace.*/preview.* key left -> true
ASSERT missingPrefixes 3->2 -> true (removed 1)
  remaining prefixes: ["gantt.linkEnd.","organization.invitations.status."]
ASSERT file still valid JSON -> true (parsed)

remaining by namespace:
{"common":4,"dashboard":1,"detail":4,"empty":3,"home":5,"kanban":1,"layout":3,"perm":9,"workspace":1}

棘轮按行删除而非重序列化(切片四量过:重序列化会把 diff 变成全文件重排)。最终 diff:38 deletions(-),零插入。

严重度:全部是"英文可见、十语不可译"

按切片一验收修正后的框架叙述。37 处全部带内联 defaultValue,无一渲染裸 key(那类由切片一 PR #3583 收)。zh 会话下实际看到的英文:

  • 应用市场:「Your organization」区块标题与其卡片上的 Install / Installing… / Installed / 安装成功提示;包详情页主按钮的 Update,环境下拉里的 Update → v1.2.3Installed v1.2.3
  • ADR-0025 PD4 的安装前权限披露整块(用户在这里授予代码执行权限):This package contains code、信任层级徽章、Reviewed & approved / Not yet reviewed / SignedOn install, this package will be granted: 与四个权限组标签(平台服务、生命周期钩子、网络访问、文件系统访问)、Requests no special permissions.,以及用户勾选的同意复选框 I understand this package runs code and grants the permissions above.
  • ADR-0045 未发布应用横幅:整句说明、History / Publish / Publishing… 按钮,以及发布成功/失败两个 toast。
  • ADR-0067 构建历史面板整套:标题 Build history、说明、Reverted a change / Build change 行标签、revert 徽章、item(s)Revert 按钮、Revert failedReverted — the change has been undone. 两个结果 toast、加载/失败/空态三句。

en ↔ 内联 defaultValue:36/36 逐字节相同,第 37 处结构上不可能

slice call sites: 37  (literal defaultValue: 36, none: 0, non-literal: 1)
en value === inline defaultValue: 36/36
  [TEMPLATE] marketplace.org.installed
      dv = "Installed ${pkg.display_name}"
      en = "Installed {{name}}"

第 37 处(MarketplacePage.tsx:143)的 defaultValue模板串而不是静态字面量:

t('marketplace.org.installed', { defaultValue: `Installed ${pkg.display_name}`, name: pkg.display_name })

逐字节比对对模板串结构上不可能。取的等价关系是:模板串的静态头 + 被插值的标识符 === 包值的静态头 + i18next 的洞,而驱动那个洞的 name 参数该站点本来就在传。所以渲染文本一字不变,只是换成 i18next 来插值。这条等价关系有断言钉住(读源码正则取出模板串与参数名,再比 en 值),站点一移动就红。

无 defaultValue 的站点:0 处 —— 本切片不需要从组件语境取文案。

前缀家族 marketplace.disclosure.runtime.:按封闭枚举,不通配

调用点(PluginDisclosure.tsx:70):

t(`marketplace.disclosure.runtime.${version.runtime}` as any, {
  defaultValue: RUNTIME_FALLBACK[version.runtime] ?? version.runtime,
})

取值面不是开放字符串,而是 spec 的封闭枚举 —— objectstack packages/spec/src/kernel/manifest.zod.ts:84:

export const PluginRuntimeSchema = z
  .enum(['node', 'sandbox', 'worker'])
  .describe('Plugin trust tier (ADR-0025 §3.6)');

所以修法是枚举三个成员,不是补通配前缀。与切片四 console.ai.group. 的差别值得记一笔:那里的封闭联合在同一个文件里(ConversationGroupKey),这里的权威在姐妹仓,本仓测试读不到。故测试的仓内权威取组件自己的 RUNTIME_FALLBACK 表(它也正是 defaultValue 实际读的那张表),并在注释里引 spec 枚举作最终依据。钉两件事:

  • key 面:enmarketplace.disclosure.runtime 子对象键集合 === RUNTIME_FALLBACK 键集合 === ['node','sandbox','worker'](第四个层级一旦加入,这条红 —— 正是棘轮里那条 prefix 条目原本的职责,从棘轮搬进测试);
  • 值面:三个 en 值与 RUNTIME_FALLBACK 的英文逐字节相同(包在时 i18next 答、包不在时兜底表答,用户不能分辨跑的是哪条)。

"key 可达性 vs 值判定"是两件不同的事,分别断言。棘轮 missingPrefixes 3 → 2,切片三/四写的"恰 3 条"钉子同步改为 2 条并加一条 not.toContain('marketplace.disclosure.runtime.')

⚠️ 该家族的宽松处没有在本 PR 修,已立 #3846:marketplaceApi.ts:157runtime 声明为开放 string,由此长出兜底表的 Record< string, string >?? version.runtime(未知层级时把 wire 原值当界面文案渲染到权限面板上)与 as any

死兜底:先量再动,本切片 key 为 0(并顺带量出 16 处别人的)

marketplace.* + preview.*全部 172 个调用点(11 个文件,不只本切片的 37 个)做 AST 扫描,找 t(key) || '…' / ?? '…' 形态:

checked 172 marketplace.*/preview.* call sites in 11 files; dead-fallback constructs found: 16
  → 全部 16 处在 packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx,涉 10 个不同 key
  → 10 个 key 在 en 全都有值(所以 `||` 右侧恒不求值),一处也不属本切片的 37 个 key

其中三条是"误导读者"的完整形态而不只是冗余:moreOptions 包值 More install options / 死句 More options;purgeSuccess 包值用 {{count}} 插值 / 死句用模板串;reseedLocalSuccess 连句式都不同。切片一(PR #3583)删的是同类里 key 缺失的 5 处(那时 || 因 i18next miss 返回 key 恒真而失效);这 16 处的 key 存在,从来没进过棘轮,谁也扫不到。已回填 #3810(该单选项 B 的门禁若只判 defaultValue 会整支漏掉 ||;|| 存量现在是 21 处而非 5 处)。所以本 PR 零组件改动。

同一次扫描顺带给 #3810 交了它正文点名"最该先做"的普查数据:这两个命名空间已存在 key 的 29 个带 defaultValue 站点与 en 零分歧(65/65 含新增),是该单目前第一个零分歧命名空间,与 auth 家族的 8/68 形成对照 —— 支持该单选项 B 落硬门禁而不必配基线。

九包实译:逐包邻键取证

每包都在同一个包marketplace / preview 邻域取证(不跨包套用),证据是量出来的而不是印象:

邻键证据 本切片落值举例
zh marketplace.subtitle(该命名空间 3:0);preview.draftBar.*(2:0);preview.changes.loadFailed = 无法加载待发布变更:(全角冒号,全包 13:6 全角占优);preview.draftBar.message 长破折号写 —— 你的组织;发布后即可让您的用户看到;无法加载历史记录:;已撤销 —— 该变更已还原。
ja 尾冒号是有条件的:谓语结尾用半角(12 例中 10 例,含 preview.changes.loadFailed),名词标签结尾用全角(5 例中 4 例,含 preview.changes.detailChangedKeys)。本切片两处都是谓语结尾故都取半角(初版把 grantsIntro 写成全角,e01dff775 已改正);home.pendingDrafts.published! 换成 履歴を読み込めませんでした:;…付与されます:;公開しました。アプリがユーザーに表示されます。
ko 全包 16 处 : 结尾、0 处全角 설치하면 이 패키지에 다음 권한이 부여됩니다:;{{name}} 설치됨
de preview.draftBar.sampleDataBody / message 一致 Sie;KI-Nutzung 式连字符复合 Veröffentlichen Sie sie, damit Ihre Benutzer sie sehen.;Sandbox-isoliertLifecycle-Hooks
fr 直角撇号 U+0027 占 502:21 压倒 U+2019;:! 前留空格(6:0 / 49:0);preview.changes.confirmNotepaquet Impossible de charger l'historique :;Publié ! L'application…;Ce paquet contient du code
es marketplace.* 一致 usted(9:0);home.pendingDrafts.published = ¡Publicado! Sus cambios están activos. Su organizaciónPublíquela para que sus usuarios la vean.¡Publicado! La aplicación ya es visible para sus usuarios.
pt preview.draftBar.* 一致 você;计数单位取 common.itemCount = {{count}} itens(初版自造 item(ns),在该包追加式约定下得不到 itens,c3e30870c 已改正) Publique-o para que seus usuários o vejam.;itens
ru 全包 161 处写 ё(grid.import.undoConfirmобновлённые);全包 0 处使用括号复数记法(改写句式或缩写,如 {{count}} дн.) Ещё не проверено(有断言钉 ё);элементов(取 common.itemCount)
ar marketplace.browseLink 保留 ;进行式用 جارٍ …;全包只有 30 处以拉丁字母开头 جارٍ تحميل السجل…;40 个值无一以拉丁字母开头(全量断言)

en 相同的字符串一律复用邻键既有译文而不另造,10 对,有断言双向钉住(先验证两边 en 真的同字符串,再验证十包同值):marketplace.action.updateToform.update(不是 layout.activityFeed.typeUpdate,后者 ru 是名词 Обновление 而这是按钮);marketplace.install.installedVersionmarketplace.installedBadge;marketplace.org.install / installedBadge / installingmarketplace.action.install / marketplace.installed / marketplace.action.installing;preview.unpublishedBar.publish / publishing ← 同命名空间的 preview.draftBar.*;publishFailedhome.pendingDrafts.publishFailed;preview.history.buttondetail.history

两处刻意不复用,也有断言钉住:marketplace.action.installed 的 es/ar 是与 aplicación 一致的阴性/带符变体(Instalada / مثبَّت)而 org 徽章标的是 package,取 marketplace.installedInstalado / مثبت;home.pendingDrafts.publishing 的 ja/ko 与同命名空间的 preview.draftBar.publishing 不同,同命名空间优先。

逐字节搜邻键的一个盲区(c3e30870c 暴露,建议后续切片采纳):上面那条复用规则按 en 逐字节相同搜邻键,所以看不见"同一概念、不同英文写法"的邻键。preview.history.items 的 en 是 item(s),而本仓早已译好的 common.itemCount 的 en 是 {{count}} items —— 同一种「计数 + 单位」相邻结构(数字都由组件给、包只供单位词),却因英文拼写不同而搜不到,于是九包自造了括号复数记法。凡 en 是裸单位词 / 裸标签这类由组件拼装句子的 key,都要额外按概念找一次邻键。现取 common.itemCount 的单位词: / / Elemente / éléments / elementos / itens / элементов / عناصر;ko 是唯一刻意偏离 —— common.itemCount ko 是 {{count}}개 항목(量词紧贴数词无空格),而本调用点强制插空格,照搬会渲染 3 개 항목,故舍量词留 항목

一 key 多句英文的裁量:本切片无此情形,但有一条邻近的

切片四的 console.ai.dock.open(同一按钮两句英文)在本切片没有对应物 —— 37 个站点 1:1,不存在同 key 两站点。

需要裁量的是另一件:marketplace.action.updateTo 传了 version 参数,而 defaultValue 是裸 Update(没有洞),i18next 静默丢弃;同一个文件marketplace.install.updateTo 却渲染 Update → v{{version}}。按切片四先例的取向(取无障碍/平台中立句)在这里不适用,因为这不是两句英文的取舍,而是"要不要新增信息"。

裁定:en 落 Update,不加洞。 两条理由:(1) 加洞会改变用户今天看到的字串,而本切片的硬纪律就是 en 逐字节等于内联 defaultValue;(2) 主按钮要不要带版本号是产品裁量(三态按钮宽度、与 Installed / Installing… 的视觉一致性),不是 i18n 决定。已立 #3845(全仓实测该类仅此 1 处 / 1055),并在测试里立此存照:

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}}');

无论将来是删参数还是十包加洞,这条都会红,迫使做决定的人正面处理而不是以为漏了。

反抄袭断言:精确集合是空集,靠三条防空转顶住

实测"与 en 逐字节相同"的 lang :: key 对:0 / 360。这比切片二(12)、三(18)、四(1)都干净 —— 本切片确实没有一个合法同形词。

空集的集合相等断言是最容易空转的形状(切片二 B1 暴露的问题:包为空时同样"绿"),所以配了三条防空转,而不是切片四的一条:

  1. presence:十包 × 40 key = 400 个值全部是非空字符串(独立一条 it.each);
  2. 反面对照(新增,针对空集特有的第二种假绿):空集也可能因为"译者绕开了所有像英文的词"而成立。这四条证明外来词确实在,而且仍不与 en 逐字节相同 —— 所以零是真零,不是回避:
    expect(at(builtInLocales.de, 'marketplace.disclosure.hooks')).toBe('Lifecycle-Hooks');
    expect(at(builtInLocales.fr, 'marketplace.disclosure.hooks')).toBe('Hooks de cycle de vie');
    expect(at(builtInLocales.es, 'marketplace.disclosure.runtime.sandbox')).toBe('En sandbox');
    expect(at(builtInLocales.pt, 'marketplace.disclosure.runtime.sandbox')).toBe('Em sandbox');
  3. 每包一条用户可见面的具名钉子(见下 provider 抽样)。

另加四条本切片特有的形状断言:

  • 插值恰 3 个 key(与切片四的"零插值"正相反):install.updateTo / install.installedVersion{{version}}org.installed{{name}},其余 37 个十包一律不许有洞。判据按 key 具名列出,写错哪一个都读得出来。
  • 省略号/中点/箭头随 en:3 个 key 以 (U+2026)结尾、1 个带 ·(U+00B7,随 marketplace.detail.installedV)、1 个带 (U+2192,随 marketplace.browseLink),十包逐一镜像。
  • ja/zh/ko 尾冒号按各包自己的条件式约定(ja 谓语→半角、zh 全角、ko 半角),三个包在同一个 key 上给出三种正确答案。
  • 两个 revert 标签只在无大小写的包里合并:en 靠大小写区分行类型徽章 revert 与按钮 Revert,zh/ar 无大小写必然同串,而 ja/ko 同样无拉丁大小写却用名词/动词形拉开(取り消し/取り消す되돌림/되돌리기)。钉精确集合 ['ar','zh'],免得 review 把 zh/ar 的重复读成复制粘贴失误。

provider-mounted 抽样钉扎(discrimination check)

五个组件(MarketplacePage / MarketplacePackagePage / PluginDisclosure / CommitTimeline / UnpublishedAppBar)都是裸 useObjectTranslation(),没有 createSafeTranslation defaults map 挡在前面 —— 所以挂 provider 不是仪式。这条前提本身也断言了(读五个文件的源码,确认仍 import useObjectTranslation 且不含 createSafeTranslation),而不是假设。抽样(不是 40 key 全测):

  • en / zh 各解析 6 个代表 key(每组件/每子区一个),断言 not.toBe(key) 且等于包内值;
  • 前缀家族按真实模板调用形态在 en/zh 两路把三个层级全渲染一遍;
  • 三个插值 key 按调用点的传参方式实跑:en 得 Update → v2.1.0 / Installed v1.4.2 / Installed Acme CRM,zh 得 更新 → v2.1.0 / 已安装 Acme CRM;
  • 另八包各钉一条用户可见面(横跨四种书写系统):fr history.title、de disclosure.noPermissions、es history.empty、pt disclosure.fs、ru disclosure.unreviewed、ja history.revertFailed、ko disclosure.grantsIntro、ar unpublishedBar.published;
  • ru 的 ё 一条专项;ar 的"RTL 句首不落拉丁 token"全量 40 key(不是抽样),外加插值后 تحديث → v2.1.0 仍不以拉丁开头;
  • fr 的直角撇号全量 40 key(不许出现 U+2019)+ 冒号/感叹号前空格两条。

反向验证:三处方向跑前先判,三处全中

方向在跑之前就定了。本切片是"包里没有 → 补上",没有被删的死枝、没有计数类下游门禁、没有 canonical-first 的 ?? 链,所以不存在先例里的"诊断变多"或"反转"情形:第 1、2 条应为常规的红,第 3 条应为守卫绿而 parity/本测试红(#3530 记录的门禁分工,不是漏检)。

  1. 只把棘轮 38 行放回(包保留)→ 守卫报 38 条 stale,退出码 1:
    38 baseline entries are stale — the defect is gone, so the entry must go too (this file is a ratchet; it only shrinks):
      missingKeys: marketplace.action.updateTo
      …(37 条)
      missingPrefixes: marketplace.disclosure.runtime.        ← 前缀那条同样被判 stale
    GUARD EXIT=1
    
  2. 只回退 en.ts(棘轮保持已删)→ 守卫报 38 条 unexpected / 38 distinct,退出码 1,解析数同步回落 2283 → 2246:
    2372 pack-backed (2246/2320 literal keys resolve)
    38 call sites reference a key the en pack does not define (38 distinct):
      …MarketplacePackagePage.tsx:555:17  [missing-key]  marketplace.action.updateTo
      …MarketplacePage.tsx:143:35  [missing-key]  marketplace.org.installed
    GUARD EXIT=1
    
    38 = 37 个 missing-key 站点 + 1 个 missing-prefix 站点;38 distinct 正是棘轮那 38 行。
  3. 只回退 zh.ts(en 保留)→ 守卫仍然绿(它只看 en),而 parity 与本切片测试红,失败文本就是前提本身:
    守卫: Every in-scope call-site key resolves against the en pack (2876 keys).   GUARD EXIT=0
    AssertionError: zh is missing 40 key(s)
    AssertionError: expected 'In-process · full trust' to be '进程内 · 完全信任'
    AssertionError: expected 'Update → v2.1.0' to be '更新 → v2.1.0'
    AssertionError: expected 'This package contains code' to be '此软件包包含代码'
    Test Files  2 failed (2) | Tests  13 failed | 51 passed (64)
    
    第 3 条同时把 fallbackLng: 'en' 这个藏了这些 key 数月的机制直接摆出来:zh 缺 key 时 t() 返回的是en 的值(In-process · full trust),不是 key、不报错 —— 所以只补 en 不补九包这种半成品,守卫看不见,parity 看得见。

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

  1. es 语言包的 preview.draftBar 同一条横幅两种敬称 —— messageClean 用 usted、message 用 tú,西语用户按「有无待发布变更」看到不同人称 #3844(新立,concrete) — es 的 preview.draftBar 同一条横幅两种敬称:message 用 tú(estáspubliques)、messageCleansampleDataBody 用 usted(veEstáPublíquela)。西语用户发布掉手上草稿之后,横幅从 tú 切到 usted。本切片给 19 个 preview 新 key 裁定 usted,依据写进断言:es 全包默认 usted(102:30,tú 集中在 auth/report/organizations 邻域 —— 切片二当时正确裁定的那几处)、marketplace.* 邻域 9:0 usted、且最强的一条 —— preview.unpublishedBar.published 的结构孪生 home.pendingDrafts.published 就是 usted(Sus cambios)。既有的两个 key 不在本切片围栏内,未动。
  2. [finding] marketplace.action.updateTo 传了 version 插值参数但值里没有对应的洞 —— i18next 静默丢弃,主按钮只写 Update 而同文件的姐妹键渲染 Update → v1.2.3(全仓 1/1055) #3845(新立,finding)marketplace.action.updateTo 的休眠 version 参数(见上文裁量)。全仓实测 1 / 1055,是个单例。附了实现该门禁的一个陷阱:参数名提取必须先消掉嵌套的 t() 调用 —— 第一遍扫描就在 ImageField.tsx:105 上误报过一次(外层 enlarge 的参数对象里嵌了内层 imageAltindex:),剔除嵌套括号跨度后复测为 1。本 PR 报的所有数字都是复测后的数字。
  3. [finding] marketplaceApi 把插件信任层级 runtime 声明为开放 string,而生产方 spec 的 PluginRuntimeSchema 是三成员封闭枚举 —— 消费侧比契约宽松,PluginDisclosure 的 as any 与开放兜底表都由此而来 #3846(新立,finding)marketplaceApi.ts:157runtime 声明为开放 string,而生产方 spec 的 PluginRuntimeSchema 是三成员封闭枚举;由此长出 Record< string, string > 兜底表、?? version.runtime(把 wire 原值当文案渲染到权限披露面板上)与 as any。contract-first 的反面:消费方比生产方宽松。
  4. [finding] 8 处 auth 调用点的内联 defaultValue 与 en 包值不一致 —— key 存在故兜底是死代码,三道 i18n 门禁按设计都看不见这一类 #3810(已存在,回填评论) — 上面的 16 处 || 死兜底 + 这两个命名空间已存在 key 的零分歧普查数据(该单正文自陈"规模未量",累计已量约 600 / 2320 站点)。

验证

node scripts/check-i18n-call-site-keys.mjs
  → Every in-scope call-site key resolves against the en pack (2876 keys).   EXIT=0
node scripts/check-i18n-en-drift.mjs
  → 0 en value(s) changed (40 key(s) added, 0 removed) — No en value changed in this range.
node scripts/check-control-bytes.mjs
  → OK (scanned 3755 tracked text file(s); skipped 85 binary)
node scripts/check-changeset-presence.mjs
  → 13 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]' 全部 16 个改动/新增文件   → 零命中(gate 之外的自扫,语言包全覆盖)
git log --format=%B origin/main..HEAD | grep -ci fable            → 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 31 passed (31) | Tests 536 passed (536)      (534 → 535 → 536,后两次是两条改正各带的新断言)

pnpm --workspace-concurrency=2 --filter @object-ui/i18n type-check     → tsc --noEmit 通过
pnpm --workspace-concurrency=2 --filter '@object-ui/react^...' build   → 先建依赖(新 worktree)
pnpm --workspace-concurrency=2 --filter @object-ui/react type-check    → tsc --noEmit + tsconfig.typetests.json 通过
npx eslint 本 PR 改动的 14 个 i18n 文件                                → 0 problems

all-locales-key-parity.test.ts 含在上面 31 个文件内,已绿(它是补完 en 后立刻要求九包补齐的那道门禁)。@object-ui/i18n 无工作区内依赖(只依赖 i18next / react-i18next),所以它自己不需要先建依赖;@object-ui/react 需要,已按 §9 先建。@object-ui/react 只是 type TranslationKeys 的转出口(TranslationKeys = typeof en),全仓无消费方把它当必需形状声明,所以加 key 不会窄化谁 —— 已 grep 确认并实跑该包 type-check。

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

测试文件自身的两处 bug 在首次提交前被自己的套件抓到并修掉,记在这里因为第一条是会静默漏检的那种:(1) 用带 /g 的正则做 .test() 放在 filter 里,lastIndex 跨调用残留,隔一个漏一个 —— 现在拆成 HOLES(/g,给 .match())与 HAS_HOLE(无 /g,给 .test()),并把原因写在注释里;(2) 复用清单里误把 preview.history.revertAction 当成"en 同字符串"(en 是 Revert,grid.bulk.undoUndo),已移出该清单、单独立一条断言并把更弱的那个主张说清楚 —— 复用的是各包既有的 undo 动词而非同一句英文,因为九包都没有区别于 undo 的 revert 专词,另造一个会让同一个按钮在用户那里有两个名字;en 若将来真的区分 revert/undo,那条断言就是必须回头看的地方。

围栏

16 文件(相对 merge-base d3e738af8 实测:1357 insertions(+), 55 deletions(-)):packages/i18n/src/locales/*.ts(十包,仅新增本切片 key)、scripts/i18n-call-site-key-baseline.json(仅删本切片 38 条)、packages/i18n/src/__tests__/marketplace-preview-namespace-3546.test.tsx(新增)、auth-namespace-3546.test.tsx + console-namespace-3546.test.tsx + organization-namespace-3546.test.tsx(棘轮总数 68 → 31,前两者另同步 missingPrefixes 3 → 2 —— 那个计数器每切片动一次且只降)、changeset(@object-ui/i18n patch)。零组件改动(本切片 key 的死兜底量出为 0)。与在飞 #3808(plugin-detail/console/components)零相交;main 其后落地的 #3841 与本围栏实测零文件相交。未动 content/docs/releases/**

…disclosure.runtime. 前缀家族,十包补齐 (#3546 切片五)

`scripts/check-i18n-call-site-keys.mjs`(#3530 的守卫)实测:37 个不重复 key
在 37 个调用点上被 `t()` 引用而十个语言包一个都没有,外加 1 个整族缺失前缀
(`marketplace.disclosure.runtime.`,静态头在 en 一条都不匹配,所以插值取任何
值都是缺的)。分母以脚本实测为准不手数 —— 本切片罕见地是 1:1(前四切片都有
一 key 多站点,切片二预测 90 实为 93)。

37 处全部带内联 `defaultValue`,无一渲染裸 key(那类由切片一 PR #3583 收),
所以严重度是 #3517 类:英文照常渲染,十种语言全部卡在英文上。zh 会话下实际
看到的英文:应用市场「Your organization」区块与其 Install / Installing… /
Installed 按钮、版本升级提示;ADR-0025 PD4 的**安装前权限披露**整块 —— This
package contains code、信任层级徽章、Reviewed & approved / Not yet reviewed /
Signed、四个权限组标签(平台服务/生命周期钩子/网络/文件系统)与用户勾选的
同意复选框;ADR-0045 未发布应用横幅及其发布 toast;ADR-0067 构建历史面板
整套 —— 标题、说明、每条提交的标签、Revert 按钮与它的两个结果 toast。

前缀家族按**封闭枚举**处理,不通配:取值面是 spec 的 `PluginRuntimeSchema`
= `z.enum(['node', 'sandbox', 'worker'])`(ADR-0025 §3.6),故枚举三个成员,
该家族离开棘轮 `missingPrefixes`(3 → 2)。测试读组件自己的 `RUNTIME_FALLBACK`
兜底表(spec 在姐妹仓,本仓测试读不到),第四个层级加进来就红 —— 那正是棘轮
里那条 prefix 条目原本的职责,从棘轮搬进测试。

en 值与被替换的内联 `defaultValue` 逐字节相同(36 个字面量站点 36/36);
第 37 个站点的 `defaultValue` 是模板串 `` `Installed ${pkg.display_name}` ``,
逐字节比对结构上不可能,取其静态头加 i18next 的洞 `Installed {{name}}` ——
`name` 参数该站点本来就在传。所以用户今天看到的英文一字未改。

九包实译逐包取邻键证据,en 字符串已存在的一律复用邻键既有译文而不另造
(10 对,含 form.update / marketplace.installedBadge / preview.draftBar.publish
/ home.pendingDrafts.publishFailed / detail.history),使一句英文不会在同一
语言里渲染成两句。两个命名空间在 zh 里合法地取**不同**第二人称
(marketplace 你 3:0,preview 您 2:0),es 的 marketplace 邻域一致 usted 9:0。

零组件改动:对 marketplace/preview 全部 172 个调用点(11 文件)做 AST 扫描,
量出 16 处 `t(key) || '英文'` 死兜底,10 个 key 在 en 全都有值、且一处也不属
本切片的 37 个 key,故一处未动(已回填 #3810)。

棘轮按行删除而非重序列化:38 deletions,零插入。

顺手量出、未在本 PR 修:#3844(es preview.draftBar 同一横幅两种敬称)、
#3845(marketplace.action.updateTo 传了 version 但值里没有洞,全仓 1/1055)、
#3846(marketplaceApi 把 runtime 声明为开放 string 而 spec 是封闭枚举)。

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@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 8:23pm

Request Review

@github-actions github-actions Bot added the tests label Aug 8, 2026
自查 ja 包尾冒号分布时发现本切片自己写错了一处:`marketplace.disclosure.grantsIntro`
以谓语 `付与されます` 结尾,却给了全角 `:`。

ja 包在这件事上不是不一致,而是**有条件**的:

- **谓语结尾 → 半角 `:`**,12 例中 10 例(五个 connectAgent 正文、
  `oauth.consent.willAllow`、`auth.login.devAdminHint.body`、
  `grid.bulk.affectedRecords`、`connectAgent.apiKey.showOnce`、
  `preview.changes.loadFailed`);
- **名词标签结尾 → 全角 `:`**,5 例中 4 例(`grid.import.mappingTemplate`、
  `grid.import.savedMapping`、`auth.verifyEmail.sentTo`、
  `preview.changes.detailChangedKeys`)。

本切片两个 ja 冒号都是谓语结尾,故都取半角。`preview.history.loadFailed`
本来就对(它另外还与同名兄弟键 `preview.changes.loadFailed` 逐字节相同);
`grantsIntro` 没有同名兄弟,只能靠这条分布裁,之前裁反了。

同时把这条约定连同 zh(全包全角标点,13:6)与 ko(全包 16 处半角、0 处全角)
的反向对照写成断言,免得下一个读者以为 ja/zh 在同一个字串上取不同冒号是笔误,
也免得同一个错再犯一次。

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@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-CbOG9mv_.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.41KB 106.33KB
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.14KB 58.81KB
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

追加一次提交:e01dff77 —— ja 尾冒号自查改正,并推翻本 PR 正文里的一处说法

正文的九包证据表在 ja 那行写了:preview.changes.loadFailed 半角冒号"与同包 detailLoadFailed 的全角并存 —— 既有分歧,镜像同名兄弟键"。

那个判断是错的,而且因为它错,本切片自己写错了一处。把 ja 全包的尾冒号按结尾类型分类之后,规律很清楚 —— ja 包不是不一致,是有条件的:

谓语结尾 → 半角 :(12 例中 10 例)

grid.bulk.affectedRecords          対象レコード({{count}}):
auth.login.devAdminHint.body       …サインインします:
oauth.consent.willAllow            このアプリは次のことができます:
connectAgent.claudeCode.body       …自動で開きます:
connectAgent.claudeCode.plugin     …コマンドを追加):
connectAgent.cursor.body           …追加してください:
connectAgent.vscode.body           …コマンドで):
connectAgent.codex.body            …ご利用ください(下記参照):
connectAgent.apiKey.showOnce       …再表示されません:
preview.changes.loadFailed         保留中の変更を読み込めませんでした:

名词标签结尾 → 全角 (5 例中 4 例)

grid.import.mappingTemplate        マッピングテンプレート:
grid.import.savedMapping           保存済みマッピング:
auth.verifyEmail.sentTo            送信先:
preview.changes.detailChangedKeys  その他の変更:

两边各有一个例外(dashboard.noDataSourceForpreview.changes.detailLoadFailed 是谓语结尾却用了全角),所以是 10:2 / 4:1,不是绝对律 —— 但方向不含糊。

本切片写错的那处

marketplace.disclosure.grantsIntro 的 ja 值以谓语 付与されます 结尾,我却给了全角 。已改为半角:

- grantsIntro: "インストール時に、このパッケージには次の権限が付与されます:",
+ grantsIntro: "インストール時に、このパッケージには次の権限が付与されます:",

preview.history.loadFailed(履歴を読み込めませんでした:)本来就对 —— 它有同名兄弟键 preview.changes.loadFailed 可以逐字节镜像,所以镜像掩盖了我对规律的误解。grantsIntro 没有同名兄弟,只能靠分布裁,于是裁反了。这就是"镜像兄弟键"这个方法的边界:它在有兄弟时安全,没兄弟时不给任何提示。

连同反向对照写成了断言

新增一条 it,把这条约定和它的两个反向对照钉住,免得下一个读者把 ja/zh 在同一个字串上取不同冒号读成笔误,也免得同一个错再犯:

expect(at(builtInLocales.ja, 'marketplace.disclosure.grantsIntro')).toBe(
  'インストール時に、このパッケージには次の権限が付与されます:',
);
expect(at(builtInLocales.ja, 'preview.history.loadFailed')).toBe('履歴を読み込めませんでした:');
for (const key of ['marketplace.disclosure.grantsIntro', 'preview.history.loadFailed']) {
  expect((at(builtInLocales.ja, key) as string).endsWith(':')).toBe(false);   // ja: 谓语 → 半角
  expect((at(builtInLocales.zh, key) as string).endsWith(':')).toBe(true);    // zh: 全包全角标点
}
expect((at(builtInLocales.ko, 'marketplace.disclosure.grantsIntro') as string).endsWith(':')).toBe(true);

zh 与 ko 那两条是反向对照,不是凑数:zh 全包全角标点(165 处全角逗号、尾冒号 13:6 全角占优),所以同一个字串在 zh 必须是全角;ko 全包 16 处半角尾冒号、0 处全角。三个包在同一个 key 上给出三种正确答案 —— 与正文里 zh 的"marketplace 用你、preview 用您"是同一条"同一规则、不同答案"的先例。

请以本评论的分类取代正文九包表 ja 行里"既有分歧"那半句。

复验

flock … vitest run packages/i18n scripts/__tests__/check-i18n-{call-site-keys,en-drift}.test.ts --maxWorkers=2
  → Test Files 31 passed (31) | Tests 535 passed (535)     (较上一版 +1,即新增的这条)
node scripts/check-i18n-call-site-keys.mjs → Every in-scope call-site key resolves against the en pack (2876 keys).
node scripts/check-i18n-en-drift.mjs       → No en value changed in this range.
node scripts/check-control-bytes.mjs       → OK (scanned 3755 tracked text file(s))
npx eslint ja.ts + 本切片测试文件            → 0 problems
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]' 两个改动文件 → 零命中
git log --format=%B origin/main..HEAD | grep -ci fable → 0

en 值未变(改的是 ja),所以 check-i18n-en-drift 与 en↔defaultValue 的 36/36 都不受影响;棘轮、分母、前缀家族处理一概不变。围栏仍是那 16 个文件。


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-lYS93pmR.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.41KB 106.33KB
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.14KB 58.81KB
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

`CommitTimeline.tsx:142` 渲染的是 `{c.itemCount} {t('preview.history.items')}` ——
数字由组件给,包只供单位词。这与 `common.itemCount`(en `{{count}} items`)
是**同一种「计数 + 单位」相邻结构**,而后者本仓十包早已译好。之前自造了括号
复数记法,两处错:

1. **ru 包全 2832 个值里一次也没用过 `(s)` 记法** —— 它要么改写句式
   (`marketplace.detail.purgeSuccess` = `Удалено … записей: {{count}}.`),
   要么缩写(`fields.relativeDate.overdue` = `Просрочено на {{count}} дн.`)。
   `элемент(ов)` 是凭空引进的形状。
2. **`item(ns)` 在 pt 自己的追加式约定下根本得不到 `itens`**。该包的记法是
   `linha(s)` / `registro(s)` / `coluna(s)` 这种词干加后缀,变干的情形写成
   `alteração(ões)`(`home.pendingDrafts.message`)—— 无论按哪种读法,
   `item(ns)` 都不成立。

改为直接取 `common.itemCount` 去掉 `{{count}} ` 前缀后的单位词:

    zh 项   ja 件   de Elemente   fr éléments   es elementos
    pt itens   ru элементов   ar عناصر

**ko 是唯一刻意的偏离**:`common.itemCount` ko 是 `{{count}}개 항목`,量词 개
紧贴数词不带空格;而本调用点在数字与单位之间强制插一个空格,照搬会渲染成
`3 개 항목`(空格落在数词—量词之间)。故舍量词留名词,渲染为 `3 항목`。

**没找到这个邻键的原因值得记下来**:本切片的复用清单是按 **en 逐字节相同**
搜邻键的,而 `common.itemCount` 的 en 是 `{{count}} items` 而不是 `item(s)`,
所以"同一概念、不同英文写法"的邻键对那个搜索是不可见的。已把这条连同上面的
裁定写成断言(并双向验证 `common.itemCount` 确实就是那个单位词),避免复发。

en 值不变(仍是调用点的 `item(s)`,逐字节纪律不动),故 en-drift、
en↔defaultValue 的 36/36、棘轮与前缀家族处理一概不受影响。

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt

Copy link
Copy Markdown
Collaborator Author

追加第三次提交:c3e3087 —— preview.history.items 九包改用既有单位词,并暴露本切片取证方法的一个盲区

上一条评论改 ja 冒号时顺手把各包的括号复数记法也查了一遍,查出本切片自造了一个不该自造的形状,而且顺带发现了取证方法本身的一个洞。这条比冒号那条重要,因为洞是方法性的。

现场

CommitTimeline.tsx:142 渲染 {c.itemCount} {t('preview.history.items')} —— 数字由组件给,包只供单位词。之前九包写的是:

zh 项    ja 件    ko 개    de Element(e)    fr élément(s)
es elemento(s)    pt item(ns)    ru элемент(ов)    ar عنصر

括号复数记法是照抄各包别处的做法,两处错:

  1. ru 包全 2832 个值里一次也没用过 (s) 记法。 实测 0 处。该包遇到可数名词是改写句式缩写:

    marketplace.detail.purgeSuccess    Удалено демонстрационных записей: {{count}}.
    marketplace.detail.reseedPartialErrors    (не удалось записать записей: {{count}})
    fields.relativeDate.overdue    Просрочено на {{count}} дн.        ← 数字紧邻单位、无法变格时用缩写
    

    элемент(ов) 是凭空引进 ru 从不使用的形状。对照 de/fr/es/pt —— 那四个包确实用这个记法(linha(s)Zeile(n)fila(s)colonne(s)),所以在它们那里不算错;错的是把 ru 也一并套进去。

  2. item(ns) 在 pt 自己的约定下根本得不到 itens 该包的记法是词干加后缀(linha(s)registro(s)coluna(s)aprovador(es)),词干变化的情形写成 alteração(ões)(home.pendingDrafts.message)。item 的复数是 itens(-m → -ns),无论按追加读还是按替换读,item(ns) 都不成立。

改法:取 common.itemCount,不自造

本仓已经有这个字串。common.itemCount 的 en 是 {{count}} items —— 与本 key 是同一种「计数 + 单位」相邻结构,而且十包早已译好:

common.itemCount 本 key 新值
zh {{count}} 项
ja {{count}} 件
de {{count}} Elemente Elemente
fr {{count}} éléments éléments
es {{count}} elementos elementos
pt {{count}} itens itens
ru {{count}} элементов элементов
ar {{count}} عناصر عناصر

包里另有 common.itemCountOne({{count}} itemэлемент / item / Element …)承担单数,所以这个包本来就是用「裸复数 + 另一个单数 key」解决这件事的,不是括号记法。本调用点没有单数分支,取复数。ru 也因此干净收口 —— 不需要缩写,элементов 就是该包自己的答案。

ko 是唯一刻意的偏离:common.itemCount ko 是 {{count}}개 항목,量词 개 紧贴数词、不带空格;而本调用点在数字与单位之间强制插一个空格,照搬会渲染成 3 개 항목(空格落进数词—量词之间)。故舍量词留名词,渲染 3 항목。偏离与理由都写进了断言。

方法性的洞,记下来给后续切片

为什么之前没找到这个邻键:本切片(以及切片二/三/四)的复用清单是按 en 逐字节相同搜邻键的 —— 那条规则本身是对的,它保证"一句英文不会在同一语言里渲染成两句"。但它只看得见英文拼写相同的邻键。common.itemCount 的 en 是 {{count}} items,本 key 的 en 是 item(s),同一概念、不同英文写法,对那个搜索完全不可见。

所以逐字节搜邻键是必要但不充分的:凡 en 是"裸单位词 / 裸标签"这类由组件拼装句子的 key,都要额外按概念找一次邻键。这条建议后续切片(以及 #3810 的门禁设计)采纳。

新增断言把裁定和它的前提都钉住(双向:既断言本 key 的值,也断言 common.itemCount 确实就是那个单位词,所以邻键一改本条就红),并加一条"任何包不得在此重新引入 (…) 记法"。

复验

flock … vitest run packages/i18n scripts/__tests__/check-i18n-{call-site-keys,en-drift}.test.ts --maxWorkers=2
  → Test Files 31 passed (31) | Tests 536 passed (536)      (首版 534 → 535 → 536,两条新增断言)
node scripts/check-i18n-call-site-keys.mjs → Every in-scope call-site key resolves against the en pack (2876 keys).
node scripts/check-i18n-en-drift.mjs       → No en value changed in this range.
node scripts/check-control-bytes.mjs       → OK (scanned 3755 tracked text file(s))
npx eslint 十个语言包 + 本切片测试文件        → 0 problems
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]' 八个改动文件 → 零命中
git log --format=%B origin/main..HEAD | grep -ci fable → 0

en 值未变(改的是九包),所以 en↔内联 defaultValue 的 36/36check-i18n-en-drift 的 0 changed、棘轮 68 → 31missingPrefixes 3 → 2、分母 37 key / 37 站点 / 1 前缀、以及三方向反向验证的结论全部不受影响。围栏仍是那 16 个文件。

反抄袭空集也仍然成立(新单位词无一与 en 的 item(s) 相同),零插值断言不变。


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-BxladU7W.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.41KB 106.33KB
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.14KB 58.81KB
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

✅ 切片五验收:ACCEPT — PR #3847(Fixes-part-of,#3546 继续 open,余 31 key + 2 前缀家族:perm 9 为最大块,gantt.linkEnd. 取值面待查)。(objectui 分片 PM,session_01GTRjn8xBqp75dk7kFupVRt)

裁定要点:(1) 分母六轮如一(37+1 前缀,1:1 仍先量证明);模板串 defaultValue 的等价处置(静态头 + i18next 洞 + 读源码断言钉等价)正确;(2) 敬称「同一规则不同答案」的量化取证(zh 你 3:0 / 您 2:0;es preview 邻域 2:2 分裂时按结构孪生裁)是该纪律迄今最严格执行;(3) 反抄袭空集 0/360 配三条防空转(外来词反面对照证明零是真零)—— 优于前四轮;(4) 两处自误自查改正如实入档采纳(ja 尾冒号约定系有条件、复数记法自造被包内证据推翻)—— 并由此浮出方法性盲区入方法库:按 en 逐字节搜邻键看不见「同概念不同英文写法」,裸单位词/裸标签类 key 须额外按概念找邻键(后续切片与 #3810 门禁设计采纳);(5) 假绿监视第 N 例照例弃用改 MCP 核实。19/19 CI 全绿;棘轮 68→31 三测试同步;衍生 #3844(bug 待分诊)/#3845/#3846(finding)+ #3810 累计量化回填(~600/2320 站点)边界合规。undraft + auto-merge。


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