# Capability Spec: i18n-shared (i18n-shared) **Package**: `@suaveplan/i18n-shared` **Status**: PROPOSED — first delta; becomes canonical spec on archive. > Every requirement below carries a `**Verified by:**` line at authoring time. (See `openspec/AGENTS.md` Section 13.) ## ADDED Requirements ### Requirement: BCP-47 locale tag map The package SHALL export `LOCALE_BCP47_MAP: Record` mapping short ISO 639-1 codes (and common mixed-case forms) to canonical BCP-47 tags. Lookups for unknown codes SHALL pass through unchanged at the consumer level (the constant is a plain `Record`; consumers handle the miss). #### Scenario: short code maps to canonical BCP-47 - **WHEN** `LOCALE_BCP47_MAP["en"]` is looked up - **THEN** the value equals `"en-GB"` #### Scenario: mixed-case code maps to canonical form - **WHEN** `LOCALE_BCP47_MAP["zh-cn"]` is looked up - **THEN** the value equals `"zh-CN"` **Verified by:** `packages/i18n/i18n-shared/src/constants/locale-bcp47-map.test.ts::"short code en maps to en-GB"` AND `packages/i18n/i18n-shared/src/constants/locale-bcp47-map.test.ts::"mixed-case zh-cn maps to zh-CN"` --- ### Requirement: RTL locale set and tuple The package SHALL export `RTL_LOCALES: ReadonlySet` containing the seven ISO 639-1 codes for right-to-left scripts: `ar`, `he`, `fa`, `ur`, `ps`, `sd`, `yi`. The package SHALL also export `RTL_LOCALE_LIST` as a tree-shake-friendly readonly tuple of the same codes. #### Scenario: ar is RTL - **WHEN** `RTL_LOCALES.has("ar")` is evaluated - **THEN** the result is `true` #### Scenario: en is not RTL - **WHEN** `RTL_LOCALES.has("en")` is evaluated - **THEN** the result is `false` #### Scenario: list and set agree on size - **WHEN** `RTL_LOCALE_LIST.length` is compared to `RTL_LOCALES.size` - **THEN** the values are equal **Verified by:** `packages/i18n/i18n-shared/src/constants/rtl-locales.test.ts::"ar is RTL"` AND `packages/i18n/i18n-shared/src/constants/rtl-locales.test.ts::"en is not RTL"` AND `packages/i18n/i18n-shared/src/constants/rtl-locales.test.ts::"RTL_LOCALE_LIST length matches RTL_LOCALES size"` --- ### Requirement: Locale descriptors with name, native name, and direction The package SHALL export `LOCALE_DESCRIPTORS` as a `Record` covering every entry in `DEFAULT_LOCALES`. Each descriptor's `direction` SHALL match the locale's membership in `RTL_LOCALES`. #### Scenario: every default locale has a descriptor - **WHEN** every code in `DEFAULT_LOCALES` is looked up in `LOCALE_DESCRIPTORS` - **THEN** every lookup returns a defined descriptor #### Scenario: Arabic descriptor is RTL - **WHEN** `LOCALE_DESCRIPTORS.ar.direction` is read - **THEN** the value equals `"rtl"` #### Scenario: native names are non-empty - **WHEN** every descriptor's `nativeName` is checked - **THEN** every value is a non-empty string **Verified by:** `packages/i18n/i18n-shared/src/constants/locale-descriptors.test.ts::"every DEFAULT_LOCALES code has a descriptor"` AND `packages/i18n/i18n-shared/src/constants/locale-descriptors.test.ts::"ar descriptor direction is rtl"` AND `packages/i18n/i18n-shared/src/constants/locale-descriptors.test.ts::"every descriptor nativeName is non-empty"` --- ### Requirement: Default interpolation defaults The package SHALL export `DEFAULT_INTERPOLATION` as a frozen object with `prefix: "{{"`, `suffix: "}}"`, and `maxDepth: 10`. The shape SHALL be `as const` so consumers can narrow at the type level. #### Scenario: default prefix and suffix - **WHEN** `DEFAULT_INTERPOLATION.prefix` and `DEFAULT_INTERPOLATION.suffix` are read - **THEN** the values equal `"{{"` and `"}}"` respectively #### Scenario: default max depth - **WHEN** `DEFAULT_INTERPOLATION.maxDepth` is read - **THEN** the value equals `10` #### Scenario: type is narrowed via `as const` - **WHEN** `expectTypeOf().toEqualTypeOf<"{{">()` runs - **THEN** the type-level check passes **Verified by:** `packages/i18n/i18n-shared/src/constants/interpolation-defaults.test.ts::"default prefix is {{"` AND `packages/i18n/i18n-shared/src/constants/interpolation-defaults.test.ts::"default suffix is }}"` AND `packages/i18n/i18n-shared/src/constants/interpolation-defaults.test.ts::"default maxDepth is 10"` AND `packages/i18n/i18n-shared/src/constants/interpolation-defaults.test.ts::"DEFAULT_INTERPOLATION is narrowed via as const"` --- ### Requirement: I18N error code registry The package SHALL export `I18N_CODES` constructed via `defineCodes("I18N", [...] as const)` from `@suaveplan/error`, containing the following 18 codes: - Parser / interpolation / pluralization: `MISSING_KEY`, `INVALID_LOCALE`, `INVALID_OPTIONS`, `LOADER_FAILED`, `INTERPOLATION_ERROR`, `PLURAL_RULE_NOT_FOUND`, `NESTING_DEPTH_EXCEEDED` - Backends: `BACKEND_READ_FAILED`, `BACKEND_WRITE_FAILED`, `BACKEND_NOT_REGISTERED`, `BACKEND_LOCALE_NOT_AVAILABLE`, `BACKEND_TIMEOUT`, `CHAINED_BACKEND_ALL_FAILED` - Formatters: `INVALID_FORMATTER_OPTIONS`, `UNSUPPORTED_LOCALE`, `FORMATTER_CACHE_OVERFLOW` - Detector: `DETECTOR_CHAIN_EXHAUSTED` - Plugin / core: `PLUGIN_ALREADY_INSTALLED` Every code SHALL be addressable as `I18N_CODES.` and SHALL produce a string literal of the form `"I18N."`. The package SHALL also export `I18nErrorCode` as the discriminant union of all 18 string literals. #### Scenario: every documented code is in the registry - **WHEN** every name listed in this requirement is read from `I18N_CODES` - **THEN** every read returns a defined value #### Scenario: code values follow the I18N. prefix - **WHEN** `I18N_CODES.MISSING_KEY` is read - **THEN** the value equals `"I18N.MISSING_KEY"` #### Scenario: I18nErrorCode union has cardinality 18 - **WHEN** the type-level cardinality of `I18nErrorCode` is asserted - **THEN** the union covers exactly 18 string literals **Verified by:** `packages/i18n/i18n-shared/src/errors/error-codes.test.ts::"every documented code is in I18N_CODES"` AND `packages/i18n/i18n-shared/src/errors/error-codes.test.ts::"code values are namespaced under I18N."` AND `packages/i18n/i18n-shared/src/errors/error-codes.test.ts::"I18nErrorCode union has cardinality 18"` --- ### Requirement: I18nError class The package SHALL export `I18nError extends SuaveplanError` from `@suaveplan/error`. The class SHALL be constructible from any code in `I18N_CODES` and SHALL satisfy `instanceof I18nError` and `instanceof SuaveplanError`. The class SHALL preserve the cause chain when constructed with `{ cause }`. The class SHALL NOT carry any biome / linter suppression comments per CLAUDE.md §9. #### Scenario: construction with each code - **WHEN** `new I18nError(code, "test")` is called for every code in `I18N_CODES` - **THEN** every construction succeeds #### Scenario: instanceof discrimination - **WHEN** an `I18nError` is caught - **THEN** `instanceof I18nError` returns `true` AND `instanceof SuaveplanError` returns `true` #### Scenario: cause preservation - **WHEN** `new I18nError(I18N_CODES.LOADER_FAILED, "k", { cause: new Error("network") })` is constructed - **THEN** the resulting error's `.cause.message` equals `"network"` #### Scenario: no biome suppressions in source - **WHEN** the file `src/errors/i18n-error.ts` is grepped for `biome-ignore` - **THEN** zero matches are found **Verified by:** `packages/i18n/i18n-shared/src/errors/i18n-error.test.ts::"constructs with every code in I18N_CODES"` AND `packages/i18n/i18n-shared/src/errors/i18n-error.test.ts::"instanceof I18nError and SuaveplanError"` AND `packages/i18n/i18n-shared/src/errors/i18n-error.test.ts::"preserves cause chain"` AND `packages/i18n/i18n-shared/src/errors/i18n-error.test.ts::"source contains no biome-ignore suppressions"` --- ### Requirement: Subpath exports for tree-shaking The package SHALL declare subpath exports in `package.json` so consumers can import individual constants without pulling the entire bundle. The following subpaths SHALL resolve: - `@suaveplan/i18n-shared` — root barrel - `@suaveplan/i18n-shared/constants` — all constants - `@suaveplan/i18n-shared/constants/locale-bcp47-map` — `LOCALE_BCP47_MAP` only - `@suaveplan/i18n-shared/constants/rtl-locales` — `RTL_LOCALES` and `RTL_LOCALE_LIST` only - `@suaveplan/i18n-shared/constants/locale-descriptors` — `LOCALE_DESCRIPTORS`, `DEFAULT_LOCALES`, `DefaultLocale` only - `@suaveplan/i18n-shared/constants/interpolation-defaults` — `DEFAULT_INTERPOLATION` only - `@suaveplan/i18n-shared/errors` — `I18N_CODES`, `I18nError`, `I18nErrorCode` #### Scenario: every subpath resolves at type-check time - **WHEN** every documented subpath is imported in a consumer test fixture - **THEN** every import resolves with no `Module not found` errors **Verified by:** `packages/i18n/i18n-shared/src/index.test.ts::"every documented subpath export resolves"` --- ### Requirement: Side-effect-free package The package SHALL declare `"sideEffects": false` in `package.json` so bundlers can tree-shake unused exports. #### Scenario: package.json declares sideEffects false - **WHEN** `package.json` is parsed - **THEN** the `sideEffects` field equals `false` **Verified by:** `packages/i18n/i18n-shared/src/byte-size.test.ts::"package.json declares sideEffects false"`