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

13 KiB

Tasks — react-i18n plugin registry & react-ui first-class i18n surface

Phased so each phase ends with a green pre-publish gate. Each task is a single concrete unit of work; check off when the implementation, sibling .md, test file, and (where applicable) story file are all complete and the corresponding gate is green.

Phase 1 — @suaveplan/react-i18n plugin registry foundation

  • 1.1 Add new error codes (PLUGIN_DUPLICATE_ID, PLUGIN_INVALID, REGISTRY_NOT_PROVIDED, FORMATTER_NOT_REGISTERED, DETECTOR_PLUGIN_DUPLICATE_ID, DETECTOR_PLUGIN_INVALID) to src/errors/error-codes.ts. Update the registry test.
  • 1.2 Author src/registry/registry.types.ts — DetectorPlugin, FormatterPlugin<TArgs, TResult>, PostProcessorPlugin, PostProcessorContext, ReactI18nRegistry interfaces. No nested object literals; every shape named.
  • 1.3 Author src/registry/schemas/registry.schemas.ts — DetectorPluginSchema, FormatterPluginSchema, PostProcessorPluginSchema. Strict-object Zod schemas. Export both schemas and RegistryConfigSchema (the union container shape <RegistryProvider> accepts).
  • 1.4 Implement src/registry/registry.ts — createReactI18nRegistry() returning a fresh ReactI18nRegistry. Idempotent re-registration, duplicate-id throw, OTel observability (react_i18n.registry.plugins_total{kind,outcome}), disposable handles.
  • 1.5 Author src/registry/registry.test.ts — registration / duplicate / dispose / schema-rejection / OTel-emit scenarios. Property test for register-then-dispose-N-times → empty registry.
  • 1.6 Sibling .md for registry.ts (≥200 words, all six required sections).

Phase 2 — <RegistryProvider> and useReactI18nRegistry

  • 2.1 Implement src/registry/registry-context.ts — context with null default and a brand symbol for safe cross-bundle identity.
  • 2.2 Implement src/registry/registry-context.tsx — <RegistryProvider plugins? disableBuiltInFormatters? children>. Mounts a fresh registry on first render, registers built-in formatters unless disabled, registers user-provided plugins, disposes the registry on unmount.
  • 2.3 Implement src/registry/use-registry.ts — useReactI18nRegistry(). Throws REACT_I18N.REGISTRY_NOT_PROVIDED outside a provider.
  • 2.4 Tests for the provider — registry-context.test.tsx. Verify context isolation, dispose-on-unmount, built-in registration, user-plugin registration, disabled built-ins.
  • 2.5 Sibling .md files for both registry-context.tsx and use-registry.ts.

Phase 3 — Detector plugin spliced into chain

  • 3.1 Modify src/detector/default-chain/default-chain.ts — accept an optional registry?: ReactI18nRegistry argument; when provided, splice plugins by priority between built-ins.
  • 3.2 Modify src/hooks/use-locale-detection/use-locale-detection.ts — read registry from context (when available) and feed it into defaultDetectorChain.
  • 3.3 Update default-chain.test.ts — coverage for plugin splice, deterministic tie-break, environment gating, no-registry fallback to current behavior.
  • 3.4 Update sibling default-chain.md to document the new optional parameter.

Phase 4 — Formatter plugins + useFormatter hook

  • 4.1 Author src/registry/formatters/built-in/ — six files (number.ts, date.ts, currency.ts, list.ts, relative-time.ts, plural.ts), each exporting a FormatterPlugin whose format calls the existing per-package implementation.
  • 4.2 Implement src/hooks/use-formatter/use-formatter.ts — looks up plugin by id from registry, returns format result, memoizes on (locale, id, JSON.stringify(args)).
  • 4.3 Modify each existing formatter hook (use-number, use-date, use-currency, use-list, use-relative-time, use-plural) to delegate through useFormatter when a registry is available; fall back to direct call when not.
  • 4.4 Tests — use-formatter.test.tsx (built-in resolution, custom override, missing-id throw, memoization), updated tests for each modified hook (no behavior change without a registry, override works with a registry).
  • 4.5 Sibling .md for use-formatter.ts and updates to each modified hook's .md documenting the registry-override path.

Phase 4b — Locale-adapter plugin slot

  • 4b.1 Author src/registry/locale-adapters/locale-adapters.types.ts — LocaleAdapterPlugin<TLocale> interface.
  • 4b.2 Author src/registry/locale-adapters/built-in/date-fns.ts — built-in adapter, dynamic-import the date-fns locales map so non-Calendar consumers do not pay the bundle cost.
  • 4b.3 Implement src/hooks/use-locale-adapter/use-locale-adapter.ts + sibling .md.
  • 4b.4 Wire <RegistryProvider> to auto-register the date-fns built-in unless disableBuiltInLocaleAdapters is set.
  • 4b.5 Tests — built-in resolution, swap-with-luxon-shaped adapter, missing-id throw.
  • 4b.6 Add LOCALE_ADAPTER_NOT_REGISTERED to error registry.

Phase 5 — Post-processor pipeline

  • 5.1 Author src/registry/post-processors/post-processors.ts — runPostProcessors(message: string, ctx: PostProcessorContext, plugins: PostProcessorPlugin[]): string. Sorts by priority, ties broken lexicographically by id.
  • 5.2 Modify src/components/trans/trans.tsx to run the registry's post-processor pipeline after ICU interpolation, before rendering.
  • 5.3 Tests — pipeline ordering, identity (no plugins → message unchanged), associativity property test, integration with <Trans> rendering.
  • 5.4 Sibling .md for post-processors.ts and update trans.md.

Phase 6 — Subpath exports + barrel

  • 6.1 Add subpath exports to react-i18n/package.json::exports: ./registry, ./registry/detectors, ./registry/formatters, ./registry/post-processors.
  • 6.2 Add explicit named re-exports to src/registry/index.ts (no export *).
  • 6.3 Add the new public surface to src/index.ts root barrel (also explicit named).
  • 6.4 Run §13 step 4a barrel-shape scan — zero hits expected.

Phase 7 — Pre-publish gate for react-i18n

  • 7.1 bunx turbo lint typecheck test --filter=@suaveplan/react-i18n green.
  • 7.2 bunx turbo test:coverage --filter=@suaveplan/react-i18n ≥ 100 / 100 / 100 / 100.
  • 7.3 bunx turbo build --filter=@suaveplan/react-i18n green.
  • 7.4 §13 banned-pattern scan, missing-.md scan, README check, bare-version check, subpath-export count, security scan — all clean.
  • 7.5 bunx turbo test --filter=...@suaveplan/react-i18n (downstream sweep) — green. Includes react-ui, react-forms, every pack/theme.

Phase 8 — @suaveplan/react-ui formatted primitives

  • 8.1 src/i18n/formatted-number/formatted-number.tsx + .md + .test.tsx + .stories.tsx.
  • 8.2 src/i18n/formatted-currency/formatted-currency.tsx + 3 siblings.
  • 8.3 src/i18n/formatted-date/formatted-date.tsx + 3 siblings.
  • 8.4 src/i18n/formatted-list/formatted-list.tsx + 3 siblings.
  • 8.5 src/i18n/formatted-relative-time/formatted-relative-time.tsx + 3 siblings (auto-mode interval test uses fakeClock from @suaveplan/testing/runner).
  • 8.6 src/i18n/plural/plural.tsx + 3 siblings.
  • 8.7 src/i18n/formatted-message/formatted-message.tsx (re-export <Trans> with react-ui branding) + 3 siblings.
  • 8.8 src/i18n/index.ts explicit-named barrel.
  • 8.9 New subpath exports in react-ui/package.json::exports: ./i18n, ./i18n/formatted-number, ./i18n/formatted-currency, etc.

Phase 8b — ReactUiI18nBridge + TanStack collator bridges

  • 8b.1 src/i18n/react-ui-i18n-bridge/react-ui-i18n-bridge.tsx + .md + .test.tsx + .stories.tsx. Mounts <DirectionProvider> (Radix), exposes LocaleContext, syncs <html lang>/<html dir> (opt-out), forwards dir to descendant sonner <Toaster>.
  • 8b.2 src/i18n/tanstack-bridge/tanstack-bridge.ts — localeAwareSortingFn(locale) and localeAwareFilterFn(locale) backed by Intl.Collator. Plus sibling .md and .test.ts.
  • 8b.3 Compose <ReactUiI18nBridge> into the default <ReactUiProvider> so the canonical app-level provider gets it for free; standalone mounting remains supported.
  • 8b.4 Subpath exports: ./i18n/react-ui-i18n-bridge, ./i18n/tanstack-bridge.

Phase 9 — Locale-aware behavior wiring on existing components

  • 9.1 <Drawer> adds inline-start / inline-end direction values; resolves via useDirection. Update drawer.tsx, drawer.md, drawer.test.tsx, drawer.stories.tsx.
  • 9.2 <Sheet> adds inline-start / inline-end side values; same useDirection resolution. Update siblings.
  • 9.3 <DataTable> ColumnDef.formatter shortcut. Update data-table.tsx, data-table.md, data-table.test.tsx, data-table.stories.tsx.
  • 9.4 <Pagination> page-number rendering switches to <FormattedNumber/>. Verify keys remain stable.
  • 9.5 <Calendar> wires useLocale() into react-day-picker's locale prop. Add localeStringToDateFnsLocale helper with a curated map for the five bundled locales + a documented fallback path. Update siblings.

Phase 10 — <InputNumber>

  • 10.1 src/components/input/input-number/input-number.tsx + .md + .test.tsx + .stories.tsx. Locale-aware decimal/grouping parse, focus-state buffer, onValueChange numeric emission.
  • 10.2 Subpath export ./components/input/input-number.
  • 10.3 Bundle keys for placeholder / aria-label across en-GB, zh-CN, ar-AE, cy-GB, ru-RU.

Phase 11 — Translation bundles + extraction script

  • 11.1 Extend src/locales/schema.ts with the new namespaces.
  • 11.2 Extend each src/locales/<locale>.json with translations for the new keys (real translations, not English placeholders for non-English bundles — request review from native readers if uncertain, document in proposal).
  • 11.3 Update src/locales/augmentation.ts to register the new keys into ContributedTranslationKeys.
  • 11.4 Author scripts/extract-translation-keys.ts. AST walk via @babel/parser (or the same parser the rest of the repo uses), diff against the canonical en-GB.json. Document missing-vs-orphan output format.
  • 11.5 Add bun run check:i18n-keys to react-ui/package.json::scripts. Wire into bunx turbo lint via the package's turbo.json lint pipeline.

Phase 12 — Validation message bridge

  • 12.1 src/i18n/validation-message-bridge/validation-message-bridge.ts + sibling siblings.
  • 12.2 Update <Toast>, <Alert>, and <ConfirmDeleteDialog> (the obvious validation-error consumers) to optionally accept a SuaveplanError and render it through the bridge. Existing string props continue to work.
  • 12.3 Optional touch on react-forms to import the bridge from @suaveplan/react-ui/i18n/validation-message-bridge instead of its own ad-hoc copy. (Only if the diff stays trivial; otherwise mark as a follow-up.)

Phase 12b — shadcn registry regeneration + ejection gate

  • 12b.1 Author / regenerate registry/r/<name>.json for every new primitive (formatted-number, formatted-currency, formatted-date, formatted-list, formatted-relative-time, plural, formatted-message, input-number, react-ui-i18n-bridge, tanstack-bridge, validation-message-bridge). Each lists its @suaveplan/react-i18n dep in registryDependencies.
  • 12b.2 Regenerate registry entries for modified components: drawer, sheet, calendar, data-table, pagination — capturing new imports (useDirection, useLocale, useLocaleAdapter, <FormattedNumber/>).
  • 12b.3 Author scripts/check-registry-eject.ts — applies the registry transform to each entry's source files, type-checks the transformed result against a synthesized consumer tsconfig.json, asserts @suaveplan/react-i18n imports survive.
  • 12b.4 Wire bun run check:registry-eject into package.json::scripts and into bunx turbo lint via turbo.json.
  • 12b.5 Test: scripts/check-registry-eject.test.ts — covers happy-path eject, missing-registry-entry diagnostic, transformed-source-fails-tsc diagnostic.

Phase 13 — Pre-publish gate for react-ui

  • 13.1 bunx turbo lint typecheck test --filter=@suaveplan/react-ui green.
  • 13.2 bunx turbo test:coverage --filter=@suaveplan/react-ui ≥ 100 / 100 / 100 / 100.
  • 13.3 bunx turbo build --filter=@suaveplan/react-ui green.
  • 13.4 bun run check:i18n-keys zero-defect.
  • 13.4b bun run check:registry-eject zero-defect — every registry entry compiles when ejected.
  • 13.5 §13 banned-pattern / missing-.md / barrel-shape / runner-import scans — all clean.
  • 13.6 bunx turbo test --filter=...@suaveplan/react-ui downstream sweep — green.
  • 13.7 Storybook smoke run — every new story renders without console errors / warnings.

Phase 14 — Spec fold + archive

  • 14.1 Fold specs/react-i18n/spec.md deltas into openspec/specs/react-i18n/spec.md.
  • 14.2 Create openspec/specs/react-ui-i18n/spec.md from the deltas.
  • 14.3 Update root docs: MANIFEST.md (auto-generate), MISSING.yml (auto-generate), STATUS.md, SUMMARY.md.
  • 14.4 bunx markdownlint-cli2 --fix across touched files; second pass without --fix exits 0.
  • 14.5 Move openspec/changes/react-i18n-plugin-registry-and-ui-primitives/ → openspec/archive/2026-MM-DD-react-i18n-plugin-registry-and-ui-primitives/.