Files
suaveplan/openspec/archive/2026-05-10-react-i18n-plugin-registry-and-ui-primitives/proposal.md
2026-05-27 23:29:53 +01:00

29 KiB

React i18n — plugin registry, formatter primitives, and first-class react-ui i18n surface

Packages: @suaveplan/react-i18n, @suaveplan/react-ui Status: PROPOSED Type: feature + extensibility refactor

Why

@suaveplan/react-i18n shipped 2026-05-05 as a complete React binding for @suaveplan/i18n — <TranslationProvider>, 12 hooks, four components (<Trans>, <LanguageSwitcher>, <I18nSuspense>, <MissingKeyOverlay>), a 9-detector chain, and the branded TranslationKey contract that downstream react-ui / react-forms augment. It works, and downstream sweeps stay green.

It is monolithic in two specific ways that block the next wave of consumer features:

  1. Detector chain is hard-wired. defaultDetectorChain() builds the same fixed 9-element array every call (detector/default-chain/default-chain.ts). <DetectorChainProvider> lets a consumer swap the entire array, but there is no surface for a third-party plugin to insert a single detector without re-authoring the chain. That means a future react-i18n-detector-cookie-signed or a consumer-owned A/B-bucket detector cannot ship as a drop-in package.
  2. Formatter access is ad-hoc. Each hook (useNumber, useDate, useCurrency, useList, useRelativeTime, etc.) imports its own per-formatter package directly. There is no central formatter registry, no way to register a project-specific formatter (e.g. a domain-specific useDuration, useTemperature, or useFiscalQuarter) without forking the package, and no overlay mechanism for swapping the default Intl.NumberFormat-backed implementation with a custom one (BigDecimal currency, locale-tagged unit formatter, etc.).

Separately, @suaveplan/react-ui already consumes react-i18n through t() calls in Pagination, Calendar, DataTable, ErrorBoundary, and others — and ships pre-built bundles for en-GB, zh-CN, ar-AE, cy-GB, ru-RU (react-ui/src/locales/). What it does not ship is first-class formatted-value primitives. Consumers who want a number, date, currency, plural, or relative-time inside a button label, a <Card> body, a <DataTable> cell, or a <Toast> description currently call the bare hook (useNumber, useDate, …) and inline-format the value, with no shared component, no consistent fallback story, no built-in <Suspense> boundary, and no built-in dev-mode missing-key surface around formatted values. Components with locale-sensitive behavior (RTL slide direction on <Drawer> / <Sheet>, locale-bound numeric formatters on <DataTable> columns, locale-aware decimal/grouping on <Input type="number">) are likewise un-wired today.

This change closes both gaps in one atomic landing — the registry abstraction (react-i18n) AND the consumer-facing primitives (react-ui) ship together so downstream packages can adopt the new surface without two waves of churn.

What changes

@suaveplan/react-i18n

Plugin registry — single substrate for detectors, formatters, and post-processors

  • ADDED: src/registry/registry.ts — ReactI18nRegistry interface with three surfaces:
    • registerDetector(plugin: DetectorPlugin): Disposable
    • registerFormatter<TArgs, TResult>(plugin: FormatterPlugin<TArgs, TResult>): Disposable
    • registerPostProcessor(plugin: PostProcessorPlugin): Disposable
    • Each registration is reversible (Disposable), idempotent on duplicate plugin.id, and validated against a Zod schema at registration time. Duplicate ids throw REACT_I18N.PLUGIN_DUPLICATE_ID.
  • ADDED: src/registry/registry-context.tsx — <RegistryProvider> wraps <TranslationProvider> (or composes with it) and injects a ReactI18nRegistry into context.
  • ADDED: src/registry/use-registry.ts — useReactI18nRegistry() returns the active registry from context, throws REACT_I18N.REGISTRY_NOT_PROVIDED outside a provider.
  • ADDED: subpath export ./registry and per-plugin-type exports (./registry/detectors, ./registry/formatters, ./registry/post-processors).

Detector plugins — chain becomes registry-driven

  • MODIFIED: defaultDetectorChain() consults the registry for any registered DetectorPlugin whose priority falls between two adjacent built-in detectors. Built-in detectors keep their current order; registered plugins splice in based on priority (numeric, lower = earlier) with deterministic tie-break by id.
  • ADDED: DetectorPlugin interface — { id: string; priority: number; environment: 'browser' | 'server' | 'both'; detect: LocaleDetector } (re-uses LocaleDetector shape from @suaveplan/i18n-detector).
  • ADDED: error code REACT_I18N.DETECTOR_PLUGIN_DUPLICATE_ID, REACT_I18N.DETECTOR_PLUGIN_INVALID.

Formatter plugins — pluggable useFormatter(id, args) surface

  • ADDED: src/registry/formatters/formatter-plugin.types.ts — FormatterPlugin<TArgs, TResult> interface: { id: string; format: (locale: string, args: TArgs) => TResult; schema?: ZodSchema<TArgs>; defaultOptions?: TArgs }.
  • ADDED: src/hooks/use-formatter/use-formatter.ts — useFormatter<TArgs, TResult>(id: string, args: TArgs): TResult. Resolves the plugin from registry by id, calls plugin.format(locale, args), memoizes by (locale, id, args). Throws REACT_I18N.FORMATTER_NOT_REGISTERED for unknown ids.
  • MODIFIED: existing per-type formatter hooks (useNumber, useDate, useCurrency, useList, useRelativeTime, usePlural) keep their public signatures but internally delegate to the registry-backed useFormatter with built-in plugin ids ('number', 'date', 'currency', 'list', 'relative-time', 'plural'). Callers see no API change; the implementation is now overridable by registering a plugin with the same id (last-write-wins on priority).
  • ADDED: built-in formatter plugins for the six existing hooks live in src/registry/formatters/built-in/ and are auto-registered by <RegistryProvider> unless disableBuiltInFormatters is set.

Post-processor plugins — message pipeline overlay

  • ADDED: PostProcessorPlugin interface — { id: string; priority: number; process(message: string, ctx: PostProcessorContext): string }. Wraps the message AFTER ICU interpolation but BEFORE rendering. Use cases: pseudo-localization in dev, redaction overlays, custom debug markers.
  • The pipeline runs registered post-processors in priority order. Order is deterministic for identical priorities (lexicographic by id).

Schema + observability + errors

  • ADDED: Zod schemas at every public registration boundary (DetectorPluginSchema, FormatterPluginSchema, PostProcessorPluginSchema). Invalid plugins throw REACT_I18N.PLUGIN_INVALID with the failed Zod issues attached.
  • ADDED: react_i18n.registry.plugins_total{kind,outcome} counter, react_i18n.formatter.duration_ms{id} histogram, react_i18n.formatter.errors_total{id,code} counter.
  • ADDED: error codes REACT_I18N.PLUGIN_DUPLICATE_ID, REACT_I18N.PLUGIN_INVALID, REACT_I18N.REGISTRY_NOT_PROVIDED, REACT_I18N.FORMATTER_NOT_REGISTERED, REACT_I18N.DETECTOR_PLUGIN_DUPLICATE_ID, REACT_I18N.DETECTOR_PLUGIN_INVALID.

Locale-adapter plugin slot — for third-party date / number libraries

  • ADDED: LocaleAdapterPlugin<TLocale> — a fourth plugin kind. Registers a (localeString: string) => TLocale resolver under a stable id (e.g. "date-fns", "luxon", "temporal"). Used by primitives that need a non-Intl locale object (the canonical case is react-day-picker's locale prop, which wants a date-fns Locale). Consumers swap the date library by registering a different adapter under the same id, no fork required. Built-in adapter is date-fns (loaded only when used — dynamic import inside the adapter to keep the universal-tier surface clean).
  • ADDED: useLocaleAdapter<TLocale>(id: string): TLocale hook. Throws REACT_I18N.LOCALE_ADAPTER_NOT_REGISTERED when no adapter matches.

Backwards compatibility

  • All existing public exports keep their signatures. useNumber(value, options) / useDate(...) / <Trans> / <LanguageSwitcher> / detectors all continue to work without a registry — when no <RegistryProvider> is mounted, hooks fall back to the current direct-import code path so no consumer change is required to upgrade.
  • <TranslationProvider> is unchanged. <RegistryProvider> is a NEW, additive layer that callers opt into when they want plugin support. Recommended composition: <RegistryProvider><TranslationProvider>...</TranslationProvider></RegistryProvider>.

@suaveplan/react-ui — first-class i18n surface (new capability react-ui-i18n)

Third-party library bridges — single source of truth for locale + direction

The biggest flexibility risk in a shadcn-derived component library is that locale + direction state lives in N third-party providers (Radix, vaul, sonner, react-day-picker, tanstack/react-table) and can drift independently. This change centralizes propagation so every third-party context picks up the active i18n state automatically:

  • ADDED: <ReactUiI18nBridge> — a single mounting point that lives inside (or composes with) <ReactUiProvider>. It reads useLocale() + useDirection() and:
    • Mounts @radix-ui/react-direction's <DirectionProvider dir={…}> so EVERY Radix primitive (popover, dropdown, dialog, tooltip, select, menubar, context-menu, hover-card, scroll-area, tabs, toggle-group, …) gets RTL state without per-component wiring.
    • Sets dir on <Toaster> from sonner so toasts pick up direction.
    • Exposes the active locale to a LocaleContext consumed by <DataTable>'s default sort/filter functions (Intl.Collator-backed) and by the <Calendar> locale prop.
    • Updates the document <html lang> and <html dir> attributes when the active locale changes (opt-out via prop for SSR / nested-app scenarios).
  • ADDED: locale-aware default sortingFn and filterFn for @tanstack/react-table exposed as localeAwareSortingFn(locale) / localeAwareFilterFn(locale) from react-ui/i18n/tanstack-bridge. Consumers opt in per column or globally; ColumnDef.formatter: shortcut auto-installs them for the column it controls.

This bridge is also what makes future third-party additions plug-and-play: a new TanStack lib (@tanstack/react-virtual, @tanstack/router) just consumes the same LocaleContext / DirectionContext exposed from the bridge, no second integration point.

New formatted-value primitives — a <FormattedX/> component per formatter hook

  • ADDED: src/i18n/formatted-number/formatted-number.tsx — <FormattedNumber value={n} options? as? /> renders via useNumber. as defaults to <span>, accepts any HTML element or React component. options matches the underlying useNumber options shape exactly.
  • ADDED: src/i18n/formatted-currency/formatted-currency.tsx — <FormattedCurrency amount currency options? as? />.
  • ADDED: src/i18n/formatted-date/formatted-date.tsx — <FormattedDate value options? as? />. Accepts Date | string | number.
  • ADDED: src/i18n/formatted-list/formatted-list.tsx — <FormattedList items options? as? />.
  • ADDED: src/i18n/formatted-relative-time/formatted-relative-time.tsx — <FormattedRelativeTime value unit? options? as? /> with an auto mode that re-renders on a configurable interval (refreshIntervalMs, default 60_000).
  • ADDED: src/i18n/plural/plural.tsx — <Plural value one few many other zero? two? /> selects a child by CLDR plural rule for the active locale via usePlural.
  • ADDED: src/i18n/formatted-message/formatted-message.tsx — alias of <Trans> re-exported from react-ui so consumers do not need a second import to render translated text. Brand-accepts TranslationKey<KeyPath> so the type-safe key contract carries through.

Each primitive ships with a sibling .md, a Vitest test (with WSL2-safe pool: forks), and a .stories.tsx (default + playground + variants) per the genesis story-required rule.

Locale-aware behavior wiring on existing components

  • MODIFIED: <Drawer> (src/components/drawer/drawer.tsx) — when direction is 'inline-start' / 'inline-end', the underlying vaul direction prop resolves to 'left' / 'right' based on useDirection(). Existing direction='left' | 'right' | 'top' | 'bottom' continues to work unchanged.
  • MODIFIED: <Sheet> (src/components/sheet/sheet.tsx) — adds 'inline-start' / 'inline-end' to the side union; same useDirection-based resolution. RTL locales render an inline-start Sheet from the right edge, LTR from the left.
  • MODIFIED: <DataTable> (src/components/data-table/data-table.tsx) — ColumnDef<TData> accepts an optional formatter?: { kind: 'number' | 'currency' | 'date' | 'relative-time'; options?: ... } shape. When set, the cell renderer wraps the raw value in the matching <FormattedX/> primitive. Cells with explicit cell: overrides win; formatter: is a shortcut for the common case.
  • MODIFIED: <Pagination> pagination.tsx — page-number rendering already uses t() via the react-ui.pagination.* keys; no surface change. The MOD here is upgrading the PaginationStatus "Page X of Y" line to render <FormattedNumber/> so locales using non-Latin numerals (Arabic-Indic, Devanagari) display native digits when the active locale's numberingSystem differs from latn.
  • MODIFIED: <Calendar> calendar.tsx — passes the useLocale() value into react-day-picker's locale prop, resolved through useLocaleAdapter("date-fns"). The active date-library adapter (date-fns by default; luxon/Temporal/day.js if a consumer registered a different LocaleAdapterPlugin with id "date-fns") provides the resolver. Today the chassis only translates the nav-button aria-labels; this MOD makes weekday and month names follow the active locale and is library-swappable without a fork.

Numeric input — locale-aware decimal/grouping

  • ADDED: src/components/input/input-number/input-number.tsx — a thin <Input type="number"> wrapper that:
    • Parses incoming text using the active locale's decimal + grouping separators (via Intl.NumberFormat(locale).formatToParts(12345.6) introspection — no new external dep).
    • Emits a canonical number (or null when blank) up through onValueChange.
    • Renders the displayed value through <FormattedNumber/> when not focused, and switches to a raw editable buffer on focus to avoid cursor jumps.
  • The plain <Input> is untouched. Consumers opt into locale-aware numeric handling by switching to <InputNumber>. This avoids surprising existing form code.

Bundled translations + module augmentation extension

  • ADDED: bundles for the new primitive aria-labels and the new <InputNumber> strings under each existing locale (en-GB, zh-CN, ar-AE, cy-GB, ru-RU). Schema (locales/schema.ts) extended with the new react-ui.formatted.* and react-ui.inputNumber.* namespaces. Module augmentation registers the new keys.
  • ADDED: extraction script scripts/extract-translation-keys.ts walks src/components/**/*.tsx AST for t('react-ui.*') calls and <Trans i18nKey='react-ui.*'> usages, diffs against locales/en-GB.json, and reports both missing keys (key in code, not in bundle) and orphan keys (key in bundle, not in code). Used by a new bun run check:i18n-keys script. Wired into the package's pre-publish gate.

shadcn registry preservation — ejected components keep i18n contract

@suaveplan/react-ui ships a shadcn-style component registry at registry/r/*.json that the shadcn CLI consumes to eject component source into a consumer's repo. The registry transform that runs during build rewrites @suaveplan/react-ui imports into local relative paths but explicitly preserves @suaveplan/react-i18n imports so the i18n contract survives ejection.

This change extends the registry deliverable as follows:

  • ADDED: registry entries for every new primitive (formatted-number.json, formatted-currency.json, formatted-date.json, formatted-list.json, formatted-relative-time.json, plural.json, formatted-message.json, input-number.json, react-ui-i18n-bridge.json). Each entry declares its @suaveplan/react-i18n dependency in registryDependencies so the shadcn CLI installs the binding when the component is ejected.
  • MODIFIED: registry entries for Drawer, Sheet, Calendar, DataTable, Pagination regenerated to capture the new imports (useDirection, useLocale, useLocaleAdapter, <FormattedNumber/>).
  • ADDED: scripts/check-registry-eject.ts — a smoke test that resolves each registry entry as if ejected (clones the JSON's source files into a tmp dir, applies the transform, runs tsc against the result with the consumer's expected @suaveplan/react-i18n import shape). Wired into the package's pre-publish gate so registry / source drift fails the build.

This is the gate that prevents a "works in-tree but broken when ejected" regression — the failure mode the spec already calls out as the load-bearing invariant.

Theme integration

react-ui ships three in-tree themes (light, dark, high-contrast) and is the foundation for downstream theme packages (react-ui-theme-*, in-flight per STATUS.md).

  • The new primitives are as-polymorphic and ship with no opaque DOM — they inherit theme tokens transparently through whatever wrapping element the consumer chooses (<Card>, <Badge>, raw <span>).
  • Theme switcher already has translation keys (react-ui.theme.*); this change does not touch them.
  • Future theme packages plug into the registry exactly like any other consumer: a theme package that wants to contribute a per-theme post-processor (e.g. a "playful" theme that wraps every label, or a "compliance" theme that masks PII patterns) registers a PostProcessorPlugin through the same <RegistryProvider plugins={…}/> API. Theme packages contributing translations continue to use module augmentation against ContributedTranslationKeys exactly as the existing pattern documents.

In short: themes are not blocked by this work, and the registry abstraction is the substrate themes will use to ship i18n-aware extensions in their own packages.

Validation bridge for react-forms (additive)

  • ADDED: src/i18n/validation-message-bridge/validation-message-bridge.ts — exports useValidationMessageBridge() returning a (error: SuaveplanError) => string function that resolves the error to a localized message via useErrorMessage from react-i18n. This is the canonical adapter that @suaveplan/react-forms (today already i18n-aware) and downstream form-bound consumers use to render validation errors uniformly. The bridge already exists ad-hoc in react-forms; this change centralizes it inside react-ui so other UI surfaces (toasts, dialogs, alerts) can render validation errors with the same resolution rules.

Out of scope for this change

  • Authoring the full react-ui capability spec. This change ships the narrow react-ui-i18n capability spec only. The broader enterprise-react-ui-v2 spec is in-flight separately.
  • Replacing the existing pack/ translation merge path. Packs continue to merge translations through i18n.catalog.mergeMessages. The new registry is for plugins (detectors, formatters, post-processors) not for translations.
  • Non-React i18n primitives. The plugin registry lives in react-i18n because formatter / post-processor / detector hooks are React-bound. Universal-tier plugin wiring belongs in @suaveplan/i18n itself and is out of scope.
  • Replacing <Trans> with a different ICU runtime. <Trans> continues to use the existing @suaveplan/i18n-icu-parser path. The post-processor pipeline runs after ICU interpolation, not in place of it.
  • Built-in formatter plugins beyond the six existing hook-types. Domain-specific formatters (useDuration, useTemperature, useFiscalQuarter, …) are exactly what the registry is for — consumers register them. We deliberately do not ship them in react-i18n itself.
  • A redux-style global registry. The registry is React-context-scoped. Two <RegistryProvider>s in the same tree get two independent registries. This matches every other context-scoped capability in the React stack.
  • <InputNumber> integer/BigInt mode. Today's scope is locale-aware decimal numbers. BigInt / integer-only / fixed-point variants are follow-up changes if demand arrives.
  • CLI-side translation extraction. The new extract-translation-keys.ts script lives inside react-ui. A repo-wide @suaveplan/i18n-extract CLI is a separate change; this script is a package-local tool only.

Parity Checklist

@suaveplan/react-i18n and @suaveplan/react-ui are genesis-original; no upstream ../suaveplan/ parent for the registry surface or the formatted-value primitives. The "upstream" column captures whether the current shipped react-i18n / react-ui exposes the capability today.

Capability Current genesis Post-change Notes
Detector chain extension by 3rd-party plugin 🚫 ✅ Today only <DetectorChainProvider> lets a consumer swap the entire array.
Formatter overlay (override the built-in useNumber impl) 🚫 ✅ Today the formatter hooks import their per-package implementation directly.
Project-specific formatter (useDuration, useFiscalQuarter) 🚫 ✅ via useFormatter(id, args) Custom plugin pattern.
Post-processor pipeline (pseudo-loc, debug overlay, redaction) 🚫 ✅ The @suaveplan/i18n core has post-processors at the catalog layer; this exposes them at the React-render layer too.
<FormattedNumber/> / <FormattedDate/> / <FormattedCurrency/> / <Plural/> / <FormattedRelativeTime/> / <FormattedList/> 🚫 ✅ New primitives live in react-ui.
<Drawer> / <Sheet> inline-start / inline-end (RTL-aware slide) 🚫 ✅ New direction values; existing values unchanged.
<Calendar> weekday / month names follow active locale 🚫 (only nav aria-labels translated) ✅ react-day-picker locale prop now wired.
<DataTable> column-level locale formatter shortcut 🚫 ✅ formatter: shortcut prop on ColumnDef; cell: override still wins.
<InputNumber> locale-aware decimal/grouping 🚫 ✅ New component, plain <Input> untouched.
<Pagination> non-Latin numeral rendering 🚫 ✅ Already-translated keys, but page numbers are raw String(n) today.
Translation-key extraction script 🚫 ✅ (package-local) bun run check:i18n-keys in react-ui.
Validation message bridge centralized in react-ui 🚫 (lives ad-hoc in react-forms) ✅ Centralized so toasts / dialogs / alerts can reuse.
OTel metrics for plugin registration + formatter latency / errors 🚫 ✅ Genesis-wide rule.
Structured error codes for plugin lifecycle 🚫 ✅ Six new REACT_I18N.* codes.
Backwards compatibility with no <RegistryProvider> mounted n/a ✅ All existing consumers continue working without opt-in.
LocaleAdapterPlugin slot — swap date-fns for luxon / day.js / Temporal 🚫 ✅ Calendar resolves through the adapter; default is date-fns.
<ReactUiI18nBridge> — single bridge to Radix <DirectionProvider>, sonner <Toaster dir>, document <html lang>/<html dir> 🚫 ✅ One mount point updates every third-party context.
Locale-aware default sortingFn / filterFn for @tanstack/react-table 🚫 ✅ localeAwareSortingFn(locale) / localeAwareFilterFn(locale) exposed; auto-installed by formatter: shortcut.
shadcn registry: new entries for new primitives n/a ✅ Registry shipped alongside source.
shadcn registry: existing entries regenerated for modified components n/a ✅ Drawer / Sheet / Calendar / DataTable / Pagination registry JSON refreshed.
Registry ejection smoke test gate (check-registry-eject.ts) 🚫 ✅ New pre-publish gate ensures ejected source still compiles against @suaveplan/react-i18n.
Theme-package extension via registry post-processors / locale adapters n/a (no theme packages exist on genesis yet) ✅ contract Documented; themes plug in through the same <RegistryProvider> substrate, no new theme-side surface needed.

Verification approach

  • Unit tests (vitest, pool: 'forks') per new module:
    • registry/registry.test.ts — registration, duplicate-id detection, disposable lifecycle, schema validation rejection.
    • registry/use-registry.test.tsx — context lookup, missing-provider error.
    • hooks/use-formatter/use-formatter.test.tsx — built-in id resolution, custom plugin override, missing-id error, memoization across renders.
    • Per-detector-plugin scenarios: priority splice, deterministic tie-break, environment gating, abort signal threading.
    • Per-formatted-primitive: renders correct text for each formatter id, as prop polymorphism, hydration mismatch suppression for <FormattedRelativeTime/> auto mode.
  • Spec-conformance tests — one per requirement in the new spec deltas.
  • Property tests (fast-check) — registry idempotency, plugin priority ordering, post-processor pipeline associativity for same-priority plugins.
  • Storybook stories — every new primitive ships default + playground + variants; all 70 react-ui components keep their existing stories green.
  • Translation-key gate — the new check:i18n-keys script is run in CI, hard-fails on missing or orphan keys.
  • Downstream sweep — bunx turbo test --filter=...@suaveplan/react-i18n (covers react-ui, react-forms, every pack, every theme).

Impact

Affected dependents

Run bunx turbo ls --filter=...@suaveplan/react-i18n and bunx turbo ls --filter=...@suaveplan/react-ui to enumerate.

  • @suaveplan/react-ui — [x] tests run / [x] docs reviewed / [ ] no action needed (this change AUTHORS the new react-ui i18n surface)
  • @suaveplan/react-forms — [x] tests run / [x] docs reviewed / [ ] no action needed (validation-message bridge centralization; existing useTranslation / useErrorMessage consumption unchanged)
  • @suaveplan/react-ui-themes/* (per-theme packages) — [x] tests run / [ ] docs reviewed / [x] no action needed (theme labels go through tDynamic; surface unchanged)
  • @suaveplan/react-ui-packs/* (per-pack packages) — [x] tests run / [ ] docs reviewed / [x] no action needed (pack translation-merge surface unchanged; new registry is additive)
  • Every dependent of react-ui (large set) — [x] tests run via bunx turbo test --filter=...@suaveplan/react-ui / [ ] docs reviewed / [x] no action needed (no breaking surface change)

@suaveplan/react-i18n is on the foundation path of the React stack, so the dependent sweep is non-optional. Step 10 of §13 is mandatory and runs:

bunx turbo test --filter=...@suaveplan/react-i18n
bunx turbo test --filter=...@suaveplan/react-ui

Migration impact

  • No breaking changes. All existing react-i18n exports keep their signatures. All existing react-ui component props keep their semantics; new directions on <Drawer> / <Sheet> and the new formatter: shortcut on <DataTable> columns are additive.

  • Recommended adoption path (for downstream packs / themes / consumer apps wanting plugin support):

    // BEFORE
    <TranslationProvider config={i18nConfig}>{children}</TranslationProvider>
    
    // AFTER (opt-in; old form continues to work)
    <RegistryProvider plugins={[myDetector, myFormatter]}>
      <TranslationProvider config={i18nConfig}>{children}</TranslationProvider>
    </RegistryProvider>
    
  • Locale-aware behavior wiring (inline-start / inline-end Drawer/Sheet, locale-aware Calendar, <InputNumber>, <DataTable> formatter: shortcut) is opt-in per call site. No global flag flip.

Source mapping

This change is genesis-original. The "source" is the current implementation of react-i18n and react-ui plus the @suaveplan/i18n core's plugin slot and post-processor pipeline (which already exists at the catalog layer and is now mirrored at the React-render layer).

Requirement Source Notes
@suaveplan/i18n plugin slot on createI18n packages/i18n/i18n/src/instance/instance.ts Existing pattern — registry mirrors this at the React layer.
Catalog-layer post-processors packages/i18n/i18n/src/post-processors/ Existing pattern — react-i18n adds a render-layer pipeline using the same priority + id scheme.
Detector chain shape packages/i18n/i18n-detector/src/chain/chain.ts Existing — registry-driven splice extends this.
<TranslationProvider> context wiring packages/react/react-i18n/src/provider/provider.tsx Existing — <RegistryProvider> is a sibling provider; both compose.
react-ui locale bundles + module augmentation packages/react/react-ui/src/locales/ Existing — extended with new namespaces for the formatted primitives.
Pack-system translation merge packages/react/react-ui/src/pack/pack-registry-provider/pack-registry-provider.tsx:236 Existing call site for i18n.catalog.mergeMessages. Untouched.