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

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 2:06am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-CvL4NT47.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)24.58KB7.04KB
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)479.63KB105.43KB
core (index.js)2.47KB0.91KB
create-plugin (index.js)9.28KB2.98KB
data-objectstack (index.js)136.30KB34.76KB
fields (index.js)229.82KB56.53KB
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)3.26KB1.44KB
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.05KB1.53KB
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)111.54KB26.97KB
plugin-gantt (index.js)162.55KB39.57KB
plugin-grid (index.js)185.25KB49.08KB
plugin-kanban (index.js)48.03KB13.22KB
plugin-list (index.js)104.72KB25.36KB
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.46KB1.21KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)0.20KB0.18KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 7, 2026 02:13
@yinlianghui
yinlianghui added this pull request to the merge queueAug 7, 2026
Merged via the queue into main with commit c0c771cAug 7, 2026
18 of 19 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3469-dead-defaultvalue branch August 7, 2026 02:14
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 10, 2026
…locales (objectstack-ai#3526)
`form.createTargetOrg` was defined in no locale pack — not even `en` — so
i18next genuinely missed it and `RecordFormPage`'s inline `defaultValue`
rendered instead. The ADR-0105 badge therefore read English `Creates in <org>`
in all ten languages (a Chinese console creating a record on an org-walled
object showed `Creates in 某某组织`).
`all-locales-key-parity.test.ts` could not see this: it asserts every pack
defines every `en` key, so a key `en` itself lacks is outside the comparison,
and ten packs missing it identically kept parity green.
Backfill `createTargetOrg: 'Creates in {{org}}'` into `en` (the parity gate then
demands it from the other nine, each translated to its pack's existing form
tone), delete the inline default, and invert the two exception pins objectstack-ai#3516 left:
the key joins BARE_KEYS, and the render assertion checks the badge against the
pack copy in both `en` and `zh`.
Fixesobjectstack-ai#3517
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 10, 2026
…veness ratchet (objectstack-ai#3601) (objectstack-ai#3623)
The deny-list guards objectstack-ai#2561 decision (a): spec/ui zod validators are not part
of @object-ui/types' public surface. A row only asserts something while the
spec still publishes the name — once upstream retires it, nothing can
re-export it and the row passes as a tautology.
37 of 82 rows had rotted that way: objectstack#4988 / PR #5321 (32, the five
interaction-config modules), objectstack-ai#4610 (2), objectstack-ai#3494 / PR objectstack-ai#3516 (2), objectstack-ai#3896 (1).
They are deleted; the 45 rows that still guard a live spec export are
untouched.
The new ratchet asserts every remaining row resolves to a real
`@objectstack/spec/ui` export (with the Spec-prefix alias fallback the list's
own doc comment describes, load-bearing for 16 of the 45), so the next
upstream retirement fails here by name instead of waiting for an audit. Same
technique as PR objectstack-ai#3568's `$icontains` exclusion ratchet.
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 10, 2026
…t path filtering into the jobs (objectstack-ai#3523 steps 1-2) (objectstack-ai#3722)
* ci(step 1): subscribe ci/lint/control-bytes/docs-links to merge_group (objectstack-ai#3523)
Step 1 of objectui#3523, on its own commit as the issue's ruling requires:
this is a pure addition and changes nothing about which pull requests or
pushes run CI.
The merge queue is enforced on this repository by a ruleset (objectstack-ai#3243 measured a
direct push to `main` returning 405 `Changes must be made through the merge
queue`), but not one of the 22 workflows subscribed `merge_group` — repo-wide
`event=merge_group` runs stood at total_count = 0. A queue nothing subscribes
to can only carry an empty required-check set, so it rebuilt each PR on the
current `main` and let it through without validating anything. On 2026-08-07
that was cashed in: objectstack-ai#3503 / objectstack-ai#3510 / objectstack-ai#3516 merged between 02:11Z and 02:15Z
with `Type Check` at conclusion=failure, on a `main` poisoned by objectstack-ai#3498, and
objectstack-ai#3505 had to hot-fix it.
The four workflows named by the issue now carry the trigger. It is spelled
`merge_group: types: [checks_requested]`; `checks_requested` is the only
activity type GitHub defines for this event today, so this is equivalent to
objectstack's bare `merge_group:` and merely refuses to inherit a second type
added later.
Two `ci.yml` jobs additionally needed to be told the third event exists —
both changes are no-ops for `pull_request` and `push`:
- `test` moves from `if: github.event_name == 'pull_request'` to
`!= 'push'`, so a queue build actually runs the suite instead of skipping
every shard.
- `docs`'s `should_run` treated anything that is not `push` as a pull request
and diffed `github.event.pull_request.*`, which is null on a queue build —
an empty revision range, i.e. the site build silently skipped on the last
check before `main`.
Verified rather than assumed: `concurrency` needs no merge-queue case. On
`merge_group` the group expression falls back to `github.ref`, which is the
queue's own generation — measured on objectstack, whose 3552 queue runs report
head_branch `gh-readonly-queue/main/pr-6594-251e888a…`. That can collide with
neither a PR group (a bare number) nor a push group (`refs/heads/main`).
Refs objectstack-ai#3523
* ci(step 2): move ci/lint path filtering out of the PR trigger into the jobs (objectstack-ai#3523)
Step 2 of objectui#3523, deliberately a separate commit from step 1: step 1
adds runs that did not exist, this one changes which pull requests start CI,
and mixing them would make that impossible to review apart.
`on.pull_request.paths-ignore` skips the WHOLE workflow when every changed
file matches, and GitHub has no per-job path filter. A docs-only or
changeset-only PR therefore started neither `ci.yml` nor `lint.yml` — objectstack-ai#3509
measured zero check runs from them. A check that is never *created* does not
fail a required-status-check rule, it leaves the pull request pending; inside
the merge queue it fails on the ruleset's 60-minute status-check timeout. So
none of `Lint`, `Type Check`, `Test (shard N/4)`, `Build & E2E` or
`Changeset Fixed Group Check` could be made required while the filter lived
on the trigger — which is why the queue's required set was empty to begin
with.
The filter is not deleted, it moves. `type-check`, `test`, `e2e` (ci.yml) and
`lint` (lint.yml) each open with a `Decide whether this change needs a full
run` step, and every step after it carries
`if: steps.relevant.outputs.should_run == 'true'`. The job always runs and
always reports; the paths decide only whether it does any work. That is the
shape `ci.yml`'s `docs` job has used since objectstack-ai#3450 — not a new mechanism — and
the exclusion lists are byte-for-byte the `paths-ignore` they replace, so
which PRs pay for a full run is exactly as before.
Two deliberate narrowings, both stated so they can be argued with:
- `paths-ignore` stays on the `push` trigger. Branch protection and the merge
queue judge pull requests and queue builds, never pushes to `main`, so the
push lane gains nothing from losing it and would cost a full CI run on
every docs merge. It also gives the ignore list one authored home, which
`merge-queue-reporting.test.ts` then pins the in-job copies against.
- `changeset-check` is not gated. It is a checkout and one `node` call, so
short-circuiting it would cost more in complexity than it saves.
The gate fails OPEN: an unresolvable diff runs everything rather than
reporting green having built nothing (objectstack#4928's filter contract).
Measured both ways against a fixture repository — as shipped an unreachable
base sha yields should_run=true; rewritten with the `|| echo ""` spelling
`ci.yml`'s older `docs` gate still uses, the same input yields
should_run=false, i.e. a silent full skip. That pre-existing `docs` gate is
reported separately rather than changed here.
`scripts/__tests__/merge-queue-reporting.test.ts` pins both steps; against
the pre-change workflows 9 of its 10 assertions go red, each naming its own
regression. `lint-workflow.test.ts`'s TypeScript-exclusion tripwire now reads
the in-job list as well as the trigger, which is where that list now lives.
Refs objectstack-ai#3523
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@yinlianghui@claude