# 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()` 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: "