Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View 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.

View File

@@ -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"`

View 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)