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(