Skip to content

fix(i18n): 十包补齐 organization 命名空间的 90 个缺失 key (#3546 切片二) - #3605

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3546-slice2-organization
Aug 7, 2026
Merged

fix(i18n): 十包补齐 organization 命名空间的 90 个缺失 key (#3546 切片二)#3605
yinlianghui merged 1 commit into
mainfrom
claude/issue-3546-slice2-organization

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes-part-of #3546

#3546 切片二,接 PR #3583(切片一,已合并)。#3547 的守卫量出 258 个 t() 引用但十个包都没有的 key,organization 是命名空间轴上最大的一批:90 个 key、93 个调用点、7 个组件。本 PR 只处理这一批,其余 163 个 key 与 4 个前缀家族一条未动。

⛔ 没有新增任何 defaultValue

动手前的核实:这批全部是"英文可见、十语不可译",没有一处裸 key

切片一验收把严重度框架修正为"8 处里 6 处真渲染裸 key,另 2 处属 #3517 类"。本批按同一标准逐点核实,结论是整批 90 个 key 都属于较轻的 #3517,而且三个前提都是量出来的,不是推断的:

核实项方法结果
是否有无兜底站点AST 提取 93 个调用点的第二参数93/93 都带内联 defaultValue,0 处无兜底
是否有 defaults map 遮蔽逐文件读绑定7 个组件全部裸用 useObjectTranslation(),无 createSafeTranslation
是否有死 `` 兜底可删

所以组件一个字没改 —— 切片一认领时写的"仅当有死兜底需删(先量再动)",量下来是没有。这也是本 PR 只有语言包、基线、测试和 changeset 的原因。

另外两处值得记录的量测:

  • 90 个 key 对 93 个调用点,差额是 organization.backToListinvitations.copyFailedinvitations.linkCopied 三个 key 各有两个调用点。守卫计数器统计的是调用点不是去重 key,这个差额下面预测表里会再出现一次。
  • en 文案与调用点内联 defaultValue 逐字节一致,93/93 零漂移,脚本比对而非目测。带 defaultValue 的站点上,包里有值时 i18next 用包、无值时用默认值,两条路径文案不一致就是静默分叉。

改了什么

1. 新增 organization 顶层命名空间,插在既有 organizations 段之后 —— 十个包同一锚点,方便对读。两者刻意保持独立:单数是组织管理面(console/organizations/** 七个组件 + 工作区切换器),复数是组织选择器,在调用点上二者只差一个字母,合并会静默重指 90 个 key。en 包里为此写了注释,测试里也钉了一条断言。

段内按调用点顺序分 6 组:tabs(3)、members(10)、invitations(24)、settings(32)、accept(14)、switcher(4),外加 3 个顶层 key。

2. 九包逐条实译,按各自邻键的惯例写,不照抄切片一的用词。 每包的依据(摘要,非 90×10 逐条):

  • zh —— 组织 / 成员 / 邀请 / 角色(随 capability.group.orgworkspace.members);全角标点 。?,fields.file.uploadFailed 的全角引号惯例用在 输入“{{slug}}”以确认;第二人称用「您」,随 organizations.heading
  • fr —— 冒号/问号前空格(Retirer ce membre ?),随该包 55 处同类;引号用 « »(随 fields.file.uploadFailed);失败类统一 Impossible de …,与 detail.linkCopyFailed 完全一致 —— copyFailed 直接复用了那句既有译文。
  • de —— 敬语 Sie(随 organizations.subtitle);失败类用 … konnte nicht … werden 被动式(随 detail.linkCopyFailed);破折号用该包主用的
  • es —— 本段用非正式 tú,这是本包该邻域的惯例:organizations.heading = "Tus organizaciones"、emptyDescription = "Crea tu primera…"、workspace.createDescription = "para que tu equipo colabore"。全仓 es 正式/非正式各 42/43 对半,所以按邻域定,而非按切片一(wizard.* 邻域是 form.*/validation.*,那边是正式体)。引号 « »
  • ja —— 組織 / メンバー / 招待 / ロール;失败类 …に失敗しました;引用用 「」(随 fields.file.uploadFailed)。
  • ko —— members 用 구성원 而非 멤버(随 workspace.members);助词处理写全 {{name}}을(를){{role}}(으)로,随 lookup.selectFirst 的既有写法。
  • pt —— Excluir(随 common.delete)、GerenciarSalvar alterações;views 译作 exibições,与切片一在同包里定的 objectView 用词一致。
  • ru —— 保留 ё 正字法(удалёнприглашённому),随该包 152 处;失败类统一 Не удалось …;引号 « »
  • ar —— المؤسسة(随 cloudConnection.bound.organizationorganizations.*,而非 capability.group.orgمنظمة);占位符按阿语语序自然落位。

3. 棘轮基线精确减 90(missingKeys 253 → 163),脚本断言 removed === 90 且删除项全部organization. 开头、missingPrefixes 一条未动、note 未动。

一处诚实的偏差:12 对与 en 逐字相同的译文

切片一的"九包不得与 en 逐字相同"断言在 5 个 key 上是干净的空集。铺到 90 个 key 上不是 —— 有 4 个 key 在部分语言里本来就该跟英文一样:

key相同的包理由
settings.logoLabel = "Logo"zh fr de es pt外来词;appDesigner.logoUrl 在这五包里同样保留 "Logo"
settings.slugLabel = "Slug"fr de es pt同上;workspace.slugLabel 在这四包里也保留 "Slug"
settings.generalTitle = "General"es就是西语这个词
tabs.invitations / invitations.title = "Invitations"fr就是法语这个词

没有为了让断言好看而扭曲译文。 做法是把这 12 对写成显式白名单,断言用集合相等而不是包含 —— 多一对相同会红,把其中一条改成不相同也会红并强制删掉那一行,白名单没法悄悄变成一张通行证。810 对里 12 对例外,即 90 个 key 中 86 个在九包里全部不同

验证:方向与数字都是跑之前先写下的

预测原文在提交前落盘。守卫 node scripts/check-i18n-call-site-keys.mjs:

指标预测实测
退出码00
en 包 key 数2646 → 2736(+90)2736
基线 missingKeys253 → 163(−90)163
基线 missingPrefixes4,不变4
可解析调用点2051 → 2141(+90)2051 → 2144(+93)

最后一行我预测错了,如实记录。 我把 2319 当成了去重 key 数,它其实是调用点数(工单正文的 276 处 / 258 个不重复 key2319 − 2043 = 276 就是这个关系)。90 个 key 里有 3 个各有两个调用点,所以增量是 93 不是 90。数字对得上、方向也对,错的是我对分母的理解 —— 补记在这里,免得下一个切片照抄这个错误。

逆向验证 A —— 中间态(只补 en,九包与基线不动)

预测:平价 RED ×9 + 棘轮 stale RED。两条都精确命中:

9 个包各报:is missing 90 key(s) ← 9 FAIL,数目一个不差
90 baseline entries are stale — the defect is gone, so the entry must go too
missingKeys: organization.accept.accept
... EXIT=1

missingPrefixes: organization.invitations.status.没有混进 stale 列表 —— 预测过它不会,因为本 PR 没有任何 key 以那个静态前缀开头。补齐九包后平价转绿而棘轮仍红,补完基线才全绿:三步各自独立,顺序符合守卫头注的设计。

逆向验证 B —— 新断言真的有鉴别力(用了保存的补丁分级,未用 git stash)

两个方向分别验,因为它们抓的是不同的东西:

B1,删掉 fr 整段 → 4 条红:平价的 fr defines every en key、本文件的 fr defines every organization key as a non-empty string、集合相等的防抄袭断言(fr 的 3 条 cognate 消失了)、占位符一致性。这条顺带证明了防抄袭断言单独看是空的 —— 键不存在时 undefined !== en,它反而会变绿,是"存在性"那条兜住的。

B2,把 fr 的 settings.dangerZone 改成英文 "Danger zone"只有防抄袭断言红,平价全绿:

× the nine non-en packs carry real translations, not the English strings
Test Files 1 failed | 1 passed (2)
Tests 1 failed | 39 passed (40)

平价绿是正确的 —— 键集合没变,它按设计就看不见值。这正是这条断言存在的理由:"九包抄英文"能拿到全键平价、十包全绿,而九种语言仍在读英文。B2 是这个盲区的直接演示。

两次逆验后把 fr 还原,并用 diff 比对补丁确认与逆验前逐字节一致。

测试与门禁

npx vitest run packages/i18n 24 files / 304 tests ✅
其中新增 organization-namespace-3546.test.tsx 20 tests ✅
npx vitest run packages/app-shell/src/console/organizations
+ layout/__tests__/WorkspaceSwitcher.test.tsx 8 files / 38 tests ✅
pnpm --filter @object-ui/i18n type-check ✅(tsc --noEmit 无输出)
pnpm --filter @object-ui/i18n lint ✅ 0 errors(28 条既有 warning)
node scripts/check-i18n-call-site-keys.mjs ✅ EXIT=0
pnpm changeset:check ✅
pnpm check:control-bytes ✅ 3637 files

消费半径扫过了,不只改动包:WorkspaceSwitcher.test.tsx 里有 getByText('Working organization') / 'Switch organization' / 'New records are created here…' 三处硬编码英文断言。它们保持绿,而且是有理由保持绿 —— en 包值与该组件的 defaultValue 逐字节一致,provider 两条路径不分叉。这也是前面那条零漂移量测的实际价值。

新测试为什么长这样

packages/i18n/src/__tests__/organization-namespace-3546.test.tsx,20 条:

  • 挂 provider。这七个组件全部裸用 useObjectTranslation(),不挂 provider 时 i18next 根本不是应答方,断言描述的就不是控制台里的那条路径 —— 工单记录的假绿陷阱。
  • en 断言没有鉴别力,zh 才有。这批修复前 en 输出就是对的(内联默认值),修前修后完全相同;真正由红转绿的是 zh 那半边,所以钉修复的断言全部是 zh。en 用例只用来证明 key 经真实绑定可达、没有被遮蔽。
  • 90 个 key 全量做结构断言(存在、非空、译文不等于 en、占位符一致),渲染断言按代表性抽样:每个子命名空间一个 key(各标注了归属组件),外加全部 6 个带插值的 key —— 插值是译错了会真出问题的地方,不是读着别扭而已。
  • 棘轮反向钉住:断言基线里已经没有任何 organization. key,并且 organization.invitations.status. 前缀仍在 —— 那条断言就是防止这个前缀家族被顺手忘掉的记号。

本 PR 不做、留给后续的

organization.invitations.status. 这个整族缺失的模板前缀(InvitationsPage.tsx:161:206)留在基线里没动。它跟 90 个字面量 key 不是同一种修法 —— 要先定下邀请状态的取值集合再整族补,属于工单正文列的"4 个整族缺失的模板 key"那一批。切片二认领范围写的是 90 个 key、棘轮恰减 90,所以按范围停手,并在测试里立了标记。

顺带发现:。本批没有量到范围外的缺陷。

文件面

十个语言包(仅新增 organization 段)、scripts/i18n-call-site-key-baseline.json(仅删 90 条 organization)、1 个新测试、1 个 changeset。组件 0 改动(死兜底量测为 0)。与在飞的 #3363(types/mobile)、#3584(CONTRIBUTING)、#3578 / #3575(runner)零文件相交,起分支时已核。


Generated by Claude Code

#3546 切片二。#3547 的守卫量出 258 个 t() 引用但十个包都没有的 key,
organization 是其中最大的一批:90 个 key、93 个调用点、7 个组件。
全部 90 个 key 补进 en.ts 新增的 organization 顶层段(与既有的 organizations
相邻但相互独立:单数是组织管理面,复数是组织选择器),九包按各自邻键的语气、
词汇与标点惯例逐条实译。en 文案与调用点内联 defaultValue 逐字节一致。
⛔ 未新增任何 defaultValue。组件未改:先量后动,93 个调用点全部裸用
useObjectTranslation,无 defaults map、无死 || 兜底。
棘轮基线精确减 90(missingKeys 253 → 163);missingPrefixes 4 条未动,
organization.invitations.status. 属模板 key 家族,是另一种修法。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercelBot commented Aug 7, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 7, 2026 3:45pm

Request Review

@yinlianghui
yinlianghui marked this pull request as ready for review August 7, 2026 15:46
@yinlianghui
yinlianghui added this pull request to the merge queueAug 7, 2026
Merged via the queue into main with commit 42ae5c6Aug 7, 2026
19 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3546-slice2-organization branch August 7, 2026 15:47
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-DQLTNuyK.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.66KB3.13KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)480.72KB105.64KB
core (index.js)2.96KB1.13KB
create-plugin (index.js)9.28KB2.98KB
data-objectstack (index.js)137.51KB35.11KB
fields (index.js)230.87KB56.83KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)26.14KB6.07KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.53KB10.71KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)44.98KB12.37KB
plugin-charts (index.js)61.04KB17.31KB
plugin-chatbot (index.js)180.09KB42.72KB
plugin-dashboard (index.js)112.03KB28.88KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)232.79KB57.42KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)112.10KB27.10KB
plugin-gantt (index.js)162.55KB39.57KB
plugin-grid (index.js)186.61KB49.34KB
plugin-kanban (index.js)48.30KB13.28KB
plugin-list (index.js)105.12KB25.48KB
plugin-map (index.js)16.81KB5.24KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)25.76KB7.33KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)19.28KB6.38KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.71KB1.34KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

Sign up for freeto 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

@yinlianghui@claude