Files
suaveplan/openspec/archive/2026-05-09-i18n-core-slim/specs/i18n/spec.md
2026-05-27 23:29:53 +01:00

15 KiB
Raw Blame History

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)