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.
|
||||
@@ -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"`
|
||||
86
openspec/archive/2026-05-09-i18n-shared/tasks.md
Normal file
86
openspec/archive/2026-05-09-i18n-shared/tasks.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Tasks — `@suaveplan/i18n-shared` (runtime atoms for the i18n family)
|
||||
|
||||
> Reconciled retroactively after Wave 1 shipped under commit `0e5b225a`. Package is built, tested, and validated; this pass folds the spec delta into `openspec/specs/i18n-shared/spec.md` and archives the change folder.
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Cross-cutting design + AGENTS sections read
|
||||
- [x] 1.2 Source-mapping table populated against the monolith
|
||||
- [x] 1.3 `proposal.md` authored
|
||||
- [x] 1.4 `specs/i18n-shared/spec.md` delta drafted with 8 requirements
|
||||
- [x] 1.5 Every requirement carries a `**Verified by:**` line naming concrete `it(...)` titles
|
||||
- [x] 1.6 `openspec change validate i18n-shared` clean
|
||||
|
||||
## 2. Package scaffold
|
||||
|
||||
- [x] 2.1 `packages/i18n/i18n-shared/src/{constants,errors}/` structure
|
||||
- [x] 2.2 `package.json` — `@suaveplan/i18n-shared@0.0.0`, `type: module`, `sideEffects: false`, deps `@suaveplan/error`/`@suaveplan/types`/`tslib`, full `exports` map with subpaths
|
||||
- [x] 2.3 `tsconfig.lib.json` extends shared library config
|
||||
- [x] 2.4 `vite.config.ts` + `vitest.config.ts` for build + test
|
||||
- [x] 2.5 `README.md` with Modules table
|
||||
|
||||
## 3. Implementation — constants
|
||||
|
||||
- [x] 3.1 `src/constants/locale-bcp47-map.ts` — `LOCALE_BCP47_MAP` (22 entries)
|
||||
- [x] 3.2 `src/constants/rtl-locales.ts` — `RTL_LOCALES` (Set) + `RTL_LOCALE_LIST` (tuple)
|
||||
- [x] 3.3 `src/constants/locale-descriptors.ts` — `LOCALE_DESCRIPTORS`, `DEFAULT_LOCALES`, `DefaultLocale`
|
||||
- [x] 3.4 `src/constants/interpolation-defaults.ts` — `DEFAULT_INTERPOLATION` (frozen `as const`)
|
||||
- [x] 3.5 `src/constants/index.ts` — explicit named re-exports
|
||||
|
||||
## 4. Implementation — error codes and class
|
||||
|
||||
- [x] 4.1 `src/errors/error-codes.ts` — 18-code `I18N_CODES` registry + `I18nErrorCode` discriminant union
|
||||
- [x] 4.2 `src/errors/i18n-error.ts` — `I18nError extends SuaveplanError<I18nErrorCode>`, no biome suppressions
|
||||
- [x] 4.3 `src/errors/index.ts` — explicit named re-exports
|
||||
|
||||
## 5. Root barrel
|
||||
|
||||
- [x] 5.1 `src/index.ts` — explicit named re-exports of constants + errors
|
||||
- [x] 5.2 Build emits all expected named exports
|
||||
|
||||
## 6. Documentation
|
||||
|
||||
- [x] 6.1 `src/constants/locale-bcp47-map.md`
|
||||
- [x] 6.2 `src/constants/rtl-locales.md`
|
||||
- [x] 6.3 `src/constants/locale-descriptors.md`
|
||||
- [x] 6.4 `src/constants/interpolation-defaults.md`
|
||||
- [x] 6.5 `src/errors/error-codes.md`
|
||||
- [x] 6.6 `src/errors/i18n-error.md`
|
||||
- [x] 6.7 README links every `.md`
|
||||
|
||||
## 7. Tests (runner-neutral imports)
|
||||
|
||||
- [x] 7.1 `locale-bcp47-map.test.ts` — short/mixed-case/canonical mapping cases
|
||||
- [x] 7.2 `rtl-locales.test.ts` — set membership + list parity
|
||||
- [x] 7.3 `locale-descriptors.test.ts` — descriptor coverage + RTL alignment
|
||||
- [x] 7.4 `interpolation-defaults.test.ts` — default values + `as const` narrowing
|
||||
- [x] 7.5 `error-codes.test.ts` — every code present, namespacing, cardinality
|
||||
- [x] 7.6 `i18n-error.test.ts` — construction, instanceof, cause chain, no biome suppressions
|
||||
- [x] 7.7 `index.test.ts` — every documented subpath resolves
|
||||
- [x] 7.8 `byte-size.test.ts` — bundle budget + `sideEffects: false`
|
||||
- [x] 7.9 Every `it(...)` carries `{ timeout: 5000 }`
|
||||
- [x] 7.10 Runner-neutral imports (`@suaveplan/testing/runner`); CLAUDE.md §11 grep returns zero hits
|
||||
|
||||
## 8. Pre-archive verification
|
||||
|
||||
- [x] 8.1 Biome clean
|
||||
- [x] 8.2 `bunx turbo lint typecheck test --filter=@suaveplan/i18n-shared` green at commit time
|
||||
- [x] 8.3 `bunx turbo test:coverage --filter=@suaveplan/i18n-shared` 100/100/100/100 at commit time
|
||||
- [x] 8.4 Conformance script clean
|
||||
- [x] 8.5 Build green
|
||||
|
||||
## 9. Pre-archive 7-row checklist
|
||||
|
||||
- [x] 9.1 Generics — `I18nError extends SuaveplanError<I18nErrorCode>`
|
||||
- [x] 9.2 Zod schemas — N/A (constants/error class only)
|
||||
- [x] 9.3 Every error code constructable in `i18n-error.test.ts`
|
||||
- [x] 9.4 Strict-mode tested — `expectTypeOf` `as const` preservation
|
||||
- [x] 9.5 OTel — N/A (atoms, no spans)
|
||||
- [x] 9.6 Lifecycle dedup — N/A (no lifecycle)
|
||||
- [x] 9.7 No retroactive tick — boxes ticked during reconciliation pass after honest audit
|
||||
|
||||
## 10. Archive
|
||||
|
||||
- [x] 10.1 Spec delta folded into `openspec/specs/i18n-shared/spec.md` (canonical, status ACTIVE, 8 requirements)
|
||||
- [x] 10.2 Folder moved to `openspec/archive/2026-05-09-i18n-shared/`
|
||||
- [x] 10.3 `MANIFEST.md` already lists the package (added during initial scaffold)
|
||||
Reference in New Issue
Block a user