# Tasks: enterprise-react-i18n **Change ID**: enterprise-react-i18n **Package**: `@suaveplan/react-i18n` **Target path**: `packages/react/react-i18n` **Prerequisite changes**: `i18n-storage-backends`, `i18n-completion` (must be archived before this change ships) **Prerequisite packages on Verdaccio**: `@suaveplan/i18n` (with the two changes above folded in), `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/logger`, `@suaveplan/locale-data`, `@suaveplan/types` ## 0. Supersession - [ ] 0.1 Move `openspec/changes/enterprise-react-translate/` → `openspec/archive/-enterprise-react-translate-superseded/` with a top-level note explaining the supersession (legacy was i18next-based; this change supersedes by building on the universal core) - [ ] 0.2 Update `STATUS.md` and `SUMMARY.md` so `@suaveplan/react-translate` is replaced by `@suaveplan/react-i18n` in the pending list - [ ] 0.3 Document the consumer-side rename `@suaveplan/react-translate` → `@suaveplan/react-i18n` in the React migration section of `docs/` ## 1. Spec authoring - [ ] 1.1 Read this folder's `proposal.md` and `specs/react-i18n/spec.md` end-to-end - [ ] 1.2 Read `openspec/specs/i18n/spec.md` (folded with the two prerequisite changes) — every API used here should be listed there - [ ] 1.3 Read `openspec/AGENTS.md` Sections 13 and 14 - [ ] 1.4 `openspec change validate enterprise-react-i18n` exits with no errors ## 2. Scaffold - [ ] 2.1 Create `packages/react/react-i18n/` with `package.json` at `0.1.0`, `tsconfig.lib.json`, `vitest.config.ts`, `vite.config.ts`, scripts. React-library tier (`extends ../../tsconfig/react-library.json`). - [ ] 2.2 Dependencies: `@suaveplan/i18n workspace:^`, `@suaveplan/error workspace:^`, `@suaveplan/events workspace:^`, `@suaveplan/logger workspace:^`, `@suaveplan/locale-data workspace:^`, `@suaveplan/types workspace:^`, `@opentelemetry/api catalog:observability`, `tslib catalog:build`, `zod catalog:forms` - [ ] 2.3 Peer dependencies: `react ^18.0.0 || ^19.0.0` - [ ] 2.4 Dev dependencies: `@suaveplan/testing workspace:^`, `@suaveplan/testing-dom workspace:^`, `@testing-library/react`, `@types/react`, `@types/react-dom`, `react`, `react-dom`, `vitest`, `vite`, `vite-plugin-dts`, `fast-check`, `@vitest/coverage-istanbul` - [ ] 2.5 NO `i18next`, `react-i18next`, `i18next-browser-languagedetector`, `i18next-http-backend` — verified by grep before commit - [ ] 2.6 `"sideEffects": false` - [ ] 2.7 Subpath exports: `.`, `./provider`, `./hooks`, `./components`, `./components/trans`, `./components/language-switcher`, `./components/missing-key-overlay`, `./components/i18n-suspense`, `./hoc`, `./errors`, `./schemas` ## 3. Errors and types - [ ] 3.1 Define `ReactI18nError` and four codes via `defineCodes("REACT_I18N", [...])`: `NO_PROVIDER`, `NAMESPACE_LOAD_FAILED`, `HYDRATION_MISMATCH`, `TRANS_INVALID_CHILD` - [ ] 3.2 Define types: `I18nConfig`, `LocaleDescriptor`, `LanguageSwitcherState`, `TranslationProviderProps`, `UseTranslationReturn`, `UseTranslateReturn`, `UseLocaleReturn`, `UseDirectionReturn`, `UseErrorMessageReturn`, `UseI18nReturn`, formatting-hook return types - [ ] 3.3 `react-i18n-error.test.ts` — code registry, error chain, JSON wire format - [ ] 3.4 Sibling `.md` for the error class ## 4. I18nConfig + provider ### 4.1 Config - [ ] 4.1.1 `createI18nConfig(options)` builds an `I18nConfig` validated through `I18nConfigSchema` (Zod) - [ ] 4.1.2 Schema fields: `defaultLocale`, `fallbackLocale`, `supportedLocales`, `backend`, `backends`, `initialMessages`, `escapeHtml`, `nestingDepth`, `plugins`, `saveMissing` - [ ] 4.1.3 Throws `ReactI18nError(HYDRATION_MISMATCH)` on invalid `initialMessages`; `I18nError(INVALID_OPTIONS)` on other shape failures - [ ] 4.1.4 `config.test.ts` — happy path, every Zod rejection path - [ ] 4.1.5 Sibling `.md` ### 4.2 Provider - [ ] 4.2.1 `TranslationProvider` accepts `i18n` (caller-managed) OR `config` (Provider-managed); rejects both / neither at runtime via Zod refinement - [ ] 4.2.2 Internal subscription via `useSyncExternalStore` to `i18n.events.on('languageChanged' | 'loaded')` - [ ] 4.2.3 SSR-safe: `useSyncExternalStore` server snapshot returns the synchronous catalog state - [ ] 4.2.4 Disposes the instance on unmount only when Provider-managed - [ ] 4.2.5 OTel span on mount with attributes `{ locale, supportedLocales }` - [ ] 4.2.6 `provider.test.tsx` — caller-managed-not-disposed, provider-managed-disposed, SSR initial-messages hydration, both-i18n-and-config-rejected - [ ] 4.2.7 Sibling `.md` ## 5. Hooks — translation ### 5.1 useI18n / useTranslation / useTranslate - [ ] 5.1.1 `useI18n()` returns the active `I18nInstance`; throws `ReactI18nError(NO_PROVIDER)` in dev when no Provider - [ ] 5.1.2 `useTranslation(namespace?)` triggers `i18n.preload(namespace)` on first mount when a backend is registered; throws the in-flight Promise (Suspense) - [ ] 5.1.3 Returns `{ t, i18n, ready }` where `t` is the ergonomic-options-bag form - [ ] 5.1.4 `useTranslate(namespace?)` is `useTranslation(namespace).t` (terse alias) - [ ] 5.1.5 Production no-provider path: log once + identity translator - [ ] 5.1.6 `use-translation.test.tsx` — preload Suspense + already-loaded sync path + namespace-load failure - [ ] 5.1.7 `use-translation.spec-conformance.test.tsx` — dev throw, prod soft-fail (asserts `process.env.NODE_ENV` switch with vitest stubGlobal) - [ ] 5.1.8 Sibling `.md` documenting Suspense semantics + the dev/prod no-provider distinction ## 6. Hooks — locale + direction - [ ] 6.1 `useLocale()` returns `{ locale, setLocale }`; `setLocale` calls `i18n.changeLanguage` - [ ] 6.2 `useDirection()` returns `{ direction, locale }` derived from `getTextDirection` - [ ] 6.3 Both via `useSyncExternalStore` subscribing to `languageChanged` - [ ] 6.4 `use-locale.test.tsx`, `use-direction.test.tsx` — re-render on locale change, ar → rtl - [ ] 6.5 Sibling `.md` for each ## 7. Hooks — formatting - [ ] 7.1 `useDate(date, options?)` — memoised `formatDate(date, locale, options)` - [ ] 7.2 `useNumber(value, options?)` — memoised `formatNumber(value, locale, options)` - [ ] 7.3 `useCurrency(amount, currency, options?)` — memoised `formatCurrency(amount, currency, locale, options)` - [ ] 7.4 `useRelativeTime(deltaMs, options?)` — memoised `formatRelativeTime(deltaMs, locale, options)` - [ ] 7.5 `useList(values, options?)` — memoised `formatList(values, locale, options)` - [ ] 7.6 Memoisation key: `(locale, ...inputs, JSON.stringify(options))` — re-render on locale change verified - [ ] 7.7 `use-date.test.tsx`, `use-number.test.tsx`, `use-currency.test.tsx`, `use-relative-time.test.tsx`, `use-list.test.tsx` - [ ] 7.8 Sibling `.md` for each ## 8. Hooks — specialised - [ ] 8.1 `useErrorMessage(error)` — `errors:` lookup with `error.message` fallback - [ ] 8.2 `usePlural(count, locale?)` — returns `{ category, rule }` from `PluralizationEngine` - [ ] 8.3 `use-error-message.test.tsx`, `use-plural.test.tsx` - [ ] 8.4 Sibling `.md` for each ## 9. Components — Trans - [ ] 9.1 `` parses message via `MessageParser` and calls `formatChunks(message, values, components)` - [ ] 9.2 Fallback when key missing: parse the component's `children` as the default ICU message - [ ] 9.3 `count` prop drives plural branch via `PluralizationEngine.resolve` - [ ] 9.4 Throws `ReactI18nError(TRANS_INVALID_CHILD)` when a child cannot be matched to a tag pair - [ ] 9.5 Emits `react_i18n.render.duration_ms` histogram - [ ] 9.6 `trans.test.tsx` — tag substitution, nested tags, count, default message, missing-key fallback - [ ] 9.7 `trans.spec-conformance.test.tsx` — missing-key counter, render duration histogram - [ ] 9.8 Sibling `.md` with five rich-text examples (bold, link, nested, plural with tag, fallback) - [ ] 9.9 `trans.stories.tsx` — Storybook with all five examples ## 10. Components — LanguageSwitcher - [ ] 10.1 Headless render-prop API: `children: (state) => ReactNode` - [ ] 10.2 `state.change(locale)` wraps in `useTransition` - [ ] 10.3 Locales sourced from `i18n.supportedLocales` (or override prop) with metadata from `@suaveplan/locale-data` - [ ] 10.4 `language-switcher.test.tsx` — render prop receives full state, change wraps in transition, isPending toggles - [ ] 10.5 Sibling `.md` - [ ] 10.6 `language-switcher.stories.tsx` — Storybook with native `