245 lines
29 KiB
Markdown
245 lines
29 KiB
Markdown
# 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:
|
|
|
|
```bash
|
|
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):
|
|
|
|
```tsx
|
|
// 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. |
|