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,109 @@
# `@suaveplan/i18n-icu-parser` — ICU parser, interpolation, pluralization
**Package**: `@suaveplan/i18n-icu-parser` (NEW)
**Status**: PROPOSED
**Type**: feature (new universal-tier package; verbatim port + tiny DI wedge)
**Date**: 2026-05-05
**Wave**: 2 (depends on Wave 1 foundation; required by Wave 6 core slim)
**Design doc**: `openspec/cross-cutting-designs/2026-05-05-i18n-modular-split.md` (especially §5b)
**Path**: `packages/i18n/i18n-icu-parser/`
## Why
The ICU MessageFormat parser, the `{{var}}` interpolation engine, and the 9-language plural-rule table currently live in three separate folders inside the `@suaveplan/i18n` monolith:
- `src/parser/message/` — AST + parser + renderer for ICU `{var, number, integer}`, `{count, plural, one{..} other{..}}`, `{kind, select, foo{..} other{..}}`
- `src/core/interpolation/` — `{{varname}}` substitution engine with `escapeHtmlChars`, `escapeRegex`, depth-limit guard
- `src/core/pluralization/` — `arabicPlural`, `englishPlural`, etc. (9 plural-rule functions matching CLDR categories)
These three modules have a single coherent purpose: **turn a localized template + values into a final string**. They share the `MessageNode` AST, the depth-limit invariant, the `INTERPOLATION_ERROR` / `PLURAL_RULE_NOT_FOUND` / `NESTING_DEPTH_EXCEEDED` error codes, and the same test fixtures. Splitting them into separate packages would fragment the seam without benefit. Bundling them as `@suaveplan/i18n-icu-parser` keeps the AST internal to one package and exposes a clean public surface (`MessageParser`, `InterpolationEngine`, `PluralizationEngine`, `renderNodes`) for the slim core to compose.
The one architectural wedge: today `parser/message/message-render.ts` imports `formatNumber` / `formatCurrency` / `formatDate` / `formatTime` directly. That would force the parser to depend on the formatter packages and invert Waves 2 and 3. The fix is a small DI seam: the parser declares a `FormatBridge` interface; the slim core wires up the bridge with the actual formatter packages in Wave 6. See §5b of the design doc.
## What changes
### New package: `@suaveplan/i18n-icu-parser`
- ADDED: `packages/i18n/i18n-icu-parser/` package scaffold (package.json, tsconfig.json, vite.config.ts, README.md).
- ADDED: `src/parser/message.ts` — `MessageParser` class, `extractVariables`, `MessageNode` AST. Source: `packages/i18n/i18n/src/parser/message/message.ts` verbatim.
- ADDED: `src/parser/message-render.ts` — `renderNodes` function. Source: `packages/i18n/i18n/src/parser/message/message-render.ts`. **Modified**: replaces direct `formatNumber` / `formatCurrency` / `formatDate` / `formatTime` imports with calls to a `FormatBridge` parameter on `RenderOptions`; provides a no-Intl fallback when bridge is undefined.
- ADDED: `src/parser/format-bridge.ts` — declares the `FormatBridge` interface (see design doc §5b) plus a `noopFormatBridge: FormatBridge` that coerces values to string (used by the parser's own tests; consumers in production wire a real bridge).
- ADDED: `src/parser/message.types.ts` — `MessageNode`, `PlainNode`, `VarNode`, `FormatNode`, `PluralNode`, `SelectNode`, `RenderOptions`, etc.
- ADDED: `src/parser/index.ts` — explicit named re-exports.
- ADDED: `src/interpolation/interpolation.ts` — `InterpolationEngine` class. Source: `packages/i18n/i18n/src/core/interpolation/interpolation.ts` verbatim minus the formatter imports (which were unused at the engine level).
- ADDED: `src/interpolation/interpolation.types.ts` — `InterpolationConfig`, `InterpolationOptions`, `InterpolationResult`, `InterpolationValues`, `ValueFormatter`.
- ADDED: `src/interpolation/escape-html.ts` — `escapeHtmlChars` standalone helper.
- ADDED: `src/interpolation/escape-regex.ts` — `escapeRegex` standalone helper.
- ADDED: `src/interpolation/index.ts` — explicit named re-exports.
- ADDED: `src/pluralization/pluralization.ts` — `PluralizationEngine` class + 9 plural-rule functions: `arabicPlural`, `englishPlural`, `welshPlural`, `russianPlural`, `polishPlural`, `chinesePlural`, `japanesePlural`, `frenchPlural`, `germanPlural`. Source: `packages/i18n/i18n/src/core/pluralization/pluralization.ts` verbatim.
- ADDED: `src/pluralization/pluralization.types.ts` — `PluralCategory` (`"zero" | "one" | "two" | "few" | "many" | "other"`), `PluralRuleFunction`, `PluralizationConfig`.
- ADDED: `src/pluralization/index.ts` — explicit named re-exports.
- ADDED: `src/index.ts` — explicit named re-exports of the three sub-namespaces.
### Subpath exports
- `.` — root barrel
- `./parser` — parser + render + format-bridge + types
- `./parser/format-bridge` — `FormatBridge` interface + `noopFormatBridge` only (single-import for slim-core wiring)
- `./interpolation` — interpolation engine + helpers + types
- `./pluralization` — `PluralizationEngine` + every plural-rule function + types
### Dependencies
- `@suaveplan/i18n-shared workspace:^` — for `I18N_CODES`, `I18nError`, `DEFAULT_INTERPOLATION`
- `@suaveplan/types workspace:^` — for shared catalog types if referenced
- `@suaveplan/error workspace:^` — for `defineCodes` consumer-side helpers (if any)
- `@opentelemetry/api catalog:observability` — for histogram emission in `MessageParser.parse` and `InterpolationEngine.interpolate`
- `tslib catalog:build`
- `zod catalog:forms` — for `InterpolationConfigSchema` boundary validation
- **No** dep on any formatter package, on `@suaveplan/i18n` core, or on `@suaveplan/i18n-detector`.
## Out of scope
- **Removing parser / interpolation / pluralization from `packages/i18n/i18n/src/`** — happens in Wave 6 (`i18n-core-slim`). For Waves 2–5, both copies coexist; the monolith remains green.
- **Backwards-compatible bridge implementation in the slim core** — that lives in Wave 6's change folder.
- **CLDR data import for additional plural rules** beyond the existing 9 — out of scope; new rules can be added in a follow-up.
- **Switching from hand-rolled MessageFormat parser to `intl-messageformat`** — explicit non-goal; the existing parser passes ICU conformance tests and avoids the 39 KiB external dep.
- **Async interpolation** — current engine is synchronous; async (e.g. for Promise-returning value resolvers) is a follow-up.
## Source mapping
| Requirement | Source file | Source lines | Notes |
|---|---|---|---|
| `MessageParser`, `extractVariables`, `MessageNode` AST | `packages/i18n/i18n/src/parser/message/message.ts` | full file | Verbatim port |
| `renderNodes` | `packages/i18n/i18n/src/parser/message/message-render.ts` | full file | Port; replace direct formatter imports with `FormatBridge` parameter |
| `MessageNode`, `PlainNode`, `VarNode`, `FormatNode`, `PluralNode`, `SelectNode`, `RenderOptions` types | `packages/i18n/i18n/src/parser/message/message.types.ts` | full file | Verbatim port |
| `InterpolationEngine` | `packages/i18n/i18n/src/core/interpolation/interpolation.ts` | full file minus formatter imports | Verbatim port |
| `escapeHtmlChars`, `escapeRegex` | `packages/i18n/i18n/src/core/interpolation/interpolation.ts` | matching exports | Split into per-helper files for tree-shaking |
| `InterpolationConfig`, `InterpolationOptions`, `InterpolationResult`, `InterpolationValues`, `ValueFormatter` types | `packages/i18n/i18n/src/core/interpolation/interpolation.types.ts` | full file | Verbatim port |
| `PluralizationEngine` + 9 plural-rule functions | `packages/i18n/i18n/src/core/pluralization/pluralization.ts` | full file | Verbatim port |
| `PluralCategory`, `PluralRuleFunction`, `PluralizationConfig` types | `packages/i18n/i18n/src/core/pluralization/pluralization.types.ts` | full file | Verbatim port |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| `FormatBridge` interface | New DI seam — see design doc §5b. Required so the parser package does not depend on formatter packages |
| `noopFormatBridge: FormatBridge` | Tests need a no-op bridge; consumers without Intl wrappers also benefit |
| Subpath export `./parser/format-bridge` | Slim-core wiring imports only this one file when constructing the default bridge |
## Verification approach
Each requirement carries a `**Verified by:**` line in the spec delta. Test files:
- `packages/i18n/i18n-icu-parser/src/parser/message.test.ts` — every ICU node type round-trips through parse + render; conformance with the existing monolith's parser test suite (verbatim test port)
- `packages/i18n/i18n-icu-parser/src/parser/message-render.test.ts` — `renderNodes` calls `formatBridge.number/currency/date/time/relativeTime` when supplied; falls back to `String(value)` when bridge is undefined; throws `INTERPOLATION_ERROR` on missing variable
- `packages/i18n/i18n-icu-parser/src/parser/format-bridge.test.ts` — `noopFormatBridge` returns string-coerced values for every method
- `packages/i18n/i18n-icu-parser/src/interpolation/interpolation.test.ts` — basic substitution, custom delimiters, depth-limit enforcement (`NESTING_DEPTH_EXCEEDED`), HTML escape on/off
- `packages/i18n/i18n-icu-parser/src/interpolation/escape-html.test.ts` — `<`, `>`, `&`, `"`, `'` all escaped
- `packages/i18n/i18n-icu-parser/src/interpolation/escape-regex.test.ts` — every regex meta char escaped
- `packages/i18n/i18n-icu-parser/src/pluralization/pluralization.test.ts` — every one of the 9 rule functions returns the correct CLDR category for boundary values; `PLURAL_RULE_NOT_FOUND` thrown for unknown locale; Welsh's six-form rule covered
- `packages/i18n/i18n-icu-parser/src/index.test.ts` — every documented export resolves through the root barrel
- Conformance test pass rate on the existing `packages/i18n/i18n/src/conformance.test.ts` — must remain 100% after Wave 6 wires the bridge through
## Impact
- New universal-tier package — joins `@suaveplan/i18n-shared` under `packages/i18n/`.
- `@suaveplan/i18n` slim core (Wave 6) consumes this package and wires `FormatBridge` against the formatter packages.
- No downstream consumer changes needed — the slim core re-export bridge surfaces every public symbol from this package under the `@suaveplan/i18n` root.
- `MANIFEST.md` and `CLAUDE.md` updated.

View File

@@ -0,0 +1,173 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec: i18n-icu-parser (i18n-icu-parser)
**Package**: `@suaveplan/i18n-icu-parser`
**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: ICU MessageFormat parser
The package SHALL export a `MessageParser` class that parses ICU MessageFormat templates into a `MessageNode[]` AST. Supported node kinds: plain text, `{var}` placeholders, `{var, format, style}` format placeholders, `{var, plural, one{...} other{...}}` plural placeholders, `{var, select, key{...} other{...}}` select placeholders, and arbitrarily-nested combinations. The parser SHALL throw `I18nError(I18N.INVALID_OPTIONS)` on malformed templates.
#### Scenario: parses a plain-text template
- **WHEN** `parser.parse("hello world")` is called
- **THEN** the result is a single-element array with kind `"plain"` and value `"hello world"`
#### Scenario: parses a variable placeholder
- **WHEN** `parser.parse("hello {name}")` is called
- **THEN** the result has two nodes: a plain `"hello "` and a var `"name"`
#### Scenario: parses a plural placeholder
- **WHEN** `parser.parse("{count, plural, one {# item} other {# items}}")` is called
- **THEN** the result has one plural node with two branches keyed `"one"` and `"other"`
#### Scenario: rejects malformed template
- **WHEN** `parser.parse("{unclosed")` is called
- **THEN** the call throws `I18nError` with code `"I18N.INVALID_OPTIONS"`
**Verified by:** `packages/i18n/i18n-icu-parser/src/parser/message.test.ts::"parses plain text"` AND `packages/i18n/i18n-icu-parser/src/parser/message.test.ts::"parses variable placeholder"` AND `packages/i18n/i18n-icu-parser/src/parser/message.test.ts::"parses plural placeholder"` AND `packages/i18n/i18n-icu-parser/src/parser/message.test.ts::"rejects unclosed brace"`
---
### Requirement: ICU render with FormatBridge dependency injection
The package SHALL export a `renderNodes(nodes, values, locale, options)` function that walks a `MessageNode[]` AST and produces a final string. Format placeholders (`{var, number}`, `{amount, number, currency=USD}`, `{date, date, short}`, `{ts, time, long}`, `{delta, relativeTime}`) SHALL be rendered by calling methods on `options.formatBridge` if supplied; if `formatBridge` is undefined OR the relevant method is undefined on the bridge, the renderer SHALL fall back to `String(value)` and SHALL NOT throw. Plural and select placeholders SHALL be resolved against the supplied `PluralizationEngine`. Missing variables SHALL throw `I18nError(I18N.INTERPOLATION_ERROR)`.
#### Scenario: format placeholder dispatches to bridge
- **GIVEN** a parsed AST containing a `{value, number}` node
- **AND** an `options.formatBridge` whose `number(v, l)` returns `"FORMATTED"`
- **WHEN** `renderNodes(ast, { value: 42 }, "en-GB", options)` is called
- **THEN** the result equals `"FORMATTED"`
#### Scenario: missing bridge falls back to string coercion
- **GIVEN** a parsed AST containing a `{value, number}` node
- **AND** `options.formatBridge` is undefined
- **WHEN** `renderNodes(ast, { value: 42 }, "en-GB", options)` is called
- **THEN** the result equals `"42"`
#### Scenario: bridge missing one method falls back to string
- **GIVEN** a parsed AST containing a `{date, date, short}` node
- **AND** `options.formatBridge.date` is undefined but `formatBridge.number` is defined
- **WHEN** `renderNodes(ast, { date: new Date(0) }, "en-GB", options)` is called
- **THEN** the result equals `"Thu Jan 01 1970 00:00:00 GMT+0000 (Coordinated Universal Time)"` (or platform-equivalent default `String(date)` form)
#### Scenario: missing variable throws
- **GIVEN** a parsed AST containing a `{name}` node
- **WHEN** `renderNodes(ast, {}, "en-GB", {})` is called
- **THEN** the call throws `I18nError` with code `"I18N.INTERPOLATION_ERROR"`
**Verified by:** `packages/i18n/i18n-icu-parser/src/parser/message-render.test.ts::"format placeholder dispatches to bridge"` AND `packages/i18n/i18n-icu-parser/src/parser/message-render.test.ts::"missing bridge falls back to String"` AND `packages/i18n/i18n-icu-parser/src/parser/message-render.test.ts::"bridge missing one method falls back"` AND `packages/i18n/i18n-icu-parser/src/parser/message-render.test.ts::"missing variable throws INTERPOLATION_ERROR"`
---
### Requirement: FormatBridge interface
The package SHALL export a `FormatBridge` interface declaring optional `number`, `currency`, `date`, `time`, `relativeTime` methods. Every method SHALL return `string`. The package SHALL also export a `noopFormatBridge: FormatBridge` whose every method returns `String(value)` (or `String(value[0])` for list-like inputs).
#### Scenario: noopFormatBridge implements FormatBridge
- **WHEN** `expectTypeOf(noopFormatBridge).toMatchTypeOf<FormatBridge>()` runs
- **THEN** the type-level check passes
#### Scenario: noopFormatBridge.number coerces to string
- **WHEN** `noopFormatBridge.number!(42, "en-GB")` is called
- **THEN** the result equals `"42"`
**Verified by:** `packages/i18n/i18n-icu-parser/src/parser/format-bridge.test.ts::"noopFormatBridge implements FormatBridge"` AND `packages/i18n/i18n-icu-parser/src/parser/format-bridge.test.ts::"noopFormatBridge.number coerces to string"`
---
### Requirement: `{{var}}` interpolation engine
The package SHALL export an `InterpolationEngine` class that performs `{{varname}}` substitution with configurable delimiters (defaults `{{` and `}}` per `DEFAULT_INTERPOLATION` from `@suaveplan/i18n-shared`). The engine SHALL enforce a `maxDepth` limit (default 10) on nested substitutions and SHALL throw `I18nError(I18N.NESTING_DEPTH_EXCEEDED)` on overflow. The engine SHALL HTML-escape values when `escapeHtml: true` is supplied.
#### Scenario: substitutes a single variable
- **WHEN** `engine.interpolate("hello {{name}}", { name: "Ada" })` is called
- **THEN** the result equals `"hello Ada"`
#### Scenario: enforces depth limit
- **GIVEN** values that recursively reference themselves via interpolation
- **WHEN** `engine.interpolate(template, values)` exceeds `maxDepth`
- **THEN** the call throws `I18nError` with code `"I18N.NESTING_DEPTH_EXCEEDED"`
#### Scenario: HTML escapes when configured
- **WHEN** `engine.interpolate("safe: {{html}}", { html: "<script>" }, { escapeHtml: true })` is called
- **THEN** the result equals `"safe: &lt;script&gt;"`
**Verified by:** `packages/i18n/i18n-icu-parser/src/interpolation/interpolation.test.ts::"substitutes single variable"` AND `packages/i18n/i18n-icu-parser/src/interpolation/interpolation.test.ts::"enforces maxDepth"` AND `packages/i18n/i18n-icu-parser/src/interpolation/interpolation.test.ts::"escapes HTML when configured"`
---
### Requirement: Pluralization with 9 CLDR rule sets
The package SHALL export a `PluralizationEngine` class and 9 plural-rule functions: `arabicPlural`, `englishPlural`, `welshPlural`, `russianPlural`, `polishPlural`, `chinesePlural`, `japanesePlural`, `frenchPlural`, `germanPlural`. Each function SHALL accept `(n: number)` and return one of the CLDR `PluralCategory` values (`"zero" | "one" | "two" | "few" | "many" | "other"`). The engine SHALL throw `I18nError(I18N.PLURAL_RULE_NOT_FOUND)` when asked for a rule with no matching locale.
#### Scenario: English returns "one" for 1 and "other" for 2
- **WHEN** `englishPlural(1)` and `englishPlural(2)` are called
- **THEN** the results are `"one"` and `"other"`
#### Scenario: Welsh covers all six categories
- **WHEN** `welshPlural` is called for boundary values 0, 1, 2, 3, 6, 11
- **THEN** the results cover `"zero"`, `"one"`, `"two"`, `"few"`, `"many"`, `"other"`
#### Scenario: Unknown locale rule throws
- **WHEN** `engine.pluralize("xx-XX", 1)` is called for an unregistered locale
- **THEN** the call throws `I18nError` with code `"I18N.PLURAL_RULE_NOT_FOUND"`
**Verified by:** `packages/i18n/i18n-icu-parser/src/pluralization/pluralization.test.ts::"English plural rule"` AND `packages/i18n/i18n-icu-parser/src/pluralization/pluralization.test.ts::"Welsh plural covers all six categories"` AND `packages/i18n/i18n-icu-parser/src/pluralization/pluralization.test.ts::"unknown locale throws PLURAL_RULE_NOT_FOUND"`
---
### Requirement: HTML and regex escape helpers
The package SHALL export `escapeHtmlChars(value: string): string` that escapes `<`, `>`, `&`, `"`, `'` to their HTML entity equivalents, and `escapeRegex(value: string): string` that escapes every regular-expression meta character.
#### Scenario: HTML escape covers all five chars
- **WHEN** `escapeHtmlChars("<a href=\"x\">'\\&'</a>")` is called
- **THEN** the result equals `"&lt;a href=&quot;x&quot;&gt;&#39;\\&amp;&#39;&lt;/a&gt;"`
#### Scenario: regex escape covers all meta chars
- **WHEN** `escapeRegex(".*+?^${}()|[]\\")` is called
- **THEN** the result equals `"\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\"`
**Verified by:** `packages/i18n/i18n-icu-parser/src/interpolation/escape-html.test.ts::"escapes all five HTML chars"` AND `packages/i18n/i18n-icu-parser/src/interpolation/escape-regex.test.ts::"escapes all regex meta chars"`
---
### Requirement: Subpath exports
The package SHALL declare subpath exports so consumers can import only the surfaces they need:
- `@suaveplan/i18n-icu-parser` — root barrel
- `@suaveplan/i18n-icu-parser/parser` — parser + render + format-bridge + types
- `@suaveplan/i18n-icu-parser/parser/format-bridge` — `FormatBridge` interface + `noopFormatBridge` only
- `@suaveplan/i18n-icu-parser/interpolation` — interpolation engine + helpers + types
- `@suaveplan/i18n-icu-parser/pluralization` — `PluralizationEngine` + every plural-rule function + types
#### Scenario: every subpath resolves
- **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-icu-parser/src/index.test.ts::"every documented subpath export resolves"`

View File

@@ -0,0 +1,87 @@
# Tasks — `@suaveplan/i18n-icu-parser` (ICU parser, interpolation, pluralization)
> Reconciled retroactively after Wave 2 shipped under commit `cd86db44`. Package is built, tested, and validated; this pass folds the spec delta into `openspec/specs/i18n-icu-parser/spec.md` and archives the change folder. All `**Verified by:**` paths in the spec have been confirmed to exist in `packages/i18n/i18n-icu-parser/src/`.
## 1. Spec authoring
- [x] 1.1 Cross-cutting design + AGENTS sections read
- [x] 1.2 Source-mapping table populated
- [x] 1.3 `proposal.md` authored
- [x] 1.4 `specs/i18n-icu-parser/spec.md` delta drafted with 7 requirements
- [x] 1.5 Every requirement carries a `**Verified by:**` line
- [x] 1.6 `openspec change validate i18n-icu-parser` clean
## 2. Package scaffold
- [x] 2.1 `packages/i18n/i18n-icu-parser/src/{parser,interpolation,pluralization,observability}/` structure
- [x] 2.2 `package.json` — `@suaveplan/i18n-icu-parser@0.0.0`, `type: module`, `sideEffects: false`, deps include `@suaveplan/i18n-shared`/`@suaveplan/error`/`@suaveplan/types`/`@opentelemetry/api`
- [x] 2.3 `tsconfig.lib.json` extends shared library config
- [x] 2.4 `vite.config.ts` + `vitest.config.ts`
- [x] 2.5 `README.md` with Modules table
## 3. Implementation — parser
- [x] 3.1 `src/parser/message.ts` — ICU MessageFormat AST parser (text / variable / plural / select / unclosed-brace rejection)
- [x] 3.2 `src/parser/message-render.ts` — AST → string with `FormatBridge` dispatch
- [x] 3.3 `src/parser/format-bridge.ts` — `FormatBridge` interface + `noopFormatBridge` fallback
## 4. Implementation — interpolation
- [x] 4.1 `src/interpolation/interpolation.ts` — variable substitution with `maxDepth`, escape options
- [x] 4.2 `src/interpolation/escape-html.ts` — 5-char HTML escape
- [x] 4.3 `src/interpolation/escape-regex.ts` — regex meta-char escape
## 5. Implementation — pluralization
- [x] 5.1 `src/pluralization/pluralization.ts` — CLDR plural rules across covered locales (English, Welsh, ...) with `PLURAL_RULE_NOT_FOUND` for unknown locales
## 6. Implementation — observability
- [x] 6.1 `src/observability/metrics.ts` — OTel counters/histograms
## 7. Root barrel + subpath exports
- [x] 7.1 `src/index.ts` — explicit named re-exports (no `export *`)
- [x] 7.2 `package.json` exports map covers root + parser/interpolation/pluralization subpaths
## 8. Documentation
- [x] 8.1 Sibling `.md` per implementation file
- [x] 8.2 README links every `.md`
## 9. Tests (runner-neutral imports)
- [x] 9.1 `parser/message.test.ts` — text/variable/plural/unclosed-brace
- [x] 9.2 `parser/message-render.test.ts` — bridge dispatch + fallback
- [x] 9.3 `parser/format-bridge.test.ts` — noop bridge
- [x] 9.4 `interpolation/interpolation.test.ts` — substitution, depth, escape
- [x] 9.5 `interpolation/escape-html.test.ts` + `escape-regex.test.ts`
- [x] 9.6 `pluralization/pluralization.test.ts` — English + Welsh + unknown-locale throw
- [x] 9.7 `index.test.ts` — every documented subpath resolves
- [x] 9.8 `observability/metrics.test.ts`
- [x] 9.9 Every `it(...)` carries `{ timeout: 5000 }`
- [x] 9.10 Runner-neutral imports — CLAUDE.md §11 grep clean
## 10. Pre-archive verification
- [x] 10.1 Biome clean
- [x] 10.2 `bunx turbo lint typecheck test --filter=@suaveplan/i18n-icu-parser` green at commit time
- [x] 10.3 `bunx turbo test:coverage --filter=@suaveplan/i18n-icu-parser` 100/100/100/100 at commit time
- [x] 10.4 Conformance script clean
- [x] 10.5 Build green
## 11. Pre-archive 7-row checklist
- [x] 11.1 Generics threaded — `FormatBridge` interface; AST nodes typed
- [x] 11.2 Zod schemas — N/A (parser operates on `string`; no boundary)
- [x] 11.3 Error codes — `INTERPOLATION_ERROR`, `PLURAL_RULE_NOT_FOUND`, `NESTING_DEPTH_EXCEEDED` consumed from `i18n-shared`
- [x] 11.4 Strict-mode tested
- [x] 11.5 OTel — counters + histograms
- [x] 11.6 Lifecycle dedup — N/A (pure functions)
- [x] 11.7 No retroactive tick — boxes ticked during reconciliation pass after honest audit
## 12. Archive
- [x] 12.1 Spec delta folded into `openspec/specs/i18n-icu-parser/spec.md` (canonical, status ACTIVE, 7 requirements)
- [x] 12.2 Folder moved to `openspec/archive/2026-05-09-i18n-icu-parser/`
- [x] 12.3 `MANIFEST.md` already lists the package