13 KiB
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) tosrc/errors/error-codes.ts. Update the registry test. - 1.2 Author
src/registry/registry.types.ts—DetectorPlugin,FormatterPlugin<TArgs, TResult>,PostProcessorPlugin,PostProcessorContext,ReactI18nRegistryinterfaces. 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 andRegistryConfigSchema(the union container shape<RegistryProvider>accepts). - 1.4 Implement
src/registry/registry.ts—createReactI18nRegistry()returning a freshReactI18nRegistry. 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
.mdforregistry.ts(≥200 words, all six required sections).
Phase 2 — <RegistryProvider> and useReactI18nRegistry
- 2.1 Implement
src/registry/registry-context.ts— context withnulldefault 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(). ThrowsREACT_I18N.REGISTRY_NOT_PROVIDEDoutside 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
.mdfiles for bothregistry-context.tsxanduse-registry.ts.
Phase 3 — Detector plugin spliced into chain
- 3.1 Modify
src/detector/default-chain/default-chain.ts— accept an optionalregistry?: ReactI18nRegistryargument; when provided, splice plugins byprioritybetween built-ins. - 3.2 Modify
src/hooks/use-locale-detection/use-locale-detection.ts— read registry from context (when available) and feed it intodefaultDetectorChain. - 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.mdto 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 aFormatterPluginwhoseformatcalls the existing per-package implementation. - 4.2 Implement
src/hooks/use-formatter/use-formatter.ts— looks up plugin by id from registry, returnsformatresult, 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 throughuseFormatterwhen 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
.mdforuse-formatter.tsand updates to each modified hook's.mddocumenting 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 thedate-fnsbuilt-in unlessdisableBuiltInLocaleAdaptersis set. - 4b.5 Tests — built-in resolution, swap-with-luxon-shaped adapter, missing-id throw.
- 4b.6 Add
LOCALE_ADAPTER_NOT_REGISTEREDto 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.tsxto 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
.mdforpost-processors.tsand updatetrans.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(noexport *). - 6.3 Add the new public surface to
src/index.tsroot 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-i18ngreen. - 7.2
bunx turbo test:coverage --filter=@suaveplan/react-i18n≥ 100 / 100 / 100 / 100. - 7.3
bunx turbo build --filter=@suaveplan/react-i18ngreen. - 7.4 §13 banned-pattern scan, missing-
.mdscan, README check, bare-version check, subpath-export count, security scan — all clean. - 7.5
bunx turbo test --filter=...@suaveplan/react-i18n(downstream sweep) — green. Includesreact-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>withreact-uibranding) + 3 siblings. - 8.8
src/i18n/index.tsexplicit-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), exposesLocaleContext, syncs<html lang>/<html dir>(opt-out), forwardsdirto descendant sonner<Toaster>. - 8b.2
src/i18n/tanstack-bridge/tanstack-bridge.ts—localeAwareSortingFn(locale)andlocaleAwareFilterFn(locale)backed byIntl.Collator. Plus sibling.mdand.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>addsinline-start/inline-enddirection values; resolves viauseDirection. Updatedrawer.tsx,drawer.md,drawer.test.tsx,drawer.stories.tsx. - 9.2
<Sheet>addsinline-start/inline-endsidevalues; sameuseDirectionresolution. Update siblings. - 9.3
<DataTable>ColumnDef.formattershortcut. Updatedata-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>wiresuseLocale()intoreact-day-picker'slocaleprop. AddlocaleStringToDateFnsLocalehelper 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,onValueChangenumeric 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.tswith the new namespaces. - 11.2 Extend each
src/locales/<locale>.jsonwith 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.tsto register the new keys intoContributedTranslationKeys. - 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 canonicalen-GB.json. Document missing-vs-orphan output format. - 11.5 Add
bun run check:i18n-keystoreact-ui/package.json::scripts. Wire intobunx turbo lintvia the package'sturbo.jsonlint 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 aSuaveplanErrorand render it through the bridge. Existing string props continue to work. - 12.3 Optional touch on
react-formsto import the bridge from@suaveplan/react-ui/i18n/validation-message-bridgeinstead 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>.jsonfor 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-i18ndep inregistryDependencies. - 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 consumertsconfig.json, asserts@suaveplan/react-i18nimports survive. - 12b.4 Wire
bun run check:registry-ejectintopackage.json::scriptsand intobunx turbo lintviaturbo.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-uigreen. - 13.2
bunx turbo test:coverage --filter=@suaveplan/react-ui≥ 100 / 100 / 100 / 100. - 13.3
bunx turbo build --filter=@suaveplan/react-uigreen. - 13.4
bun run check:i18n-keyszero-defect. - 13.4b
bun run check:registry-ejectzero-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-uidownstream 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.mddeltas intoopenspec/specs/react-i18n/spec.md. - 14.2 Create
openspec/specs/react-ui-i18n/spec.mdfrom the deltas. - 14.3 Update root docs:
MANIFEST.md(auto-generate),MISSING.yml(auto-generate),STATUS.md,SUMMARY.md. - 14.4
bunx markdownlint-cli2 --fixacross touched files; second pass without--fixexits 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/.