Files
suaveplan/openspec/archive/2026-05-09-i18n-shared/proposal.md
2026-05-27 23:29:53 +01:00

8.5 KiB
Raw Blame History

@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 declared 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.