Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
# React UI i18n — deferred tooling: stories, key extraction, registry ejection gate
**Packages**: `@suaveplan/react-ui`
**Status**: PROPOSED
**Type**: tooling + completion follow-up
## Why
The `react-i18n-plugin-registry-and-ui-primitives` change (archived 2026-05-10) shipped the registry substrate, the seven `<FormattedX/>` primitives, the `<ReactUiI18nBridge>`, the tanstack collator helpers, `<InputNumber>`, and the validation message bridge. Three items from that change's tasks list were intentionally deferred so the runtime surface could ship coherent without dragging tooling along:
1. **Storybook stories for the seven new primitives + `<InputNumber>` + `<ReactUiI18nBridge>`** — CLAUDE.md mandates every React component carry a `.stories.tsx` (default + playground + variants); these primitives shipped without stories so the gates would not slip on cosmetics. Reviewers and design integrators rely on stories to discover, browse, and visually verify components in isolation.
2. **`bun run check:i18n-keys`** — an AST-walking script over `src/components/**/*.tsx` and `src/i18n/**/*.tsx` that diffs `t("react-ui.…")` calls and `<FormattedMessage i18nKey="react-ui.…" />` usages against the canonical `src/locales/en-GB.json`. The script catches missing keys (in code, not in bundle) AND orphan keys (in bundle, not in code). Specified in the new `react-ui-i18n` capability spec but not implemented yet.
3. **`bun run check:registry-eject`** — a smoke test that resolves every shadcn `registry/r/*.json` entry, applies the registry transform, and type-checks the transformed source against a synthesized consumer `tsconfig.json` so an ejected component still compiles. Needed because the registry transform preserves `@suaveplan/react-i18n` imports in ejected sources, and the new primitives + `<ReactUiI18nBridge>` exercise that contract more aggressively than any prior component.
These are tooling, not runtime — but they are part of the surface the proposal committed to. Closing them is the difference between "shipped" and "shipped, gates intact." Reviewers picking up this branch downstream will trip over the missing stories first; the two scripts close the drift loop.
## What changes
### `@suaveplan/react-ui`
#### Stories for the new i18n primitives + InputNumber + Bridge
- ADDED: `src/i18n/formatted-number/formatted-number.stories.tsx` — default + playground + variants (Latin digits, Arabic-Indic via `numberingSystem: "arab"`, percent / scientific / unit notation, `as="strong" | "data" | "output"`).
- ADDED: `src/i18n/formatted-currency/formatted-currency.stories.tsx` — default + playground + variants (USD / EUR / GBP / JPY across en-GB / fr-FR / zh-CN, display modes).
- ADDED: `src/i18n/formatted-date/formatted-date.stories.tsx` — default + playground + variants (Date / ISO string / epoch ms inputs, preset names + raw options, invalid-input renders empty).
- ADDED: `src/i18n/formatted-list/formatted-list.stories.tsx` — default + playground + variants (conjunction / disjunction / unit, long / short / narrow styles).
- ADDED: `src/i18n/formatted-relative-time/formatted-relative-time.stories.tsx` — default + playground + variants (auto-mode interval, explicit unit, future / past).
- ADDED: `src/i18n/plural/plural.stories.tsx` — default + playground + variants (en-GB collapses zero into other; ar-AE distinguishes zero / one / two / few / many).
- ADDED: `src/i18n/formatted-message/formatted-message.stories.tsx` — default + playground + variants (rich-text tag interpolation, plural via `count`).
- ADDED: `src/i18n/react-ui-i18n-bridge/react-ui-i18n-bridge.stories.tsx` — default + playground + variants showing the Radix `<DirectionProvider>` propagation (popover, dropdown-menu, dialog) under en-GB and ar-AE.
- ADDED: `src/i18n/formatted-cell/formatted-cell.stories.tsx` — default + playground showing the helper inside a `<DataTable>` for each formatter kind.
- ADDED: `src/i18n/validation-message-bridge/validation-message-bridge.stories.tsx` — default + playground showing the bridge inside an Alert and a Toast.
- ADDED: `src/components/input/input-number/input-number.stories.tsx` — default + playground + variants (en-GB / de-DE / ar-AE locale, controlled vs uncontrolled, percent / currency display options).
Every story file follows the existing `.stories.tsx` convention (no `@storybook/react` dep — the meta shape is inlined). Each story renders inside `<TranslationProvider>` + `<RegistryProvider>` + `<ReactUiI18nBridge>` so the runtime path matches production.
#### `bun run check:i18n-keys` — translation-key drift gate
- ADDED: `scripts/check-i18n-keys.ts` — AST walks `src/components/**/*.{ts,tsx}` and `src/i18n/**/*.{ts,tsx}` for:
- `t("react-ui.…")` calls in `useTranslation()` consumers
- `<FormattedMessage i18nKey="react-ui.…" />` usages
- `<Trans i18nKey="react-ui.…" />` usages (for already-shipped components)
- `tDynamic("react-ui.…")` calls
- Diffs the discovered keys against the canonical `src/locales/en-GB.json`.
- Reports `MISSING: <key> at <path>:<line>` for keys referenced in code but absent from the bundle.
- Reports `ORPHAN: <key>` for bundle keys not referenced anywhere in code.
- Exits non-zero on any mismatch.
- ADDED: `scripts/check-i18n-keys.test.ts` — fixtures-directory test exercising both the missing-key and orphan-key paths, plus a happy-path where source and bundle agree.
- MODIFIED: `package.json::scripts` adds `"check:i18n-keys": "bun scripts/check-i18n-keys.ts"`.
- MODIFIED: the package's `turbo.json` lint pipeline includes the new check so CI fails on drift.
#### `bun run check:registry-eject` — shadcn ejection compile gate
- ADDED: `scripts/check-registry-eject.ts` — walks every `registry/r/*.json`, copies the listed source files into a tmp directory, applies the registry transform (rewriting `@suaveplan/react-ui` imports to local relative paths, preserving `@suaveplan/react-i18n` imports), and runs `tsc --noEmit` against the transformed tree using a synthesized consumer `tsconfig.json` (extends `@suaveplan/typescript-config/library.json`, lists `@suaveplan/react-i18n` and the React peers in `compilerOptions.paths`).
- Reports `EJECT FAILURE: <component>` with the underlying `tsc` diagnostics on any component whose ejected source fails to compile.
- Reports `MISSING REGISTRY ENTRY: <path>` when a public component / primitive in `src/components/**` or `src/i18n/**` lacks a corresponding `registry/r/<name>.json`.
- Exits non-zero on either failure.
- ADDED: `scripts/check-registry-eject.test.ts` — happy-path test that ejects a known-good entry; failure-path test that introduces a missing import and asserts the diagnostic appears.
- MODIFIED: `package.json::scripts` adds `"check:registry-eject": "bun scripts/check-registry-eject.ts"`.
- MODIFIED: `turbo.json` lint pipeline includes the new check.
## Out of scope
- **Adding stories to the 70 existing react-ui components.** Those already have stories from the chassis Wave 3 work. This change is scoped to the i18n surface added in `react-i18n-plugin-registry-and-ui-primitives`.
- **Authoring a repo-wide `@suaveplan/i18n-extract` CLI.** The `check:i18n-keys` script is package-local — covers only `react-ui`'s `react-ui.*` namespace. A repo-wide CLI lives in a separate change.
- **Extending the registry transform itself.** The transform exists and is correct today; the new gate verifies the transform's output against a real type-checker pass.
- **Visual regression testing.** Stories enable visual review; automated visual regression is out of scope.
## Parity Checklist
Genesis-original; no upstream parent. The "Current genesis" column captures whether each artifact exists today (post-`react-i18n-plugin-registry-and-ui-primitives`).
| Capability | Current genesis | Post-change | Notes |
|---|---|---|---|
| Stories for `<FormattedNumber>` / `<FormattedCurrency>` / `<FormattedDate>` / `<FormattedList>` / `<FormattedRelativeTime>` / `<Plural>` / `<FormattedMessage>` | 🚫 | ✅ | One `.stories.tsx` per primitive. |
| Stories for `<ReactUiI18nBridge>` | 🚫 | ✅ | Demonstrates Radix `<DirectionProvider>` propagation under RTL locales. |
| Stories for `formattedCell` + `useValidationMessageBridge` | 🚫 | ✅ | Composed inside `<DataTable>`, `<Alert>`, `<Toast>` to show real-world placement. |
| Stories for `<InputNumber>` | 🚫 | ✅ | Controlled / uncontrolled, locale variants. |
| `bun run check:i18n-keys` | 🚫 | ✅ | AST-walking translation-key drift gate. |
| `bun run check:registry-eject` | 🚫 | ✅ | shadcn-eject smoke compile gate. |
| Both gates wired into `bunx turbo lint` | 🚫 | ✅ | CI fails on drift. |
## Verification approach
- `bun run check:i18n-keys` exits 0 on the current source + bundle pair; exits non-zero when an obviously-missing key is introduced (test fixtures cover both paths).
- `bun run check:registry-eject` exits 0 on every current `registry/r/*.json` entry; exits non-zero when a known-good entry is corrupted (test fixtures cover both paths).
- Storybook smoke pass: every new story renders without console errors / warnings under en-GB and ar-AE.
- Pre-publish gate: `bunx turbo lint typecheck test --filter=@suaveplan/react-ui` green.
## Impact
### Affected dependents
- `@suaveplan/react-ui` — [x] tests run / [x] docs reviewed / [ ] no action needed (this change AUTHORS the new tooling)
- Downstream consumers of `@suaveplan/react-ui` — [ ] tests run / [ ] docs reviewed / [x] no action needed (this change is purely additive: new scripts, new story files; no runtime surface changes)
`@suaveplan/react-ui` is on the foundation path of the React stack. The dependent sweep `bunx turbo test --filter=...@suaveplan/react-ui` SHALL be run as a sanity check, but no behavior change is expected.
## Migration impact
None — additive only. No runtime surface is touched.

View File

@@ -0,0 +1,73 @@
<!-- markdownlint-disable MD024 -->
# 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

View File

@@ -0,0 +1,62 @@
# Tasks — react-ui i18n deferred tooling
Three independent workstreams. Stories (Phase 1) is the largest; the two scripts (Phases 2, 3) are smaller and can run in parallel.
## Phase 1 — Stories for the new i18n surface
- [ ] 1.1 `src/i18n/formatted-number/formatted-number.stories.tsx` — default + playground + variants (Latin / Arabic-Indic / percent / unit notation, `as` polymorphism). Wrapped in `<TranslationProvider>` + `<RegistryProvider>` + `<ReactUiI18nBridge>`.
- [ ] 1.2 `src/i18n/formatted-currency/formatted-currency.stories.tsx` — USD / EUR / GBP / JPY across en-GB / fr-FR / zh-CN, display modes (`symbol` / `narrowSymbol` / `code` / `name`).
- [ ] 1.3 `src/i18n/formatted-date/formatted-date.stories.tsx` — `Date` / ISO / epoch ms inputs; preset names + raw `Intl.DateTimeFormatOptions`; invalid-input renders empty.
- [ ] 1.4 `src/i18n/formatted-list/formatted-list.stories.tsx` — conjunction / disjunction / unit; long / short / narrow.
- [ ] 1.5 `src/i18n/formatted-relative-time/formatted-relative-time.stories.tsx` — auto-mode (with `fakeClock` for deterministic story snapshots), explicit unit, future / past.
- [ ] 1.6 `src/i18n/plural/plural.stories.tsx` — en-GB / ar-AE / ru-RU showing different category buckets.
- [ ] 1.7 `src/i18n/formatted-message/formatted-message.stories.tsx` — rich-text tag interpolation, plural via `count`.
- [ ] 1.8 `src/i18n/react-ui-i18n-bridge/react-ui-i18n-bridge.stories.tsx` — Radix `<DirectionProvider>` propagation demo (popover, dropdown-menu, dialog, tooltip rendered side-by-side under en-GB and ar-AE).
- [ ] 1.9 `src/i18n/formatted-cell/formatted-cell.stories.tsx` — composed inside `<DataTable>` columns for each formatter kind.
- [ ] 1.10 `src/i18n/validation-message-bridge/validation-message-bridge.stories.tsx` — bridge inside `<Alert>` and `<Toast>`.
- [ ] 1.11 `src/components/input/input-number/input-number.stories.tsx` — en-GB / de-DE / ar-AE locales, controlled / uncontrolled, percent / currency display options.
- [ ] 1.12 Story-snapshot test (mirroring existing `*.stories.test.tsx` pattern in react-ui) for every new story.
## Phase 2 — `check:i18n-keys` translation key drift gate
- [ ] 2.1 Pick the AST parser already used elsewhere in the repo for source walks (`@babel/parser` is the typical choice; verify the catalog).
- [ ] 2.2 Author `scripts/check-i18n-keys.ts`. Walk `src/components/**/*.{ts,tsx}` and `src/i18n/**/*.{ts,tsx}`. Detect:
- `t("react-ui.…")` / `t("react-ui.…", …)`
- `tDynamic("react-ui.…")`
- `<FormattedMessage i18nKey="react-ui.…" />`
- `<Trans i18nKey="react-ui.…" />`
- [ ] 2.3 Diff discovered keys against `src/locales/en-GB.json`. Build the orphan list and the missing list.
- [ ] 2.4 Author `scripts/check-i18n-keys.test.ts` — fixture directory exercises both diagnostics + the happy path.
- [ ] 2.5 Add `"check:i18n-keys": "bun scripts/check-i18n-keys.ts"` to `package.json::scripts`.
- [ ] 2.6 Wire `check:i18n-keys` into `turbo.json`'s lint pipeline so CI fails on drift.
- [ ] 2.7 Run the script against the current source — expect zero diagnostics. If diagnostics appear, file each as a separate fix in this change.
## Phase 3 — `check:registry-eject` shadcn ejection compile gate
- [ ] 3.1 Confirm the registry transform's exact rewriting rules (which import specifiers are rewritten, which preserved). Document at the top of the script.
- [ ] 3.2 Author `scripts/check-registry-eject.ts`:
- Read every `registry/r/<name>.json`.
- For each entry: copy listed source files into `mkdtemp` directory.
- Apply registry transform.
- Synthesize a consumer `tsconfig.json` (extends `@suaveplan/typescript-config/library.json`, declares `paths` for `@suaveplan/react-i18n` + React peers).
- Run `tsc --noEmit` over the transformed tree.
- On failure, surface the underlying tsc diagnostics with the registry-entry name as context.
- [ ] 3.3 Detect missing registry entries — for every directory in `src/components/**/` and `src/i18n/**/` that has a public default-exporting `*.tsx`, assert the matching `registry/r/<name>.json` exists.
- [ ] 3.4 Author `scripts/check-registry-eject.test.ts` — happy-path test (pick one trivial component + verify ejection compiles); failure-path tests (corrupt transform output, drop a registry entry).
- [ ] 3.5 Add `"check:registry-eject": "bun scripts/check-registry-eject.ts"` to `package.json::scripts`.
- [ ] 3.6 Wire `check:registry-eject` into `turbo.json`'s lint pipeline.
- [ ] 3.7 Run the script against the current registry — expect zero diagnostics.
## Phase 4 — Pre-publish gate + spec fold
- [ ] 4.1 `bunx turbo lint typecheck test --filter=@suaveplan/react-ui` green.
- [ ] 4.2 `bunx turbo test:coverage --filter=@suaveplan/react-ui` ≥ 100 / 100 / 100 / 100 (story-snapshot + script tests must contribute to coverage).
- [ ] 4.3 `bunx turbo build --filter=@suaveplan/react-ui` green.
- [ ] 4.4 `bun run check:i18n-keys` zero-defect.
- [ ] 4.5 `bun run check:registry-eject` zero-defect.
- [ ] 4.6 §13 banned-pattern / missing-`.md` / barrel-shape / runner-import scans clean.
- [ ] 4.7 Downstream sweep `bunx turbo test --filter=...@suaveplan/react-ui` green (sanity check; no runtime surface change expected).
- [ ] 4.8 Fold spec deltas into `openspec/specs/react-ui-i18n/spec.md`. The two new scripts get their own Requirements with verification scenarios; stories are an artifact-presence requirement (one Scenario per primitive: `<feature>.stories.tsx exists`).
- [ ] 4.9 Update `STATUS.md` to reflect the closure of the deferred items.
- [ ] 4.10 Run `bunx markdownlint-cli2 --fix` across touched markdown.
- [ ] 4.11 Move `openspec/changes/react-ui-i18n-deferred-tooling/` → `openspec/archive/<yyyy-mm-dd>-react-ui-i18n-deferred-tooling/`.