From dc14b64880d670ae66f17a3303b1a37462838077 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 14:12:15 +0000 Subject: [PATCH] fix(plugin-detail): record:alert CTA action.label accepts the inline locale map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Widens the local, unexported `RecordAlertProps` CTA slot in `packages/plugin-detail/src/renderers/record-alert.tsx` from `label?: string` to `label?: string | I18nLabel` in both copies (`schema.properties.action` and the flat-compat `schema.action` mirror). The renderer already resolves `action.label` through the same inline locale-map `pickLocalized(props.action?.label, language)` call used for `title` / `body` two lines above — the declaration was narrower than the renderer's own runtime behavior, the same declaration-narrower-than-the- renderer contradiction objectui#4970 fixed for `title` / `body` in this same interface. This move was deliberately deferred by #4970 and filed as this issue. Type-only: the block's published authoring surface still declares `action` as a bare `object` with the member shape in prose only (`plugin-detail/src/index.tsx`), so there is no manifest arm to align against yet — that half stays parked on the `ComponentInput` member-shape question (PR #3795) and is out of scope here, per the issue and triage ruling. Adds a test asserting the case the type previously refused: a CTA whose `label` is an inline locale map (`{ en, 'zh-CN' }`), rendered to the resolved string for the active language via a real `I18nProvider`. A plain-string CTA label was already green before this change (existing "CTA label override" test) and cannot distinguish the widened declaration from the narrow one it replaces — reverse-verified: reverting the type widening alone reproduces `tsc` TS2322 at this test's locale-map literal (`type-check` is the gate this change is judged by), confirming the new case is what the old declaration refused. Fixes #4998. Co-authored-by: Claude --- .../record-alert-cta-label-i18n-4998.md | 20 +++++++++++ .../renderers/__tests__/record-alert.test.tsx | 33 +++++++++++++++++++ .../src/renderers/record-alert.tsx | 17 ++++++---- 3 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 .changeset/record-alert-cta-label-i18n-4998.md diff --git a/.changeset/record-alert-cta-label-i18n-4998.md b/.changeset/record-alert-cta-label-i18n-4998.md new file mode 100644 index 0000000000..c42fa2e98f --- /dev/null +++ b/.changeset/record-alert-cta-label-i18n-4998.md @@ -0,0 +1,20 @@ +--- +'@object-ui/plugin-detail': patch +--- + +`record:alert`'s renderer-local `RecordAlertProps` CTA slot (`action.label`) is +widened to `string | I18nLabel` in both copies (`properties.*` and the flat +compat mirror) in `packages/plugin-detail/src/renderers/record-alert.tsx`. + +The renderer already resolves `action.label` through the same inline-locale-map +`pickLocalized` call as `title` / `body` (`const ctaLabel = +pickLocalized(props.action?.label, language)`), so a bare `string` declaration +was narrower than the renderer's own runtime behavior — the same +declaration-narrower-than-the-renderer contradiction objectui#4970 fixed for +`title` / `body` one level up in the same interface (objectui#4998). + +Type-only: the block's published authoring surface still declares `action` as +a bare `object` with the member shape in prose only +(`plugin-detail/src/index.tsx`), so there is no manifest arm to align yet — +that half stays parked on the `ComponentInput` member-shape question (PR +#3795) and is out of scope here. diff --git a/packages/plugin-detail/src/renderers/__tests__/record-alert.test.tsx b/packages/plugin-detail/src/renderers/__tests__/record-alert.test.tsx index 5c0e85f283..3b73b71e05 100644 --- a/packages/plugin-detail/src/renderers/__tests__/record-alert.test.tsx +++ b/packages/plugin-detail/src/renderers/__tests__/record-alert.test.tsx @@ -92,6 +92,10 @@ import { RecordAlertRenderer } from '../record-alert'; // the ambient predicate scope pinned at the bottom of this file is the shipped // one, not a stand-in for it. import { PredicateScopeProvider } from '@object-ui/react'; +// Real I18nProvider (not mocked) — pins the active language so the locale-map +// case below (objectui#4998) resolves deterministically instead of riding +// whatever language another test file left on the global i18next singleton. +import { I18nProvider } from '@object-ui/i18n'; const RECORD_DEFAULTS = { recordCtx: { @@ -193,6 +197,35 @@ describe('RecordAlertRenderer', () => { expect(screen.getByTestId('alert-cta').textContent).toBe('Send again'); }); + // objectui#4998 — `action.label` accepts the inline locale map, not just a + // plain string. The renderer already resolved one before this pin existed + // (`const ctaLabel = pickLocalized(props.action?.label, language)`), so a + // plain-string CTA label is green whether or not the local `RecordAlertProps` + // type admits a map — that's the `label: 'Send again'` case just above, and + // it cannot tell the widened declaration apart from the narrow one it + // replaced. This case can: a bare `label?: string` refused a locale-map + // value at the TYPE level even though `pickLocalized` (an `unknown` input) + // already resolved it correctly at runtime, exactly the `title` / `body` + // contradiction objectui#4970 fixed one level up in the same interface. + it('resolves an inline locale-map action.label to the active-language string (objectui#4998)', () => { + render( + + + , + ); + expect(screen.getByTestId('alert-cta').textContent).toBe('Send again'); + }); + it('does NOT render a CTA when the action name fails to resolve in metadata', () => { render(