8.5 KiB
@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 inpackages/i18n/i18n/src/constants/index.tsand are imported by every formatter (forLOCALE_BCP47_MAPBCP-47 normalisation), the detector (forRTL_LOCALESdirection lookup), the catalog (forDEFAULT_LOCALESdefaults), and the interpolation engine (forDEFAULT_INTERPOLATION). - Error code registry — the full
I18N_CODESnamespace defined viadefineCodes("I18N", [...] as const). The split CANNOT spread codes across leaf packages without two failure modes: (a) consumers can'tinstanceof I18nErrorand discriminate by code without importing every leaf, and (b) the OTel error tagging surface fragments. One central registry is the only sane option. I18nErrorclass — extendsSuaveplanError<I18nCode>. Every leaf throws it; central definition keepsinstanceofdiscrimination 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— exportsLOCALE_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.tsL31-L55. - ADDED:
src/constants/rtl-locales.ts— exportsRTL_LOCALES: ReadonlySet<string>(7 ISO 639-1 codes:ar,he,fa,ur,ps,sd,yi) andRTL_LOCALE_LIST(same as a tree-shake-friendly tuple). - ADDED:
src/constants/locale-descriptors.ts— exportsLOCALE_DESCRIPTORS(per-locale{ name, nativeName, direction }metadata for 16 locales),DEFAULT_LOCALEStuple (16 codes), andDefaultLocaletype alias. - ADDED:
src/constants/interpolation-defaults.ts— exportsDEFAULT_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 fullI18N_CODESregistry viadefineCodes("I18N", [...] as const)and theI18nErrorCode(formerlyI18nCode) 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— exportsI18nError 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 explicitdeclared 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.jsonexportsfield 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-sharedwhile the monolith retains its copy; both compile. - Renaming
I18nCode→I18nErrorCodein the existing monolith — happens in Wave 6. - Adding the
I18nMessageResolveradapter 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 correctlypackages/i18n/i18n-shared/src/constants/rtl-locales.test.ts—RTL_LOCALES.has("ar")true,RTL_LOCALES.has("en")false,RTL_LOCALE_LISTlength matches set sizepackages/i18n/i18n-shared/src/constants/locale-descriptors.test.ts— everyDEFAULT_LOCALESentry has a descriptor;LOCALE_DESCRIPTORS.ar.direction === "rtl"packages/i18n/i18n-shared/src/constants/interpolation-defaults.test.ts—DEFAULT_INTERPOLATION.prefix === "{{"; type isas constpackages/i18n/i18n-shared/src/errors/error-codes.test.ts— every code is exported;I18N_CODES.MISSING_KEY === "I18N.MISSING_KEY"; new codes presentpackages/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 barrelpackages/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/i18nand@suaveplan/locale-dataunderpackages/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.mdregenerated viabun scripts/folder-reorg/generate-manifest.tsafter the package lands.CLAUDE.mdpackage listing updated with a one-line entry under the i18n cluster.