# `@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.