Skip to content

fix(i18n): six bare pack keys get their consumers' defaults rows (#4396) - #4402

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-4396-six-bare-keys
Aug 12, 2026
Merged

fix(i18n): six bare pack keys get their consumers' defaults rows (#4396)#4402
yinlianghui merged 2 commits into
mainfrom
claude/issue-4396-six-bare-keys

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4396

The six keys PR #4372's census left behind — read through createSafeTranslation with neither a row in their hook's defaults table nor an inline defaultValue at the call site, the only two fallbacks that path has. fallbackT's last resort is the key itself, so a provider-less host rendered the raw key string to the user.

All six were fixable in this PR's surface: one row each in the consumer hook's defaults table, byte-identical to the en pack value. No pack edits, no new keys, no call-site changes.

Per-key outcome

key consumer hook outcome row added to
detail.saving useDetailTranslation fixed plugin-detail/src/useDetailTranslation.ts
list.resetSortToDefault useListViewTranslation fixed plugin-list/src/ListView.tsx (LIST_DEFAULT_TRANSLATIONS)
appDesigner.widgetProperties useDesignerTranslation fixed plugin-designer/src/hooks/useDesignerTranslation.ts
appDesigner.addWidget useDesignerTranslation fixed same
appDesigner.modeEdit useDesignerTranslation fixed same
common.delete useDesignerTranslation fixed — attribution resolved, see below same

Every census attribution was re-verified against this branch's base rather than trusted: each key still reached through the named hook, still absent from that hook's table, still bare at the call site. None were stale.

common.delete — the one the census did not attribute

The census recorded it only as "outside table, no default", and the claim comment ruled it report-don't-fix if its call site landed in a held surface (notably packages/components, held by #4386). It does not. Its two call sites are

  • plugin-designer/src/FieldDesigner.tsx:413
  • plugin-designer/src/ObjectManager.tsx:269

both confirmLabel={t('common.delete')}, and both resolve through useDesignerTranslation. So the row belongs in plugin-designer's table, beside the common.cancel / common.back / common.next / common.close / common.edit entries that map already borrows from the shared namespace. Nothing in packages/components was touched.

packages/collaboration has its own common.delete row already (useCollaborationTranslation.ts:110) and is untouched — a separate hook with a separate table.

Byte-identity

Each value was resolved by evaluating the en pack, not by reading the source near a grep hit. That mattered: en.dashboard.addWidget is 'Add widget' while en.appDesigner.addWidget is 'Add Widget', and the two sit 99 lines apart in the same file. Checked codepoint by codepoint — detail.saving ends in U+2026, not three dots:

OK   detail.saving                  map="Saving…" pack="Saving…" cp=83,97,118,105,110,103,8230
OK   list.resetSortToDefault        map="Reset to view default" pack="Reset to view default"
OK   appDesigner.widgetProperties   map="Widget Properties" pack="Widget Properties"
OK   appDesigner.addWidget          map="Add Widget" pack="Add Widget"
OK   appDesigner.modeEdit           map="Edit" pack="Edit"
OK   common.delete                  map="Delete" pack="Delete"
ALL SIX BYTE-IDENTICAL

On the #3440 enforcement precedent the card cites: the only mechanical map-vs-pack gate in the repo today is presence-avatars-i18n.test.tsx's "the collaboration defaults map mirrors the en pack", and it covers the collaboration map only. No equivalent gate exists for these three maps — check:i18n-keys judges inline t(key, { defaultValue }) options, never a createSafeTranslation table, as LIST_DEFAULT_TRANSLATIONS's own #4294 comment already records. No whole-map equality test was added here, because one would land red for a reason outside this card. Measured across all three maps, source rows against the evaluated pack:

plugin-detail  DETAIL_DEFAULT_TRANSLATIONS    rows 148  drifted 1  key-absent-from-pack 0
plugin-list    LIST_DEFAULT_TRANSLATIONS      rows  59  drifted 0  key-absent-from-pack 0
plugin-designer DESIGNER_DEFAULT_TRANSLATIONS rows 165  drifted 0  key-absent-from-pack 0

One drifted row in 372 — detail.editFieldsInline, 'Edit fields inline' in the map against 'Edit fields' in the pack. Filed as #4401 rather than fixed here: which side is correct is a copy decision, and generalizing the gate has to allow the deliberate exceptions these maps carry.

Tests — red-first, one file per touched package

Three new files, each a dedicated file that never mounts I18nProvider. That is the local convention and it is load-bearing: createI18n calls instance.use(initReactI18next), which registers the instance as react-i18next's module-global default and survives unmount and cleanup(), so one provider mount anywhere in a file makes every later "no provider" render in that file resolve against it — a green file asserting nothing.

  • plugin-detail/src/__tests__/InlineEditSaveBar.savingNoProviderFallback.test.tsx
  • plugin-list/src/__tests__/ListView.resetSortToDefaultNoProviderFallback.test.tsx
  • plugin-designer/src/__tests__/bareKeysNoProviderFallback.test.tsx

Each case pairs a positive assertion with a negative one, and each file carries a non-vacuity assertion on a key already in the table (detail.cancel, list.sortRecords, common.cancel) so the "no raw key" sweeps are facts about the keys rather than about an empty DOM.

Pre-fix red evidence

Reverse verification by commit-then-revert: the three map files restored to the fork point, the three test files kept. Predicted before running — all 9 red, each rendering its raw key. Observed: 9 failed / 9.

 Test Files  3 failed (3)
      Tests  9 failed (9)

All six keys directly witnessed in the failure output:

appDesigner.addWidget                                       (rendered in the toolbar label)
appDesigner.widgetProperties                                (rendered as the inspector heading)
AssertionError: expected 'appDesigner.modeEdit' to be 'Edit'
common.delete                                               (rendered on the destructive confirm)
Unable to find an element with the text: Saving…
  → "edit-enteredit-savingCanceldetail.saving"
AssertionError: expected 'list.resetSortToDefault' to be 'Reset to view default'
  → "FilterGroupSort22 recordsSort Recordslist.resetSortToDefaultSort by…"

Restoring the three files returns 9/9 green.

Verification

pnpm exec vitest run packages/plugin-detail/ packages/plugin-list/ packages/plugin-designer/
                                              116 files, 1308 tests passed
pnpm exec vitest run (the three new files)    3 files, 9 tests passed
pnpm --filter @object-ui/plugin-detail --filter @object-ui/plugin-list \
     --filter @object-ui/plugin-designer type-check    Done (both tsc passes each)
pnpm --filter (same three) lint                        0 errors (warnings pre-existing)
node scripts/check-i18n-call-site-keys.mjs             PASS
node scripts/check-i18n-en-drift.mjs                   PASS
node scripts/check-control-bytes.mjs                   PASS
node scripts/check-changeset-presence.mjs              PASS
node scripts/check-changeset-no-major.mjs              PASS

Type-check first failed with Cannot find module '@object-ui/components' — a fresh worktree with unbuilt dependency dists, not a defect. pnpm --filter '@object-ui/plugin-detail^...' … build first, then clean.

Control-byte self-scan beyond the gate over all seven touched files: clean.

Changeset: .changeset/six-bare-keys-defaults-4396.md, patch on the three packages (user-visible: strings that rendered as raw keys now render English). Never major, per AGENTS.md's version-alignment rule.

origin/main moved three commits during the work and was merged in; no overlap with these three packages. One of those, #4397, put common.select into packages/components' own table — the adjacent half of the same family, which is consistent with common.delete belonging to plugin-designer rather than to components.

Out of scope

#4401detail.editFieldsInline renders Edit fields inline on a provider-less host and Edit fields in the console, because the map row and the pack disagree. The only such row in these three maps (1 of 372, measured above), and no gate can see it. Filed unassigned for triage; this PR adds six rows and deliberately does not touch existing ones.


Generated by Claude Code

claude added 2 commits August 12, 2026 02:10
`detail.saving`, `list.resetSortToDefault`, `appDesigner.widgetProperties`,
`appDesigner.addWidget`, `appDesigner.modeEdit` and `common.delete` were read
through `createSafeTranslation` with neither a row in their hook's defaults
table nor an inline `defaultValue` at the call site — the only two fallbacks
that path has — so a provider-less host rendered the raw key string.

Each key now has a row in its consumer hook's defaults table, byte-identical
to the `en` pack value. No pack edits, no new keys, no call-site changes.

`common.delete` was the one key PR #4372's census left unattributed; both of
its call sites (`FieldDesigner`, `ObjectManager`) resolve through
`useDesignerTranslation`, so the row lands in plugin-designer.
@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 2:47am

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-EEmbR97r.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.36KB 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) 121.56KB 31.63KB
plugin-designer (index.js) 211.06KB 42.70KB
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.18KB 26.76KB
plugin-map (index.js) 18.05KB 5.80KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 42.27KB 11.36KB
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 #4396.

Flipping ready + arming auto-merge.


Generated by Claude Code

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

Projects

None yet

2 participants