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,240 @@
<!-- markdownlint-disable MD024 -->
# 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-<name>` 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-<name>` 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 <symbol> !== "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<Required<FormatBridge>>()` 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)