Skip to content

fix(i18n): resolve an action's confirmText/successMessage from the same bundle entry as its label (#4265) - #4313

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-4265-confirmtext-translation
Aug 11, 2026
Merged

fix(i18n): resolve an action's confirmText/successMessage from the same bundle entry as its label (#4265)#4313
yinlianghui merged 2 commits into
mainfrom
claude/issue-4265-confirmtext-translation

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#4265

The measurement first

useObjectLabel() has always exposed a resolver for all three keys a
TranslationBundle action entry carries — actionLabel / actionConfirm /
actionSuccess, one resolve() and one key convention behind all three
({ns}.objects.{obj}._actions.{name}.{label|confirmText|successMessage}).
The channel was never missing. What had drifted was the call sites:

SurfacelabelconfirmText / successMessage
RecordDetailView record headerresolvedresolved
ObjectView toolbar + row kebabresolvedresolved
DeclaredActionsBarresolvedresolved
page:header actions (authored record pages)resolvedraw
record:quick_actionsresolvedraw
related-list row menuresolvedraw

The three bottom rows are the card's "same entry, two fates": the button
picked up the bundle, the confirm dialog and the toast got the authored
English. page:header is the surface an AUTHORED record page uses — its
schema.actions reach the renderer straight from page metadata with no host
to pre-localize them — and record:quick_actions resolves its defs by name
out of the object's own metadata, which is where the card's
src/actions/lead.actions.ts text comes from.

The confirm plumbing named in triage (useObjectActions.ts:28
views/ActionConfirmDialog.tsx:23) turned out to be a faithful carrier: it
passes through whatever string the runner hands it. The defect was upstream of
it, at the point where the def is built.

The fix

One resolver, not an N+1th dialect. New useActionTextLocalizer() in
@object-ui/react is a single application of the existing
useObjectLabel() resolvers over the three keys — so a surface can no longer
localize one and forget the others; there is one function to call and it can
only do all three. Every action-rendering surface now calls it, including the
three that were already correct, and RelatedRecordActionsBridge loses its
actionLabel prop — an injection point that could only ever carry the label.

Pinned semantics (unchanged fallback contract):

  • no bundle entry, or an entry lacking a key ⇒ the authored text renders;
  • a nameless action has no _actions key of its own, so its literals pass through;
  • confirmText / successMessage are resolved only when the action declares
    them
    — a bundle must not be able to bolt a confirmation gate or a toast
    onto an action whose metadata never asked for one;
  • label is reduced through pickLocalized first, since an authored label
    may be an I18nLabel map since rc.6.

Reverse verification — prediction stated before running

Reverting only the two consumer surfaces (helper and tests kept) was predicted
to turn the four translated-confirm/toast pins RED while both no-bundle
controls stayed GREEN. That is exactly what happened — 4 failed, 3 passed:

AssertionError: expected 'Are you sure you want to convert this…' to be '确认要转化此线索吗?'
AssertionError: expected 'Lead converted.' to be '线索转化成功!'
Test Files 2 failed (2)
Tests 4 failed | 3 passed (7)

The detail that makes this the card's bug rather than a neighbouring one: on
the reverted code the buttons were still found by their Chinese name
(findByRole({ name: '转化线索' }) never failed) while the confirm body came
back English. Label resolved, confirmText did not — one entry, two fates.

Tests

New pins run the real I18nProvider (real bundle) and the real ActionRunner,
asserting what the confirm and toast handlers actually receive:

  • packages/react/src/hooks/__tests__/useActionTextLocalizer.test.tsx — the
    three keys of one entry; per-key fallback; no-entry control; "does not invent
    a confirmText"; nameless action; I18nLabel collapse.
  • packages/components/src/__tests__/page-header-action-i18n.test.tsx
  • packages/plugin-detail/src/renderers/__tests__/record-quick-actions.actionText-i18n.test.tsx

One existing fixture was re-spelled rather than deleted:
page-header-actions.test.tsx "dispatches unchanged outside a record context"
asserted toBe(action) — reference identity. Every header action now goes
through the localizer, so both branches dispatch a localized copy, which is
what the record branch already did deliberately (a fresh object keeps the
runner's in-place params merge off the authored node). The case's real content
— no _rowRecord stash without a record — is unchanged, and it gained an
assertion that the authored node is not mutated.

Verification run

  • pnpm exec vitest run packages/react packages/components packages/plugin-detail packages/app-shell
    5618 passed, 1 skipped, 575 files (the single failure in the first pass
    was the identity fixture above; re-spelled and green).
  • type-check (both tsc commands each) green on @object-ui/react,
    @object-ui/components, @object-ui/plugin-detail, @object-ui/app-shell.
  • Downstream consumer sweeppnpm --filter '...@object-ui/react' --filter '...@object-ui/components' type-check, i.e. the PREFIX/consumer direction,
    33 downstream packages including apps/console and the examples: green after
    a full build closure. (The first two passes failed only on unbuilt dist/
    for packages this PR does not touch.)
  • Cross-package type change reverse-verified: re-adding the removed
    actionLabel prop to the bridge mount goes red with
    TS2322: … not assignable to type 'IntrinsicAttributes & RelatedRecordActionsBridgeProps', then restored — so the narrowed props type
    is genuinely being read, not a cached .d.ts.
  • Gates on this surface: check:i18n-keys, check:i18n-drift,
    check:action-forward-parity, check:control-bytes — all green.
  • lint on the four packages: 0 errors (warnings are the repo baseline).

Changeset: patch for the four packages (never major, per the fixed-group
rule).


Generated by Claude Code

…me bundle entry as its label (#4265)
A TranslationBundle entry for an action carries three keys under one
`_actions.<name>` node — label / confirmText / successMessage — and
`useObjectLabel()` has always exposed a resolver for each (one `resolve()`
and one key convention behind all three). What drifted was the CALL SITES:
`page:header` (authored record pages), `record:quick_actions` and the
related-list row menu resolved the button `label` only and dispatched the
authored `confirmText` / `successMessage` untouched. One bundle entry met
two fates — the button rendered the translation, the confirm dialog
rendered the authored English literal.
Introduce `useActionTextLocalizer()` in `@object-ui/react`: one application
of the existing resolvers over the three keys, so a surface can no longer
localize one of them and forget the others. Converge every
action-rendering surface onto it, including the three that were already
correct (RecordDetailView / ObjectView / DeclaredActionsBar), and drop
RelatedRecordActionsBridge's `actionLabel` prop — an injection point that
could only ever carry the label.
Fallback semantics unchanged: no bundle entry, or an entry lacking a key,
renders the authored text; a bundle cannot introduce a confirmText or
successMessage the metadata never declared.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
…reference-equal (#4265)
Every header action now goes through the shared action-text localizer before
it is rendered or dispatched, so what reaches the runner is a localized COPY
on both branches — which is what the record-context branch already did on
purpose (a fresh object keeps the runner's in-place collected-params merge off
the authored schema node). The case is about the SHAPE being untouched outside
a record context; that is carried by the `params` pin (no `_rowRecord` stash),
plus a new assertion that the authored node itself is not mutated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercelBot commented Aug 11, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 11, 2026 1:39pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)29.6 KB350 KB
Entry fileindex-BQ38tcSW.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.88KB3.25KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
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)489.05KB108.42KB
core (index.js)3.04KB1.15KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)150.04KB39.79KB
fields (index.js)228.45KB56.62KB
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)16.38KB5.47KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.98KB10.85KB
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)45.23KB12.45KB
plugin-charts (index.js)61.73KB17.54KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)121.07KB31.39KB
plugin-designer (index.js)210.91KB42.67KB
plugin-detail (index.js)238.95KB59.76KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.58KB27.68KB
plugin-gantt (index.js)164.14KB39.98KB
plugin-grid (index.js)187.97KB49.90KB
plugin-kanban (index.js)48.60KB13.41KB
plugin-list (index.js)109.93KB26.65KB
plugin-map (index.js)17.00KB5.32KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.60KB10.58KB
plugin-timeline (index.js)26.21KB7.52KB
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)23.71KB7.96KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.23KB0.66KB
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 (dashboard-filter-alias.js)6.23KB2.74KB
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)3.05KB1.52KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 11, 2026 13:51
@yinlianghui
yinlianghui added this pull request to the merge queueAug 11, 2026
Merged via the queue into main with commit ceccdcfAug 11, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4265-confirmtext-translation branch August 11, 2026 13:51
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Action confirm dialog ignores the bundle's translated confirmText — label from the SAME entry is applied, confirmText is not

2 participants

@yinlianghui@claude