# Capability Spec Delta: i18n (i18n-core-slim, Wave 6) **Package**: `@suaveplan/i18n` **Status**: PROPOSED > Every requirement below carries a `**Verified by:**` line at authoring time. (See `openspec/AGENTS.md` Section 13.) ## REMOVED Requirements The following requirements MOVE to sibling packages. The capability still holds at the family level but is no longer hosted by this package directly. Each removal carries a `**Now hosted by:**` pointer so the conformance gate can find the new home. ### Requirement: BCP-47 locale tag map exposed by `@suaveplan/i18n` **REMOVED.** The `LOCALE_BCP47_MAP` constant is no longer hosted by `@suaveplan/i18n` directly; it is re-exported from `@suaveplan/i18n-shared/constants/locale-bcp47-map` via the back-compat root barrel. **Now hosted by:** `@suaveplan/i18n-shared` (capability spec: `openspec/specs/i18n-shared/spec.md`) --- ### Requirement: RTL locale set exposed by `@suaveplan/i18n` **REMOVED.** The `RTL_LOCALES` and `RTL_LOCALE_LIST` constants move to `@suaveplan/i18n-shared`. Re-exported via root barrel for back-compat. **Now hosted by:** `@suaveplan/i18n-shared` --- ### Requirement: Locale descriptors exposed by `@suaveplan/i18n` **REMOVED.** `LOCALE_DESCRIPTORS`, `DEFAULT_LOCALES`, `DefaultLocale` move to `@suaveplan/i18n-shared`. **Now hosted by:** `@suaveplan/i18n-shared` --- ### Requirement: I18N error code registry hosted by `@suaveplan/i18n` **REMOVED.** `I18N_CODES`, `I18nError`, `I18nErrorCode` (formerly `I18nCode`) move to `@suaveplan/i18n-shared`. Re-exported via root barrel for back-compat. The legacy `I18nCode` name is preserved as a type alias for `I18nErrorCode`. **Now hosted by:** `@suaveplan/i18n-shared` --- ### Requirement: ICU MessageFormat parser hosted by `@suaveplan/i18n` **REMOVED.** `MessageParser`, `extractVariables`, `renderNodes`, every `MessageNode` AST type, every `ParserOptions` type move to `@suaveplan/i18n-icu-parser`. Re-exported via root barrel. **Now hosted by:** `@suaveplan/i18n-icu-parser` (capability spec: `openspec/specs/i18n-icu-parser/spec.md`) --- ### Requirement: `{{var}}` interpolation engine hosted by `@suaveplan/i18n` **REMOVED.** `InterpolationEngine`, `escapeHtmlChars`, `escapeRegex`, every `Interpolation*` type move to `@suaveplan/i18n-icu-parser/interpolation`. Re-exported via root barrel. **Now hosted by:** `@suaveplan/i18n-icu-parser` --- ### Requirement: Pluralization with 9 CLDR rule sets hosted by `@suaveplan/i18n` **REMOVED.** `PluralizationEngine`, every plural-rule function (`arabicPlural`, `chinesePlural`, `defaultPluralRule`, `englishPlural`, `frenchPlural`, `germanPlural`, `polishPlural`, `russianPlural`, `spanishPlural`, `welshPlural`), `getBaseLocale`, `normalizeLocale`, every `Plural*` type move to `@suaveplan/i18n-icu-parser/pluralization`. Re-exported via root barrel. **Now hosted by:** `@suaveplan/i18n-icu-parser` --- ### Requirement: Number / RelativeTime / List / DisplayName / Date / Currency formatters hosted by `@suaveplan/i18n` **REMOVED.** Every formatter and helper moves to its dedicated `@suaveplan/i18n-formatter-` package. The current root barrel exposes `formatNumber`, `formatRelativeTime`, `formatList`, `formatDisplayName`, `formatDate`, `formatTime`, `formatDateOnly`, `formatDateRange`, `formatCurrency`, `formatCurrencyValue`, `getCurrencyInfo`, `getCurrencyName`, `getCurrencySymbol`, `getDayNames`, `getMonthNames`. Every one of these is re-exported via the back-compat root barrel. Two new formatters (`Collator`, `Segmenter`) are ADDED at the family level by Wave 3 and are also re-exported via the slim core's root barrel. **Now hosted by:** `@suaveplan/i18n-formatter-number`, `@suaveplan/i18n-formatter-relative-time`, `@suaveplan/i18n-formatter-list`, `@suaveplan/i18n-formatter-display-name`, `@suaveplan/i18n-formatter-collator`, `@suaveplan/i18n-formatter-segmenter`, `@suaveplan/i18n-formatter-date`, `@suaveplan/i18n-formatter-currency` --- ### Requirement: Locale detection chain hosted by `@suaveplan/i18n` **REMOVED.** `createDetectorChain`, `DETECTOR_CHAIN_UNRESOLVED`, every `Detector*` type, every individual detector class move to `@suaveplan/i18n-detector`. Re-exported via root barrel and via the `./detector` subpath. **Now hosted by:** `@suaveplan/i18n-detector` --- ### Requirement: Translation backends hosted by `@suaveplan/i18n` **REMOVED.** `MemoryBackend`, `HttpBackend`, `StorageBackend`, `MultiFileFsBackend`, `ChainedBackend`, every backend factory function, every backend options type move to `@suaveplan/i18n-backend-` packages. Universal-tier backends are re-exported via the slim core's root barrel; the server-tier `MultiFileFsBackend` is NOT re-exported and consumers must import directly from `@suaveplan/i18n-backend-fs-multifile`. **Now hosted by:** `@suaveplan/i18n-backend-memory`, `@suaveplan/i18n-backend-http`, `@suaveplan/i18n-backend-storage`, `@suaveplan/i18n-backend-fs-multifile`, `@suaveplan/i18n-backend-chained` --- ## MODIFIED Requirements ### Requirement: ICU MessageFormat render integrates formatters via FormatBridge **MODIFIED.** Pre-split, `MessageCatalog` and `I18nInstance` invoked formatters by direct function import inside `parser/message/message-render.ts`. Post-split, the parser package declares a `FormatBridge` interface and accepts a bridge as a render option. The slim `@suaveplan/i18n` core SHALL construct a `defaultFormatBridge` by importing the formatter packages and wiring them to the bridge methods, then SHALL pass that bridge to every internal `renderNodes` call. Consumers calling `createI18n(...)` SHALL get the default bridge automatically; consumers MAY pass `{ formatBridge: customBridge }` to override. #### Scenario: createI18n auto-wires the default FormatBridge - **WHEN** `const i18n = createI18n({ catalog: { ... } })` is called with no `formatBridge` option - **AND** a translated key `t("price", { value: 42, currency: "USD" })` resolves to `"{value, number, currency=USD}"` - **THEN** the rendered string equals `"$42.00"` (or platform-locale-equivalent) #### Scenario: custom FormatBridge overrides the default - **WHEN** `const i18n = createI18n({ catalog, formatBridge: { number: () => "OVERRIDE" } })` is called - **AND** a translated key resolving to `{value, number}` is rendered - **THEN** the rendered string equals `"OVERRIDE"` #### Scenario: omitting a method on a custom bridge falls back to String coercion - **WHEN** `const i18n = createI18n({ catalog, formatBridge: { number: () => "X" } })` is called (no `currency` method) - **AND** a translated key resolving to `{amount, number, currency=USD}` is rendered - **THEN** the rendered string equals `"42"` (string coercion fallback per `i18n-icu-parser` spec) **Verified by:** `packages/i18n/i18n/src/conformance.test.ts::"createI18n auto-wires defaultFormatBridge"` AND `packages/i18n/i18n/src/conformance.test.ts::"custom formatBridge overrides default"` AND `packages/i18n/i18n/src/conformance.test.ts::"missing bridge method falls back to String"` --- ### Requirement: Public surface preserved via back-compat root barrel **MODIFIED.** Pre-split, `packages/i18n/i18n/src/index.ts` exported 33 statements totalling roughly 80 distinct symbols, all sourced from local files. Post-split, every one of those 80 symbols SHALL continue to resolve through `import { X } from "@suaveplan/i18n"`, but the *source* of every constant / formatter / parser / backend / detector symbol changes to the relevant Wave 1–5 package. The back-compat re-export map in `proposal.md` enumerates every symbol's pre/post source. #### Scenario: every documented pre-split export resolves post-slim - **WHEN** every symbol enumerated in the back-compat re-export map is imported via `from "@suaveplan/i18n"` - **THEN** every import resolves with `typeof !== "undefined"` #### Scenario: subpath exports continue to resolve - **WHEN** every documented subpath (`@suaveplan/i18n/detector`, `@suaveplan/i18n/plugin`, `@suaveplan/i18n/parser`, `@suaveplan/i18n/errors`) is imported in a consumer fixture - **THEN** every import resolves with no `Module not found` errors #### Scenario: legacy I18nCode name is preserved as type alias - **WHEN** a downstream consumer writes `import type { I18nCode } from "@suaveplan/i18n";` - **THEN** the import resolves and the type equals `I18nErrorCode` **Verified by:** `packages/i18n/i18n/src/index.test.ts::"every back-compat re-export resolves"` AND `packages/i18n/i18n/src/index.test.ts::"every documented subpath export resolves"` AND `packages/i18n/i18n/src/index.test.ts::"legacy I18nCode equals I18nErrorCode"` --- ### Requirement: Backend factories exposed at root barrel **MODIFIED.** Pre-split, backend classes (`MemoryBackend`, `HttpBackend`, `StorageBackend`, `MultiFileFsBackend`, `ChainedBackend`) and their factories (`memoryBackend`, `httpBackend`, `storageBackend`, `chainedBackend`) were exported only via `./core/backend/*` subpaths. Post-split, the four universal-tier backends (memory, http, storage, chained) SHALL be exposed at the root barrel for parity with formatter and detector exports. The server-tier `MultiFileFsBackend` SHALL NOT be exposed at the root barrel and consumers must import directly from `@suaveplan/i18n-backend-fs-multifile`. #### Scenario: universal backends importable from root - **WHEN** `import { MemoryBackend, memoryBackend, HttpBackend, httpBackend, StorageBackend, storageBackend, ChainedBackend, chainedBackend } from "@suaveplan/i18n";` is evaluated - **THEN** every symbol resolves #### Scenario: server-tier backend NOT importable from root - **WHEN** `import { MultiFileFsBackend } from "@suaveplan/i18n";` is evaluated - **THEN** TypeScript reports `Module '"@suaveplan/i18n"' has no exported member 'MultiFileFsBackend'` **Verified by:** `packages/i18n/i18n/src/index.test.ts::"4 universal-tier backend factories importable from root"` AND `packages/i18n/i18n/src/index.test.ts::"MultiFileFsBackend NOT exported from root"` --- ## ADDED Requirements ### Requirement: Default FormatBridge factory wires formatter packages The package SHALL author `src/parser/format-bridge-default.ts` exporting `defaultFormatBridge: FormatBridge`. This bridge SHALL import `formatNumber`, `formatCurrency`, `formatDate`, `formatTime`, `formatRelativeTime` from their respective Wave 3 packages and bind them to the corresponding `FormatBridge` methods. The bridge SHALL be passed automatically by `createI18n` when no override is supplied. #### Scenario: defaultFormatBridge implements every FormatBridge method - **WHEN** `expectTypeOf(defaultFormatBridge).toEqualTypeOf>()` runs - **THEN** the type-level check passes (every optional method is implemented) #### Scenario: defaultFormatBridge.number calls into i18n-formatter-number - **WHEN** `defaultFormatBridge.number(1234.5, "en-GB", "decimal")` is called - **THEN** the result equals `formatNumber(1234.5, "en-GB", { style: "decimal" })` **Verified by:** `packages/i18n/i18n/src/parser/format-bridge-default.test.ts::"defaultFormatBridge implements every FormatBridge method"` AND `packages/i18n/i18n/src/parser/format-bridge-default.test.ts::"defaultFormatBridge.number forwards to i18n-formatter-number"` --- ### Requirement: Slim-core source inventory enforced The package SHALL contain ONLY the following directories and files under `src/`: - `src/core/catalog/` — `MessageCatalog`, `deepMerge`, types, schemas, tests, docs - `src/core/plugin/` — `createI18n`, `I18nInstance`, types, tests, docs - `src/locale/negotiation/` — `negotiateLocale`, `parseAcceptLanguage`, types, tests, docs - `src/locale/resolver/` — `LocaleResolver`, `extractLanguage`, `extractRegion`, `normalizeLocaleCode`, types, tests, docs - `src/middleware/server/` — `createLocaleMiddleware`, `detectLocale`, `getDirection`, types, tests, docs - `src/observability/` — detector-metrics state and OTel adapters - `src/schemas/` — `CatalogOptionsSchema`, `InterpolationConfigSchema`, `ResolverConfigSchema`, `TranslationRecordSchema`, schema input types - `src/utils/` — key utilities (`buildKey`, `countKeys`, `flattenRecord`, `getNestedValue`, `getTextDirection`, `hasKey`, `isRtl`, `parseKey`, `sanitize`, `unflattenRecord`) - `src/parser/format-bridge-default.ts` — the wiring file added by this change - `src/conformance.test.ts` — end-to-end conformance gate - `src/index.ts` — back-compat root barrel - `src/index.test.ts` — exhaustive symbol-existence test The package SHALL NOT contain `src/constants/`, `src/error-codes.ts`, `src/errors/`, `src/parser/message/`, `src/core/interpolation/`, `src/core/pluralization/`, `src/formatting/`, `src/detector/`, `src/core/backend/`. A test SHALL enforce this inventory. #### Scenario: forbidden directories absent - **WHEN** the source tree under `packages/i18n/i18n/src/` is enumerated - **THEN** none of `constants`, `error-codes.ts`, `errors`, `formatting`, `detector` exist; the only `parser/` entry is `format-bridge-default.ts` and its sibling `.md` / `.test.ts` **Verified by:** `packages/i18n/i18n/src/source-inventory.test.ts::"forbidden directories absent post-slim"` AND `packages/i18n/i18n/src/source-inventory.test.ts::"only allowed directories present"` --- ### Requirement: Workspace deps include every Wave 1–5 universal package The package's `package.json` `dependencies` SHALL include every Wave 1–5 package as `workspace:^` except the server-tier `@suaveplan/i18n-backend-fs-multifile`. The fs-multifile backend SHALL be either omitted entirely or declared as `optionalPeerDependencies`. #### Scenario: 15 workspace deps present - **WHEN** `packages/i18n/i18n/package.json` is parsed - **THEN** `dependencies` includes the 15 packages enumerated in proposal.md §"Added workspace deps" #### Scenario: fs-multifile NOT in dependencies - **WHEN** `packages/i18n/i18n/package.json` `dependencies` is checked for `@suaveplan/i18n-backend-fs-multifile` - **THEN** the key is absent from `dependencies` (may appear under `optionalPeerDependencies` or be entirely omitted) **Verified by:** `packages/i18n/i18n/src/source-inventory.test.ts::"15 universal Wave 1-5 packages declared as workspace deps"` AND `packages/i18n/i18n/src/source-inventory.test.ts::"fs-multifile NOT in universal-tier dependencies"` --- ### Requirement: Bundle size reduction documented The package SHALL document its post-slim bundle size in `README.md` and SHALL show a meaningful reduction (target: ≥ 60% smaller than the pre-split monolith bundle). The pre-split and post-slim sizes SHALL be captured in tasks.md §4.4 and §5.16 and reported in the change folder's archive entry. #### Scenario: README documents bundle size - **WHEN** `README.md` is read - **THEN** the file contains a "Bundle size" section with a number in KB and a date **Verified by:** Manual inspection of `packages/i18n/i18n/README.md` during pre-archive review (per AGENTS.md §13)