Skip to content

fix(i18n): the drifted detail row mirrors the en pack, and the three defaults maps get a gate (#4401) - #4409

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-4401-map-pack-mirror
Aug 12, 2026
Merged

fix(i18n): the drifted detail row mirrors the en pack, and the three defaults maps get a gate (#4401)#4409
yinlianghui merged 2 commits into
mainfrom
claude/issue-4401-map-pack-mirror

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4401

Both halves of the ruling in the claim comment, implemented as ruled.

1. The pack wins — the drifted row

DETAIL_DEFAULT_TRANSLATIONS['detail.editFieldsInline'] said Edit fields inline
where the en pack says Edit fields, so InlineEditSaveBar's toggle announced two
different names for one control: the map's on a provider-less host (standalone embeds,
the preview gallery, this package's own unit tests), the pack's in the console.

Byte-identity verified by evaluating the pack and comparing codepoints, per #4402's
method — the gate's own failure message carries them:

map  = "Edit fields inline" [69,100,105,116,32,102,105,101,108,100,115,32,105,110,108,105,110,101]
pack = "Edit fields"        [69,100,105,116,32,102,105,101,108,100,115]

No pack files were touched, so the nine other locales keep their existing translations
of "Edit fields" and check:i18n-drift reports 0 changed en values.

Nothing pinned the old string: git grep "Edit fields inline" found only the map row
and the call site (InlineEditSaveBar.tsx:281, aria-label={t('detail.editFieldsInline')}).
No test pins moved.

2. Generalize the gate — the three ungated maps

New suite: packages/app-shell/src/__tests__/defaults-maps-mirror-en-pack.test.tsx,
12 cases covering all three maps.

Why app-shell is the home. It cannot live in @object-ui/i18n: every map's package
depends on that one, so importing a map back into it inverts the dependency — the exact
reason gantt-count-interpolation-4157.test.ts gives for asserting en values as
literals rather than importing plugin-gantt. app-shell is the package that depends
on all three plugins and on @object-ui/i18n, it already hosts a cross-package
parity gate (__tests__/spec-symbol-parity.test.ts), and it already imports a defaults
map through a plugin barrel (views/RecordDetailView.feedRecordScope.test.tsx
imports DETAIL_DEFAULT_TRANSLATIONS). So the maps are read, not parsed out of source
text.

LIST_DEFAULT_TRANSLATIONS and DESIGNER_DEFAULT_TRANSLATIONS are exported for it, as
DETAIL_DEFAULT_TRANSLATIONS and COLLAB_DEFAULT_TRANSLATIONS already were — this
makes the four maps consistent rather than introducing a new convention.

Shape. Per-key over the map ∩ pack intersection, never a whole-map toEqual:
#3440 could compare a whole namespace because the collaboration map is a namespace
slice, while these three borrow rows from other namespaces and are deliberate subsets
of their own.

The exceptions, graded on measurement — none of them exempts a row

claimed exception measured on this tree grade
borrowed rows — common.resizeDrawer (detail), table.search / table.rowsPerPage (list), common.cancel / back / next / close / edit / delete (designer) all 9 resolve in the en pack compared like any other row. They are pack keys; exempting them would blind the gate to 9 of 372 rows. A separate case pins that they still resolve and are still rows of the map claimed for them
detail.showEmptyRelated{,_one,_other} all three in the pack, all three already agree compared, and their key-set mirror pinned separately. Their docblock rationale (fallbackT indexes defaults[key] literally, so only the base row is reachable; the suffixed rows exist so the map's key set mirrors the packs') is a claim about keys, so it is enforced as one
a map row whose key the pack lacks 0 rows, all three maps not an allowed state. It fails, and the message names it a finding to file rather than an allow-list entry. No docblock documents a deliberate absence

Non-vacuity (#4118 family standard): each map asserts rows exist, that named
sentinel rows are present (including detail.editFieldsInline itself and #4294's
list.sortRelationalHint), and that the compared intersection is non-empty — the
last one catches a pack rename that empties the intersection while the rows stay put.

Pre-fix red — direction predicted before running

Predicted in the suite's docblock before the first run: restoring the row turns exactly
one case red, naming that key and nothing else; the list and designer maps, both
absence cases, both non-vacuity cases and the exception cases stay green.

Observed against the unfixed tree — 1 failed / 12, and the drift list has exactly
one entry:

❯ packages/app-shell/src/__tests__/defaults-maps-mirror-en-pack.test.tsx (12 tests | 1 failed)
      × every row that the pack also defines is byte-identical to it

FAIL  ... 'plugin-detail DETAIL_DEFAULT_TRANSLAT…' > every row that the pack also defines is byte-identical to it
AssertionError: expected [ Array(1) ] to deeply equal []
+ [
+   "detail.editFieldsInline
+   map  (packages/plugin-detail/src/useDetailTranslation.ts) = "Edit fields inline" [69,...,101]
+   pack (en)          = "Edit fields" [69,...,115]",
+ ]

 Test Files  1 failed (1)
      Tests  1 failed | 11 passed (12)

Reverse verification was re-run commit-then-revert (never git stash — the stack is
shared across worktrees) with the same prediction and the same result; restoring the
fix returns 12/12.

No extra drift. The card's 1-in-372 measurement reproduced exactly: rows
148 / 59 / 165, drifted 1 / 0 / 0, key-absent-from-pack 0 / 0 / 0.

3. The falsified comment

LIST_DEFAULT_TRANSLATIONS' #4294 note said "No gate compares this table to en".
This PR makes that false, so it now names the new suite — while keeping the part that
is still true (that check:i18n-keys judges inline t(key, { defaultValue }) options
and never a createSafeTranslation table; it is simply no longer the only thing
looking). All three maps' docblocks now state the byte-identity contract and point at
the gate.

Interaction with #4392, which landed mid-flight

origin/main moved during the work and was merged in; it carried #4392 (retiring
common.search from the packs) and #4394's new check:phantom-deps gate.

  • common.search: none of the three maps has a row for that key, so the
    intersection is unaffected. The gate was re-run against the post-merge pack.
  • check:phantom-deps: the new suite's three plugin imports are already declared by
    app-shell's package.json; the gate passes.

Verification

pnpm exec vitest run (the new suite, pre-fix)          1 failed | 11 passed (12)  ← predicted red
pnpm exec vitest run (the new suite, post-fix)         12 passed (12)
pnpm exec vitest run packages/plugin-detail/ packages/plugin-list/ \
    packages/plugin-designer/ packages/collaboration/ packages/app-shell/src/__tests__/
                                                       123 files, 1420 tests passed
pnpm --filter (the four touched packages) type-check   Done (both tsc passes each)
pnpm --filter (the four touched packages) lint         0 errors (2293 warnings, all pre-existing;
                                                       the new file emits none)
node scripts/check-i18n-call-site-keys.mjs             PASS
node scripts/check-i18n-en-drift.mjs                   PASS (0 en values changed — no pack edits)
node scripts/check-phantom-dependencies.mjs            PASS
node scripts/check-control-bytes.mjs                   PASS
node scripts/check-changeset-presence.mjs              PASS
node scripts/check-changeset-no-major.mjs              PASS

packages/collaboration's #3440 suite is untouched and green in the run above.
Type-check needed the dependency closure built first in a fresh worktree
(pnpm --filter '...^...' build) — the same fresh-worktree artifact trap #4402 hit, not
a defect. Control-byte self-scan beyond the gate over all seven touched files: clean.

Changeset: .changeset/map-pack-mirror-gate-4401.md, patch on the three plugin
packages (user-visible: the provider-less label loses one word). Never major, per
AGENTS.md's version-alignment rule.

Out of scope

None. No unrelated defect was tripped over; the measurement that would have produced one
(rows whose key the pack lacks) came back 0 across all three maps.


Generated by Claude Code

claude added 2 commits August 12, 2026 03:41
…maps get a gate (#4401)

`DETAIL_DEFAULT_TRANSLATIONS['detail.editFieldsInline']` said 'Edit fields
inline' where all ten packs say 'Edit fields', so InlineEditSaveBar's toggle
announced two different names depending on whether an I18nProvider was mounted.
The pack wins.

Generalizes objectui#3440's collaboration-only byte-identity test to the three
ungated maps: per-key over the map/pack intersection, not a whole-map toEqual.
LIST_ and DESIGNER_DEFAULT_TRANSLATIONS are exported for it, as DETAIL_ and
COLLAB_ already were.
@vercel

vercel Bot commented Aug 12, 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 12, 2026 3:57am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-BR3KfSp6.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
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) 489.20KB 108.43KB
core (index.js) 2.99KB 1.14KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 153.42KB 41.19KB
fields (index.js) 228.33KB 56.61KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.33KB 1.20KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.98KB 10.85KB
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) 45.23KB 12.45KB
plugin-charts (index.js) 62.18KB 17.67KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 120.57KB 31.32KB
plugin-designer (index.js) 211.16KB 42.76KB
plugin-detail (index.js) 239.03KB 59.77KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 164.14KB 39.98KB
plugin-grid (index.js) 187.99KB 49.92KB
plugin-kanban (index.js) 48.60KB 13.41KB
plugin-list (index.js) 110.21KB 26.79KB
plugin-map (index.js) 18.05KB 5.80KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.99KB 10.74KB
plugin-timeline (index.js) 26.21KB 7.52KB
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) 23.71KB 7.96KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
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 (dashboard-filter-alias.js) 6.23KB 2.74KB
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) 3.05KB 1.52KB
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 — PM 复核 (session session_017Qqyix2QcnpUC9XeYVDzx3), closes #4401.

  • Both ruled halves delivered exactly: the row is byte-identical to the pack (codepoint evidence, no pack edits, drift gate 0), and the gate landed per-key-over-intersection with the exception grading done the strong way — borrowed rows compared like any other (exempting them would have blinded 9 of 372), the showEmptyRelated rationale enforced as the key-set claim it actually is, and key-absent-from-pack a hard failure whose message says "file a finding", not "add an allow-list entry".
  • The gate-home deviation is accepted as the measured answer: packages/i18n is forbidden by dependency direction (its own test documents why at the gantt-count precedent), and app-shell is the only package that can evaluate all three maps — with the discoverability cost mitigated by all three docblocks naming the suite. The scaffolding-export deviation is consistent-with-convention, and capturing the pre-fix red WITH the scaffolding present is what keeps the red isolated to the row.
  • Pre-fix red and the post-fix reverse both landed on exactly one case naming exactly one key; the post-merge re-verification correctly absorbed [finding] common.search has zero readers repo-wide after PR #4391 — retire from the ten packs, or keep as vocabulary? #4392's pack retirement and ran green under Phantom dependency: react resolves from packages/core although core declares none — a resolution-based layering check returns the wrong answer #4394's new phantom-deps gate.
  • Systemic property recorded for future retirement cards: a pack-key retirement that forgets a defaults-map row now goes red in this gate — map rows must be deleted in the same PR. This is written into the retirement playbook from here on.

Flipping ready + arming auto-merge.


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 12, 2026 04:08
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 12, 2026
Merged via the queue into main with commit e076fd5 Aug 12, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4401-map-pack-mirror branch August 12, 2026 04:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants