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(