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:
- 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 futurereact-i18n-detector-cookie-signedor a consumer-owned A/B-bucket detector cannot ship as a drop-in package. - 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-specificuseDuration,useTemperature, oruseFiscalQuarter) without forking the package, and no overlay mechanism for swapping the defaultIntl.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—ReactI18nRegistryinterface with three surfaces:registerDetector(plugin: DetectorPlugin): DisposableregisterFormatter<TArgs, TResult>(plugin: FormatterPlugin<TArgs, TResult>): DisposableregisterPostProcessor(plugin: PostProcessorPlugin): Disposable- Each registration is reversible (
Disposable), idempotent on duplicateplugin.id, and validated against a Zod schema at registration time. Duplicate ids throwREACT_I18N.PLUGIN_DUPLICATE_ID.
- ADDED:
src/registry/registry-context.tsx—<RegistryProvider>wraps<TranslationProvider>(or composes with it) and injects aReactI18nRegistryinto context. - ADDED:
src/registry/use-registry.ts—useReactI18nRegistry()returns the active registry from context, throwsREACT_I18N.REGISTRY_NOT_PROVIDEDoutside a provider. - ADDED: subpath export
./registryand per-plugin-type exports (./registry/detectors,./registry/formatters,./registry/post-processors).
Detector plugins — chain becomes registry-driven
- MODIFIED:
defaultDetectorChain()consults the registry for any registeredDetectorPluginwhosepriorityfalls between two adjacent built-in detectors. Built-in detectors keep their current order; registered plugins splice in based onpriority(numeric, lower = earlier) with deterministic tie-break byid. - ADDED:
DetectorPlugininterface —{ id: string; priority: number; environment: 'browser' | 'server' | 'both'; detect: LocaleDetector }(re-usesLocaleDetectorshape 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, callsplugin.format(locale, args), memoizes by(locale, id, args). ThrowsREACT_I18N.FORMATTER_NOT_REGISTEREDfor unknown ids. - MODIFIED: existing per-type formatter hooks (
useNumber,useDate,useCurrency,useList,useRelativeTime,usePlural) keep their public signatures but internally delegate to the registry-backeduseFormatterwith 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 onpriority). - ADDED: built-in formatter plugins for the six existing hooks live in
src/registry/formatters/built-in/and are auto-registered by<RegistryProvider>unlessdisableBuiltInFormattersis set.
Post-processor plugins — message pipeline overlay
- ADDED:
PostProcessorPlugininterface —{ 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
priorityorder. Order is deterministic for identical priorities (lexicographic byid).
Schema + observability + errors
- ADDED: Zod schemas at every public registration boundary (
DetectorPluginSchema,FormatterPluginSchema,PostProcessorPluginSchema). Invalid plugins throwREACT_I18N.PLUGIN_INVALIDwith 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) => TLocaleresolver under a stableid(e.g."date-fns","luxon","temporal"). Used by primitives that need a non-Intllocale object (the canonical case isreact-day-picker'slocaleprop, which wants a date-fnsLocale). Consumers swap the date library by registering a different adapter under the same id, no fork required. Built-in adapter isdate-fns(loaded only when used — dynamic import inside the adapter to keep the universal-tier surface clean). - ADDED:
useLocaleAdapter<TLocale>(id: string): TLocalehook. ThrowsREACT_I18N.LOCALE_ADAPTER_NOT_REGISTEREDwhen 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 readsuseLocale()+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
diron<Toaster>fromsonnerso toasts pick up direction. - Exposes the active locale to a
LocaleContextconsumed by<DataTable>'s default sort/filter functions (Intl.Collator-backed) and by the<Calendar>localeprop. - Updates the document
<html lang>and<html dir>attributes when the active locale changes (opt-out via prop for SSR / nested-app scenarios).
- Mounts
- ADDED: locale-aware default
sortingFnandfilterFnfor@tanstack/react-tableexposed aslocaleAwareSortingFn(locale)/localeAwareFilterFn(locale)fromreact-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 viauseNumber.asdefaults to<span>, accepts any HTML element or React component.optionsmatches the underlyinguseNumberoptions 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? />. AcceptsDate | 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 anautomode that re-renders on a configurable interval (refreshIntervalMs, default60_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 viausePlural. - ADDED:
src/i18n/formatted-message/formatted-message.tsx— alias of<Trans>re-exported fromreact-uiso consumers do not need a second import to render translated text. Brand-acceptsTranslationKey<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) — whendirectionis'inline-start'/'inline-end', the underlyingvauldirection prop resolves to'left'/'right'based onuseDirection(). Existingdirection='left' | 'right' | 'top' | 'bottom'continues to work unchanged. - MODIFIED:
<Sheet>(src/components/sheet/sheet.tsx) — adds'inline-start'/'inline-end'to thesideunion; sameuseDirection-based resolution. RTL locales render aninline-startSheet from the right edge, LTR from the left. - MODIFIED:
<DataTable>(src/components/data-table/data-table.tsx) —ColumnDef<TData>accepts an optionalformatter?: { kind: 'number' | 'currency' | 'date' | 'relative-time'; options?: ... }shape. When set, the cell renderer wraps the raw value in the matching<FormattedX/>primitive. Cells with explicitcell:overrides win;formatter:is a shortcut for the common case. - MODIFIED:
<Pagination>pagination.tsx— page-number rendering already usest()via thereact-ui.pagination.*keys; no surface change. The MOD here is upgrading thePaginationStatus"Page X of Y" line to render<FormattedNumber/>so locales using non-Latin numerals (Arabic-Indic, Devanagari) display native digits when the active locale'snumberingSystemdiffers fromlatn. - MODIFIED:
<Calendar>calendar.tsx— passes theuseLocale()value intoreact-day-picker'slocaleprop, resolved throughuseLocaleAdapter("date-fns"). The active date-library adapter (date-fns by default; luxon/Temporal/day.js if a consumer registered a differentLocaleAdapterPluginwith 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(ornullwhen blank) up throughonValueChange. - Renders the displayed value through
<FormattedNumber/>when not focused, and switches to a raw editable buffer on focus to avoid cursor jumps.
- Parses incoming text using the active locale's decimal + grouping separators (via
- 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 newreact-ui.formatted.*andreact-ui.inputNumber.*namespaces. Module augmentation registers the new keys. - ADDED: extraction script
scripts/extract-translation-keys.tswalkssrc/components/**/*.tsxAST fort('react-ui.*')calls and<Trans i18nKey='react-ui.*'>usages, diffs againstlocales/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 newbun run check:i18n-keysscript. 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-i18ndependency inregistryDependenciesso 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, runstscagainst the result with the consumer's expected@suaveplan/react-i18nimport 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
PostProcessorPluginthrough the same<RegistryProvider plugins={…}/>API. Theme packages contributing translations continue to use module augmentation againstContributedTranslationKeysexactly 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— exportsuseValidationMessageBridge()returning a(error: SuaveplanError) => stringfunction that resolves the error to a localized message viauseErrorMessagefrom 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-i18ncapability spec only. The broaderenterprise-react-ui-v2spec is in-flight separately. - Replacing the existing
pack/translation merge path. Packs continue to merge translations throughi18n.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-i18nbecause formatter / post-processor / detector hooks are React-bound. Universal-tier plugin wiring belongs in@suaveplan/i18nitself and is out of scope. - Replacing
<Trans>with a different ICU runtime.<Trans>continues to use the existing@suaveplan/i18n-icu-parserpath. 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 inreact-i18nitself. - 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.tsscript lives insidereact-ui. A repo-wide@suaveplan/i18n-extractCLI 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,
asprop polymorphism, hydration mismatch suppression for<FormattedRelativeTime/>automode.
- 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-keysscript is run in CI, hard-fails on missing or orphan keys. - Downstream sweep —
bunx turbo test --filter=...@suaveplan/react-i18n(coversreact-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; existinguseTranslation/useErrorMessageconsumption unchanged)@suaveplan/react-ui-themes/*(per-theme packages) — [x] tests run / [ ] docs reviewed / [x] no action needed (theme labels go throughtDynamic; 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 viabunx 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-i18nexports keep their signatures. All existingreact-uicomponent props keep their semantics; new directions on<Drawer>/<Sheet>and the newformatter: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-endDrawer/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. |