Initial Commit
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# 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<string, string>` 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<string>` 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<string, { name: string; nativeName: string; direction: "ltr" | "rtl" }>` 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<typeof DEFAULT_INTERPOLATION.prefix>().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.<NAME>` and SHALL produce a string literal of the form `"I18N.<NAME>"`. 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<I18nErrorCode>` 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"`
|
||||
Reference in New Issue
Block a user