Skip to content

refactor(app-shell): 删除 RecordFormPage 中不可达的 i18n defaultValue 兜底 - #3516

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3469-dead-defaultvalue
Aug 7, 2026
Merged

refactor(app-shell): 删除 RecordFormPage 中不可达的 i18n defaultValue 兜底#3516
yinlianghui merged 1 commit into
mainfrom
claude/issue-3469-dead-defaultvalue

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3469

采用 issue 分诊结论的 B 向:删掉死的 defaultValue 分支,而不是把它改成和语言包一致的拼法。与 #3470/#3483 的「declared = enforced、消费端不留容忍」先例一致。

改了什么

packages/app-shell/src/views/RecordFormPage.tsx 原本在 8 处 t() 调用上挂了内联 defaultValue。逐一评估后:

key 十个语言包 处置
form.createTitle 全部定义 (默认值曾是 New ${label},包里是 Create {{object}})
form.editTitle 全部定义
form.createSuccess 全部定义
form.updateSuccess 全部定义
form.saveRecord 全部定义
common.back 全部定义
common.cancel 全部定义
form.createTargetOrg 一个包都没有,连 en 都没有 保留 — 这条兜底是真正承重的

删掉的七条,key 由 all-locales-key-parity.test.ts 永久钉住必然存在,i18next 永远解析得到包里的值,兜底分支不可能被渲染。它们真正的作用是给同一条文案带了第二处没人看管的英文拼法:form.createTitle 的默认值写的是 New ${label},包里却是 Create {{object}},同一个标题、同一个调用点、两个动词。哪天这个 key 被改名或删掉,页面标题就会从 Create Contacts 悄悄变成 New Contacts,而且没有任何测试会红。

保留的那一条已在代码里写清原委:form.createTargetOrg 在十个包里都没有定义,i18next 是真的 miss,兜底就是实际渲染出来的东西 —— 删了会把裸 key 摆到页面上。这也是该徽标在十种语言下都是英文的原因。把 key 回填进 en(parity 测试会带着其余九个包跟上)才是正解,届时在同一个改动里删掉这条兜底,新测试里有一条断言就是提醒这件事的。

行为不变的证据(以及反向验证的真实方向)

「把删掉的分支放回去看测试变红」在这个改动上从构造上就不可能成立,预测在跑之前就已写进测试文件头:key 在包里,i18next 根本不会去看 defaultValue,所以把 defaultValue 加回来,断言一条都不会动。这正是 issue 报告的那个缺陷本身 —— 一段没有任何测试能看见的死分支。所以这里如实记录三个方向的实测:

  1. 不变方向(证明行为未变 / 分支确实是死的):git checkout origin/main -- RecordFormPage.tsx 把八处 defaultValue 全部放回,新测试文件 16/16 依旧全绿
  2. 反事实 A(旧代码,静默漂移):临时把 form.createTitleen 包里删掉,旧代码渲染出 PROBE_TITLE=[New Contacts] —— 动词静默改变,没有任何测试信号。
  3. 反事实 B(新代码,响亮失败):同样删掉该 key,新代码渲染出 PROBE_TITLE=[form.createTitle](裸 key + dev missing-key 告警),且新增的覆盖率断言直接变红:form.createTitle missing from: en

反事实用的语言包临时改动是一次性探针,已还原,未进入本 PR(git status 已核验)。

新增测试

packages/app-shell/src/views/RecordFormPage.i18n.test.tsx(16 个用例),钉住删除之后真正成立的不变式:

  • 标题/toast/按钮文案渲染的就是包里的值,并且enzh 两种语言下各断言一次 —— 任何硬编码的英文默认值都无法同时满足这两条(zh 下标题是 新建Contacts)。
  • 断言 New Contacts 不在页面上。
  • 现在裸传的七个 key,逐个断言在十个包里都是字符串;少一个就红,而不是悄悄换个动词继续绿。
  • form.createTargetOrg 的两条:一条钉住它当前在十个包里都不存在(所以兜底必须留),一条钉住徽标确实渲染出 Creates in Acme Inc 而不是裸 key。

验证

  • pnpm exec vitest run packages/app-shell/src/views/RecordFormPage.i18n.test.tsx packages/i18n/src/__tests__/all-locales-key-parity.test.ts → 36 passed
  • pnpm exec vitest run packages/app-shell/src/views(消费半径清扫)→ 180 files / 1585 passed
  • pnpm exec turbo run type-check --filter=@object-ui/app-shell → 通过
  • pnpm exec eslint 两个改动文件 → 0 error
  • node scripts/check-control-bytes.mjs / check-changeset-no-major.mjs → 通过

顺带发现(未在本 PR 修改)

form.createTargetOrg 被组织租户徽标消费,却在十个语言包里全部缺失 —— 属于「组件引用了不存在的 key」这一类漂移,与本 issue 是两件事,已另行记录待 PM 分诊。


🤖 Generated with Claude Code

https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt


Generated by Claude Code

…ordFormPage

The page handed i18next an inline `defaultValue` on eight `t()` calls. Seven of
those branches were unreachable: their keys are defined in all ten locale packs
and `all-locales-key-parity.test.ts` pins that permanently, so i18next always
resolved the pack value and the fallback could never render.

What the dead fallbacks did do is carry a second, unwatched English spelling of
the same string. `form.createTitle`'s default read `New {label}` while the pack
says `Create {{object}}` — a different verb for one title at one call site. Had
the key ever been renamed or dropped, the title would have silently changed
from "Create Contacts" to "New Contacts" with every test still green. Deleting
them makes "this title has exactly one English spelling" a structural fact
rather than a coincidence, consistent with the declared=enforced precedent in
objectui#3470/#3483.

`form.createTargetOrg` is KEPT and documented: it is defined in no pack, not
even `en`, so its `defaultValue` is genuinely what renders.

Rendered copy is unchanged in every locale — verified by restoring the deleted
limbs on top of the new test file, which stays green (a dead branch cannot move
an assertion), and by a counterfactual: with `form.createTitle` removed from
`en`, the old code silently rendered "New Contacts" while the new code renders
the raw key and the new pack-coverage test fails loudly.

Fixes #3469

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercel Bot commented Aug 7, 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 7, 2026 2:06am

Request Review

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-CvL4NT47.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) 24.58KB 7.04KB
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) 479.63KB 105.43KB
core (index.js) 2.47KB 0.91KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 136.30KB 34.76KB
fields (index.js) 229.82KB 56.53KB
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) 26.14KB 6.07KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
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.05KB 1.53KB
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.09KB 42.72KB
plugin-dashboard (index.js) 112.03KB 28.88KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 232.79KB 57.42KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 111.54KB 26.97KB
plugin-gantt (index.js) 162.55KB 39.57KB
plugin-grid (index.js) 185.25KB 49.08KB
plugin-kanban (index.js) 48.03KB 13.22KB
plugin-list (index.js) 104.72KB 25.36KB
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.46KB 1.21KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
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

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

Labels

Projects

None yet

2 participants