Files
suaveplan/openspec/archive/2026-05-10-react-ui-i18n-deferred-tooling/specs/react-ui-i18n/spec.md
2026-05-27 23:29:53 +01:00

4.9 KiB

Spec Delta — react-ui-i18n deferred tooling

ADDED Requirements

Requirement: Stories ship for every i18n primitive

Every primitive added by the react-ui-i18n capability SHALL ship a sibling .stories.tsx alongside its implementation. Each story file SHALL contain a default export plus a playground and at least one variants story (the chassis-wide stories convention). Stories SHALL render inside <TranslationProvider> + <RegistryProvider> + <ReactUiI18nBridge> so the rendered surface matches production-runtime composition. Story-snapshot tests SHALL run as part of the package's test suite.

The covered surfaces are: <FormattedNumber>, <FormattedCurrency>, <FormattedDate>, <FormattedList>, <FormattedRelativeTime>, <Plural>, <FormattedMessage>, <ReactUiI18nBridge>, formattedCell, useValidationMessageBridge, and <InputNumber>.

Verified by: per-feature story-snapshot tests (one *.stories.test.tsx per primitive) plus a presence check in scripts/check-i18n-stories.test.ts (or equivalent).

Scenario: every i18n primitive has a .stories.tsx sibling

  • GIVEN the directory src/i18n/<primitive>/
  • WHEN the chassis *.stories.tsx walker runs
  • THEN every public primitive listed in the capability spec has a matching <primitive>.stories.tsx file

Requirement: bun run check:i18n-keys catches translation-key drift

The package SHALL ship scripts/check-i18n-keys.ts invoked via bun run check:i18n-keys. The script SHALL AST-walk every *.{ts,tsx} under src/components/** and src/i18n/** looking for translation-key references — t("react-ui.…"), tDynamic("react-ui.…"), <FormattedMessage i18nKey="react-ui.…" />, <Trans i18nKey="react-ui.…" /> — and SHALL diff the discovered key set against src/locales/en-GB.json. The script SHALL exit non-zero when any key is referenced in code but absent from the bundle (MISSING) or present in the bundle but referenced nowhere in code (ORPHAN). The script SHALL be wired into bunx turbo lint so CI fails on drift.

Verified by: scripts/check-i18n-keys.test.ts — fixture directory exercising the missing, orphan, and clean paths.

Scenario: missing key in code fails the gate

  • GIVEN a source file referencing t("react-ui.foo.absent") and en-GB.json lacking that key
  • WHEN bun run check:i18n-keys runs
  • THEN the script exits non-zero and prints MISSING: react-ui.foo.absent with the source path

Scenario: orphan key in bundle fails the gate

  • GIVEN en-GB.json containing react-ui.foo.unused and no source reference
  • WHEN bun run check:i18n-keys runs
  • THEN the script exits non-zero and prints ORPHAN: react-ui.foo.unused

Scenario: clean source + bundle pair passes

  • GIVEN every key in en-GB.json is referenced in code AND every code-referenced key is in the bundle
  • WHEN bun run check:i18n-keys runs
  • THEN the script exits zero with no diagnostics

Requirement: bun run check:registry-eject verifies shadcn ejection

The package SHALL ship scripts/check-registry-eject.ts invoked via bun run check:registry-eject. For every registry/r/<name>.json entry the script SHALL:

  1. Resolve the listed source files.
  2. Apply the registry transform (rewriting @suaveplan/react-ui imports to local relative paths, preserving @suaveplan/react-i18n imports).
  3. Run tsc --noEmit against the transformed tree using a synthesized consumer tsconfig.json that declares @suaveplan/react-i18n and React peers via compilerOptions.paths.

The script SHALL exit non-zero when any registry entry's transformed source fails to type-check, surfacing the underlying tsc diagnostics with the entry name as context. The script SHALL also detect missing registry entries — public components / primitives in src/components/** and src/i18n/** lacking a corresponding registry/r/<name>.json SHALL trigger a MISSING REGISTRY ENTRY diagnostic. The script SHALL be wired into bunx turbo lint.

Verified by: scripts/check-registry-eject.test.ts — fixture-driven happy-path + failure-path tests.

Scenario: ejected primitive type-checks against react-i18n

  • GIVEN the registry entry formatted-number.json
  • WHEN the transform is applied to its files and the result is type-checked
  • THEN tsc --noEmit exits 0 and the import import { useFormatter } from "@suaveplan/react-i18n" resolves

Scenario: missing registry entry for a public primitive fails the gate

  • GIVEN a public primitive at src/i18n/<new>/<new>.tsx with no corresponding registry/r/<new>.json
  • WHEN bun run check:registry-eject runs
  • THEN the script exits non-zero with MISSING REGISTRY ENTRY: src/i18n/<new>/<new>.tsx

Scenario: corrupted transform output fails the gate

  • GIVEN a registry entry whose source references a symbol the transform fails to rewrite correctly
  • WHEN the transformed source is type-checked
  • THEN the script exits non-zero with EJECT FAILURE: <name> and the underlying tsc diagnostics