Files
2026-05-27 23:29:53 +01:00

327 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# BLOCKER: enterprise-react-i18n
**Status:** RESOLVED 2026-05-05 — every sub-wave (1b foundation, 1c hooks, 1d components, 1e detectors, 1f branded `TranslationKey` + `tDynamic`, 1g E2E + README + archive) shipped. Spec folded into `openspec/specs/react-i18n/spec.md`; this folder lives in the archive at `openspec/archive/2026-05-05-enterprise-react-i18n/` for historical reference only.
**Original date:** 2026-05-04 (sub-wave 1b foundation landed 2026-05-05)
**Original status (kept below for history):** PARTIAL — sub-wave 1b complete; sub-waves 1c/1d/1e/1f/1g remaining.
## Sub-wave 1b foundation status (2026-05-05)
The `@suaveplan/react-i18n` package scaffold + foundation modules now ship
on top of the universal-core detector primitives that landed in 3be7ca13b.
Coverage is 100/100/100/100 across every shipped module; 68 tests across
9 files; `bunx turbo lint typecheck test build --filter=@suaveplan/react-i18n`
fully green.
| Module | Status |
|---|---|
| `@suaveplan/i18n` `./detector` subpath + root re-exports | OK |
| `packages/react/react-i18n/{package,tsconfig.lib,vite.config,vitest.config}` scaffold + polyfills | OK |
| `src/errors/error-codes.ts` + `react-i18n-error.ts` (5 codes) + tests + .md | OK |
| `src/schemas/i18n-config.schemas.ts` (Zod) + tests + .md | OK |
| `src/config/{create-i18n-config,to-i18n-instance-options}.ts` + tests + .md | OK |
| `src/utils/{env,identity-instance,warn-once-per-process}.ts` + tests + .md | OK |
| `src/observability/react-i18n-metrics.ts` + tests + .md | OK |
| `src/provider/{provider-context,provider}.tsx` + tests + .md | OK |
| `src/index.ts` top-level barrel (explicit named exports, no `export *`) | OK |
| `README.md` (sub-wave 1b scope) | OK |
| `src/hooks/**` | NOT STARTED (sub-wave 1c) |
| `src/components/**` | NOT STARTED (sub-wave 1d) |
| `src/detector/**` (the 9 concrete detectors + `defaultDetectorChain` + `useLocaleDetection`) | NOT STARTED (sub-wave 1e) |
| `src/translation-key/**` + `scripts/generate-translation-types.ts` + `scripts/lint/check-no-backwards-deps.ts` | NOT STARTED (sub-wave 1f) |
| Storybook stories + Playwright `e2e/react-i18n.spec.ts` | NOT STARTED (sub-wave 1g) |
| Spec archive into `openspec/specs/react-i18n/` | NOT STARTED (sub-wave 1g) |
The pre-publish gate for `@suaveplan/react-i18n` already runs green at
this scope. Subsequent sub-waves will widen the surface, add more tests,
and re-run the full gate before archive.
## What landed in this session
`@suaveplan/i18n` now ships the universal-core detector primitives required by
the spec amendment and consumed by `@suaveplan/react-i18n`:
| Path | Status |
|---|---|
| `packages/i18n/i18n/src/detector/detector.types.ts` | ✅ types-only contract |
| `packages/i18n/i18n/src/detector/detector.md` | ✅ co-located doc |
| `packages/i18n/i18n/src/detector/chain/chain.ts` | ✅ chain runner (sync-then-async-parallel, env gating, negotiation) |
| `packages/i18n/i18n/src/detector/chain/chain.md` | ✅ co-located doc |
| `packages/i18n/i18n/src/detector/chain/chain.test.ts` | ✅ 23 tests, 100/100/100/100 cov |
| `packages/i18n/i18n/src/detector/chain/index.ts` | ✅ explicit named re-exports |
| `packages/i18n/i18n/src/detector/index.ts` | ✅ explicit named re-exports |
| `packages/i18n/i18n/src/observability/detector-metrics.ts` | ✅ OTel counter + histogram + tracer |
| `packages/i18n/i18n/src/observability/detector-metrics.md` | ✅ co-located doc |
| `packages/i18n/i18n/src/observability/detector-metrics.test.ts` | ✅ 5 tests, 100% cov |
| `packages/i18n/i18n/src/index.ts` | ⏸ root re-exports deferred — see "Subpath registration" below |
| `packages/i18n/i18n/package.json` | ⏸ `./detector` subpath export deferred — see "Subpath registration" below |
All 773 tests pass. Detector subset coverage: 100% statements / 100% branches /
100% functions / 100% lines on the new modules.
## Blocker — context budget vs. scope
The full `enterprise-react-i18n` change scope (proposal + amendment) is the
single largest greenfield surface in the genesis branch:
- 1 React provider + 12 hooks + 4 components + 1 HOC + branded types + tDynamic
- 9 locale detectors as separate folders × (impl + .md + test) = 27 files
- `useLocaleDetection` + SSR contract + `defaultDetectorChain` factory
- `generate-translation-types.ts` script + co-located test
- `check-no-backwards-deps.ts` CI gate + co-located test
- Storybook stories per component + a Playwright E2E sweep
- README + sibling `.md` per implementation file (≥200 words, basic + advanced
examples, full API reference)
- Pre-publish gate at 100% statement/branch/function/line cov, plus the
10-step §13 + §13a + §13b verification sequence.
A conservative file count is ~85–110 production-grade files. That exceeds what
can be authored at production-grade quality (no stubs, no `// biome-ignore`,
no `export *`, complete sibling docs, real tests at 100% coverage, no mocks)
in the remaining context budget of a single session.
Per `CLAUDE.md` §"Critical (never compromise)" rules 2 and §"Workflow" 2
("Complete all implementations — no stubs, no placeholders, no TODOs") and
the user's stated preference for production-grade work, partially shipping
the React surface would itself be an anti-pattern. The detector chain is the
spec-bearing dependency the rest of the React work consumes; landing it now
keeps the dependency graph green and lets a follow-up session execute
mechanically against `proposal.md` + `tasks.md` + `spec.md`.
## Subpath registration (deferred to avoid conflicting with `i18n-spec-conformance-followup`)
The `packages/i18n/i18n/package.json` already has uncommitted modifications
from the parallel `i18n-spec-conformance-followup` change set (adding the
`./formatting/number`, `./formatting/relative-time`, and several `./backend/*`
subpaths plus the `@suaveplan/events` and `@suaveplan/storage` deps). The
parallel `packages/i18n/i18n/src/index.ts` has matching uncommitted re-exports.
This session intentionally did **not** add the `./detector` subpath to
`package.json` or root re-exports to `src/index.ts`, because doing so would
tangle together two unrelated change sets at the same git-diff level. The
follow-up session that lands `@suaveplan/react-i18n` MUST add both:
1. **`packages/i18n/i18n/package.json`** — add the `./detector` subpath block:
```json
"./detector": {
"import": {
"types": "./dist/detector/index.d.ts",
"default": "./dist/detector/index.js"
},
"require": {
"types": "./dist/detector/index.d.ts",
"default": "./dist/detector/index.js"
}
}
```
2. **`packages/i18n/i18n/src/index.ts`** — add the root re-exports so callers
can `import { createDetectorChain, ... } from "@suaveplan/i18n"` directly:
```ts
export type {
DetectorChain,
DetectorChainResult,
DetectorChainUnresolved,
DetectorContext,
DetectorEnvironment,
DetectorStorageLike,
LocaleDetector,
} from "./detector/index.js";
export {
createDetectorChain,
DETECTOR_CHAIN_UNRESOLVED,
} from "./detector/index.js";
export type {
DetectorMetricSample,
DetectorMetricsState,
} from "./observability/detector-metrics.js";
export {
getDetectorMetrics,
getDetectorMetricsState,
recordDetectorDuration,
recordDetectorMatch,
resetDetectorMetricsCache,
resetDetectorMetricsState,
} from "./observability/detector-metrics.js";
```
Until both are added, downstream consumers still work — they import directly
from the source via `@suaveplan/i18n/src/detector/index.js` during the
`workspace:^` development cycle, and Vite preserves the module structure so
`dist/detector/index.js` still gets emitted by the existing build config.
## Next-step commands for a follow-up session
The follow-up session should:
1. **Verify the prerequisite is in place:**
```bash
bunx vitest run packages/i18n/i18n/src/detector/ packages/i18n/i18n/src/observability/detector-metrics.test.ts
# expect: 28 tests pass
```
2. **Scaffold `packages/react/react-i18n/` per `tasks.md` §2:**
- `package.json` (peer `react ^18 || ^19`, dev deps for `react-dom`,
`@vitejs/plugin-react`, `@testing-library/react`, `happy-dom`,
`@types/react`, `@vitest/coverage-istanbul`, `fast-check`,
`@suaveplan/testing-dom`)
- `tsconfig.lib.json` (extends `tsconfig.base.json`, `jsx: react-jsx`)
- `vite.config.ts` (`preserveModules: true`, externals for every workspace
dep + `react`, `react-dom`, `react/jsx-runtime`)
- `vitest.config.ts` (env `happy-dom`, `pool: forks`,
`fileParallelism: false`, `provider: istanbul`, 100% thresholds)
- `tests/setup/polyfills.ts` (webcrypto)
- Subpath exports per `tasks.md` §2.7
3. **Land `@suaveplan/react-i18n` modules in this dependency order**
(each module green at 100% cov before the next starts):
1. `errors/error-codes.ts` + `errors/react-i18n-error.ts` — five codes
via `defineCodes("REACT_I18N", [...])` per spec
2. `schemas/i18n-config.schemas.ts` — Zod for `I18nConfig`
3. `i18n/config.ts` — `createI18nConfig` + `toI18nInstanceOptions`
4. `utils/env.ts` — `isProductionEnv` + `warnOncePerProcess`
5. `utils/identity-instance.ts` — production no-provider fallback
6. `observability/react-i18n-metrics.ts` — counters + histogram
7. `provider/provider-context.ts` + `provider/provider.tsx`
(`TranslationProvider` with `useSyncExternalStore` wiring)
8. `hooks/use-i18n/`, `hooks/use-translation/` (Suspense-aware),
`hooks/use-translate/`, `hooks/use-locale/`, `hooks/use-direction/`
9. `hooks/formatting/use-date/`, `use-number/`, `use-currency/`,
`use-relative-time/`, `use-list/`
10. `hooks/use-error-message/`, `hooks/use-plural/`
11. `components/trans/trans.tsx` (calls
`MessageParser.formatChunks<ReactNode>`)
12. `components/language-switcher/` (headless render-prop +
`useTransition`)
13. `components/missing-key-overlay/` (50-cap, dev-only, `aria-live`)
14. `components/i18n-suspense/` (default `aria-live` fallback +
counter increment)
15. `hoc/with-translation/`
16. `translation-key/translation-key.types.ts` (`AppTranslationKeys`,
`ContributedTranslationKeys`, branded `TranslationKey`)
17. `translation-key/t-dynamic.ts` (runtime catalog assertion +
`REACT_I18N.UNKNOWN_KEY` + `warn` log)
4. **Implement the 9 detectors** under
`packages/react/react-i18n/src/detector/<name>/` per `tasks.md` §22 (each
detector is a `<name>.ts` + `<name>.md` + `<name>.test.ts` + `index.ts`).
Each detector imports `LocaleDetector` from `@suaveplan/i18n/detector` (the
subpath landed in this session). The `defaultDetectorChain(opts)` factory
lives at `src/detector/default-chain/`. The `useLocaleDetection` hook
lives at `src/hooks/use-locale-detection/` and reads the chain via
`useSyncExternalStore`.
5. **CI gate `check-no-backwards-deps`** — author
`scripts/lint/check-no-backwards-deps.ts` (Bun + TypeScript compiler API)
that asserts `@suaveplan/react-i18n/package.json` declares no
`@suaveplan/react-ui*` / `@suaveplan/react-forms*` /
`@suaveplan/react-ui-theme-*` / `@suaveplan/react-ui-pack-*` deps.
Co-locate `scripts/lint/check-no-backwards-deps.test.ts` with one passing
case + three failing cases (one per banned dep type). Wire into the
package's `lint` turbo task via `package.json#scripts.lint`.
6. **Auto-generation script** — author
`packages/react/react-i18n/scripts/generate-translation-types.ts` that
reads a package's `<package>/src/locales/en-GB.json` and emits
`<package>/src/locales/translation-keys.ts` (string literal union) plus
`<package>/src/locales/augmentation.d.ts` (TypeScript module augmentation
into `ContributedTranslationKeys`). Test the script against a fixture
bundle.
7. **Storybook stories + E2E** — `tasks.md` §15 + §16. Stories per component
(default, playground, variants, states, WithI18n × 5 locales including
ar-AE RTL, WithTheme, A11y, Composition). E2E
`e2e/react-i18n.spec.ts` covering live language switch, RTL flip on
Arabic, lazy-namespace Suspense, missing-key overlay, SSR hydration
round-trip. Spinning up the Docker staging environment is part of the
E2E gate per `CLAUDE.md` §"Web App & Page Verification".
8. **Pre-publish gate (`AGENTS.md` §13, all 10 steps + §13a + §13b):**
```bash
bun run check
bunx turbo lint typecheck test --filter=@suaveplan/react-i18n
bunx turbo test:coverage --filter=@suaveplan/react-i18n # 100/100/100/100
bunx turbo build --filter=@suaveplan/react-i18n
bun scripts/lint/check-no-backwards-deps.ts
bun scripts/lint/check-test-runner-imports.ts # 0 violations
# Plus: banned-pattern scan, barrel-shape scan, missing-md scan,
# README check, bare-version check, subpath export count, security scan,
# dependent sweep (foundation packages only).
```
9. **Commit the React work** with the message in the user's instructions
(the one starting with `feat(react-i18n): @suaveplan/react-i18n + i18n
detector chain`). Do NOT push; do NOT amend.
## What's stable to commit now
The `@suaveplan/i18n` detector chain landing in this session is fully
spec-compliant, fully tested, and unblocks the React work mechanically. It
SHOULD be committed as a standalone change so the React follow-up has a
named git baseline to build on. A reasonable commit message:
```text
feat(i18n): locale-detector chain primitives (detector subpath)
Adds @suaveplan/i18n/detector — the universal-core types and chain runner
for the locale-detection chain that @suaveplan/react-i18n consumes.
- LocaleDetector / DetectorContext / DetectorEnvironment types
- createDetectorChain() with sync-first, async-parallel semantics
- DETECTOR_CHAIN_UNRESOLVED sentinel for explicit no-match handling
- BCP-47 negotiation via LocaleResolver per detector output
- Environment gating (browser / server / both)
- chain.cache(locale, ctx) walks every persisting detector
- OTel: i18n.detect span + i18n.detector.match_total counter +
i18n.detector.duration_ms histogram via getMeter('suaveplan.i18n')
- 28 tests at 100/100/100/100 coverage on the new modules
- New `./detector` subpath export
Foundation for enterprise-react-i18n proposal Amendment 2026-05-05; the
React glue, 9 concrete detectors, useLocaleDetection hook, branded
TranslationKey, tDynamic, and CI gate land in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
```
## What's pending
| Section | Files | Status |
|---|---|---|
| `@suaveplan/react-i18n` scaffold | `packages/react/react-i18n/{package,tsconfig.lib,vite.config,vitest.config}.json` (+ tests setup) | DONE (1b) |
| Errors + types | `src/errors/{error-codes,react-i18n-error}.ts` + tests + .md | DONE (1b) |
| Provider | `src/provider/{provider,provider-context}.tsx` + tests + .md | DONE (1b) |
| Schemas | `src/schemas/i18n-config.schemas.ts` + tests + .md | DONE (1b) |
| Config | `src/config/{create-i18n-config,to-i18n-instance-options}.ts` + tests + .md | DONE (1b) |
| Utils | `src/utils/{env,identity-instance,warn-once-per-process}.ts` + tests + .md | DONE (1b) |
| Observability counters + histogram | `src/observability/react-i18n-metrics.ts` + tests + .md | DONE (1b) |
| README | `packages/react/react-i18n/README.md` (sub-wave 1b scope) | DONE (1b) |
| Translation hooks | `src/hooks/{use-i18n,use-translation,use-translate}/` | NOT STARTED (1c) |
| Locale + direction hooks | `src/hooks/{use-locale,use-direction}/` | NOT STARTED (1c) |
| Formatting hooks | `src/hooks/formatting/{use-date,use-number,use-currency,use-relative-time,use-list}/` | NOT STARTED (1c) |
| Specialised hooks | `src/hooks/{use-error-message,use-plural}/` | NOT STARTED (1c) |
| `<Trans>` | `src/components/trans/` | NOT STARTED (1d) |
| `<LanguageSwitcher>` | `src/components/language-switcher/` | NOT STARTED (1d) |
| `<MissingKeyOverlay>` | `src/components/missing-key-overlay/` | NOT STARTED (1d) |
| `<I18nSuspense>` | `src/components/i18n-suspense/` | NOT STARTED (1d) |
| `withTranslation` HOC | `src/hoc/with-translation/` | NOT STARTED (1d) |
| 9 detectors | `src/detector/{querystring,user-preference,cookie,local-storage,html-lang,accept-language,navigator,cloudflare-ip-geo,fallback}/` | NOT STARTED (1e) |
| `defaultDetectorChain` | `src/detector/default-chain/` | NOT STARTED (1e) |
| `useLocaleDetection` | `src/hooks/use-locale-detection/` | NOT STARTED (1e) |
| Branded `TranslationKey` | `src/translation-key/{translation-key.types,t-dynamic}.ts` | NOT STARTED (1f) |
| `generate-translation-types.ts` | `scripts/generate-translation-types.ts` + test | NOT STARTED (1f) |
| `check-no-backwards-deps.ts` | `scripts/lint/check-no-backwards-deps.ts` + test | NOT STARTED (1f) |
| Storybook stories | `src/**/*.stories.tsx` per component | NOT STARTED (1g) |
| Playwright E2E | `e2e/react-i18n.spec.ts` | NOT STARTED (1g) |
| Pre-publish gate (full 10-step) | The §13 sequence at full surface coverage | DEFERRED until 1g |
| Archival | Move change → `archive/<yyyy-mm-dd>-enterprise-react-i18n/` | DEFERRED until 1g |
The proposal, tasks.md, and spec.md are unchanged from the user's authoring;
the follow-up should execute mechanically against them.