Initial Commit
This commit is contained in:
96
openspec/archive/2026-05-09-i18n-shared/proposal.md
Normal file
96
openspec/archive/2026-05-09-i18n-shared/proposal.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# `@suaveplan/i18n-shared` — runtime atoms for the i18n family
|
||||
|
||||
**Package**: `@suaveplan/i18n-shared` (NEW)
|
||||
**Status**: PROPOSED
|
||||
**Type**: feature (new universal-tier package)
|
||||
**Date**: 2026-05-05
|
||||
**Wave**: 1b (foundation, prerequisite for Waves 2–6 of the i18n modular split)
|
||||
**Design doc**: `openspec/cross-cutting-designs/2026-05-05-i18n-modular-split.md`
|
||||
**Path**: `packages/i18n/i18n-shared/`
|
||||
|
||||
## Why
|
||||
|
||||
The 14-package i18n modular split needs a single home for runtime atoms that every leaf consumes:
|
||||
|
||||
- **Constants** — `LOCALE_BCP47_MAP`, `RTL_LOCALES`, `RTL_LOCALE_LIST`, `LOCALE_DESCRIPTORS`, `DEFAULT_LOCALES`, `DefaultLocale`, `DEFAULT_INTERPOLATION`. Today these live in `packages/i18n/i18n/src/constants/index.ts` and are imported by every formatter (for `LOCALE_BCP47_MAP` BCP-47 normalisation), the detector (for `RTL_LOCALES` direction lookup), the catalog (for `DEFAULT_LOCALES` defaults), and the interpolation engine (for `DEFAULT_INTERPOLATION`).
|
||||
- **Error code registry** — the full `I18N_CODES` namespace defined via `defineCodes("I18N", [...] as const)`. The split CANNOT spread codes across leaf packages without two failure modes: (a) consumers can't `instanceof I18nError` and discriminate by code without importing every leaf, and (b) the OTel error tagging surface fragments. One central registry is the only sane option.
|
||||
- **`I18nError` class** — extends `SuaveplanError<I18nCode>`. Every leaf throws it; central definition keeps `instanceof` discrimination working across the family.
|
||||
|
||||
These cannot live in `@suaveplan/types` (zero runtime — see CLAUDE.md `types` rule). They cannot stay in `@suaveplan/i18n` core because backend / formatter / detector / parser packages would each need a dep on the core, which causes a cycle the moment the slim core (Wave 6) re-exports any of them. The clean answer is a leaf runtime package: `@suaveplan/i18n-shared`.
|
||||
|
||||
## What changes
|
||||
|
||||
### New package: `@suaveplan/i18n-shared`
|
||||
|
||||
- ADDED: `packages/i18n/i18n-shared/` package scaffold (package.json, tsconfig.json, vite.config.ts, README.md).
|
||||
- ADDED: `src/constants/locale-bcp47-map.ts` — exports `LOCALE_BCP47_MAP` (Record<string, string>), the 22-entry mapping from short / mixed-case codes to canonical BCP-47 tags. Source: `packages/i18n/i18n/src/constants/index.ts` L31-L55.
|
||||
- ADDED: `src/constants/rtl-locales.ts` — exports `RTL_LOCALES: ReadonlySet<string>` (7 ISO 639-1 codes: `ar`, `he`, `fa`, `ur`, `ps`, `sd`, `yi`) and `RTL_LOCALE_LIST` (same as a tree-shake-friendly tuple).
|
||||
- ADDED: `src/constants/locale-descriptors.ts` — exports `LOCALE_DESCRIPTORS` (per-locale `{ name, nativeName, direction }` metadata for 16 locales), `DEFAULT_LOCALES` tuple (16 codes), and `DefaultLocale` type alias.
|
||||
- ADDED: `src/constants/interpolation-defaults.ts` — exports `DEFAULT_INTERPOLATION = { prefix: "{{", suffix: "}}", maxDepth: 10 } as const`.
|
||||
- ADDED: `src/constants/index.ts` — explicit named re-export barrel.
|
||||
- ADDED: `src/errors/error-codes.ts` — exports the full `I18N_CODES` registry via `defineCodes("I18N", [...] as const)` and the `I18nErrorCode` (formerly `I18nCode`) discriminant union. Initial registry contains all 14 codes currently in the monolith plus 4 new codes carved out for the per-package split:
|
||||
- `MISSING_KEY`, `INVALID_LOCALE`, `INVALID_OPTIONS`, `LOADER_FAILED`, `INTERPOLATION_ERROR`, `PLURAL_RULE_NOT_FOUND` (parser/interpolation/pluralization)
|
||||
- `BACKEND_READ_FAILED`, `BACKEND_WRITE_FAILED`, `BACKEND_NOT_REGISTERED`, `BACKEND_LOCALE_NOT_AVAILABLE` (backends)
|
||||
- `INVALID_FORMATTER_OPTIONS`, `UNSUPPORTED_LOCALE` (formatters)
|
||||
- `NESTING_DEPTH_EXCEEDED`, `PLUGIN_ALREADY_INSTALLED` (core / plugin slot)
|
||||
- NEW: `BACKEND_TIMEOUT`, `DETECTOR_CHAIN_EXHAUSTED`, `CHAINED_BACKEND_ALL_FAILED`, `FORMATTER_CACHE_OVERFLOW` — codes added by the per-package split
|
||||
- ADDED: `src/errors/i18n-error.ts` — exports `I18nError extends SuaveplanError<I18nErrorCode>`. Constructor widens the protected base constructor to public; suppression-comment-free implementation per CLAUDE.md §9 (uses a no-body class with explicit `declare`d constructor signature, or equivalent biome-clean form).
|
||||
- ADDED: `src/errors/index.ts` — explicit named re-export barrel.
|
||||
- ADDED: `src/index.ts` — namespace re-exports both the constants and errors barrels.
|
||||
|
||||
### Subpath exports
|
||||
|
||||
- ADDED: `package.json` `exports` field with subpaths:
|
||||
- `.` — root barrel
|
||||
- `./constants` — constants only (zero error-class dep, useful for tree-shaking)
|
||||
- `./constants/locale-bcp47-map` — single-constant import for size-sensitive consumers
|
||||
- `./constants/rtl-locales` — single-constant import
|
||||
- `./constants/locale-descriptors` — single-constant import
|
||||
- `./constants/interpolation-defaults` — single-constant import
|
||||
- `./errors` — `I18N_CODES`, `I18nError`, `I18nErrorCode`
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Catalog / parser / formatter / backend / detector implementations** — separate change folders per the design doc.
|
||||
- **Removing constants/errors from `packages/i18n/i18n/src/`** — happens in Wave 6 (`i18n-core-slim`). For Waves 2–5, the leaves import from `@suaveplan/i18n-shared` while the monolith retains its copy; both compile.
|
||||
- **Renaming `I18nCode` → `I18nErrorCode`** in the existing monolith — happens in Wave 6.
|
||||
- **Adding the `I18nMessageResolver` adapter** that maps codes to localized error messages — that adapter belongs to `@suaveplan/error` (a follow-up change, not this one).
|
||||
|
||||
## Source mapping
|
||||
|
||||
| Requirement | Source file | Source lines | Notes |
|
||||
|---|---|---|---|
|
||||
| `LOCALE_BCP47_MAP` | `packages/i18n/i18n/src/constants/index.ts` | L31-L55 | Verbatim port |
|
||||
| `RTL_LOCALES`, `RTL_LOCALE_LIST` | `packages/i18n/i18n/src/constants/index.ts` | L57-L75 | Verbatim port; split into two named exports already present |
|
||||
| `LOCALE_DESCRIPTORS` | `packages/i18n/i18n/src/constants/index.ts` | L77-L104 | Verbatim port |
|
||||
| `DEFAULT_LOCALES`, `DefaultLocale` | `packages/i18n/i18n/src/constants/index.ts` | L11-L29 | Verbatim port |
|
||||
| `DEFAULT_INTERPOLATION` | `packages/i18n/i18n/src/constants/index.ts` | L4-L8 | Verbatim port |
|
||||
| `I18N_CODES`, `I18nErrorCode` | `packages/i18n/i18n/src/error-codes.ts` | full file | Verbatim port + 4 new codes |
|
||||
| `I18nError` | `packages/i18n/i18n/src/errors/i18n-error.ts` | full file | Port; rewrite to drop `// biome-ignore` per CLAUDE.md §9 |
|
||||
|
||||
### Requirements with no source line
|
||||
|
||||
| Requirement | Justification |
|
||||
|---|---|
|
||||
| Subpath exports for individual constants | Tree-shaking — `@suaveplan/i18n-formatter-collator` should not pull `LOCALE_DESCRIPTORS` just to get `LOCALE_BCP47_MAP` |
|
||||
| Four new error codes (`BACKEND_TIMEOUT`, `DETECTOR_CHAIN_EXHAUSTED`, `CHAINED_BACKEND_ALL_FAILED`, `FORMATTER_CACHE_OVERFLOW`) | The per-package split surfaces failure modes the monolith never modelled — chained-backend exhaustion, detector exhaustion, etc. |
|
||||
|
||||
## Verification approach
|
||||
|
||||
Each requirement carries a `**Verified by:**` line in the spec delta. Concrete test files:
|
||||
|
||||
- `packages/i18n/i18n-shared/src/constants/locale-bcp47-map.test.ts` — short codes, mixed-case codes, and known canonical tags all map correctly
|
||||
- `packages/i18n/i18n-shared/src/constants/rtl-locales.test.ts` — `RTL_LOCALES.has("ar")` true, `RTL_LOCALES.has("en")` false, `RTL_LOCALE_LIST` length matches set size
|
||||
- `packages/i18n/i18n-shared/src/constants/locale-descriptors.test.ts` — every `DEFAULT_LOCALES` entry has a descriptor; `LOCALE_DESCRIPTORS.ar.direction === "rtl"`
|
||||
- `packages/i18n/i18n-shared/src/constants/interpolation-defaults.test.ts` — `DEFAULT_INTERPOLATION.prefix === "{{"`; type is `as const`
|
||||
- `packages/i18n/i18n-shared/src/errors/error-codes.test.ts` — every code is exported; `I18N_CODES.MISSING_KEY === "I18N.MISSING_KEY"`; new codes present
|
||||
- `packages/i18n/i18n-shared/src/errors/i18n-error.test.ts` — `new I18nError(I18N_CODES.MISSING_KEY, "k")` succeeds; `instanceof I18nError`; `instanceof SuaveplanError`; `.code === "I18N.MISSING_KEY"`
|
||||
- `packages/i18n/i18n-shared/src/index.test.ts` — every documented public export resolves through the root barrel
|
||||
- `packages/i18n/i18n-shared/src/byte-size.test.ts` — root bundle stays under a documented byte budget (constants + 18 error codes + 1 class ≈ < 4 KiB minified)
|
||||
|
||||
## Impact
|
||||
|
||||
- New universal-tier package — joins `@suaveplan/i18n` and `@suaveplan/locale-data` under `packages/i18n/`.
|
||||
- Zero downstream consumers as of this change (the leaves consume it in Waves 2–5, the slim core re-exports it in Wave 6).
|
||||
- `MANIFEST.md` regenerated via `bun scripts/folder-reorg/generate-manifest.ts` after the package lands.
|
||||
- `CLAUDE.md` package listing updated with a one-line entry under the i18n cluster.
|
||||
Reference in New Issue
Block a user