Initial Commit
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
# OpenSpec Change: i18n-spec-conformance
|
||||
|
||||
- **Change ID**: i18n-spec-conformance
|
||||
- **Package**: `@suaveplan/i18n`
|
||||
- **Status**: PROPOSED
|
||||
- **Date**: 2026-04-26
|
||||
- **Reference**: `openspec/archive/2026-04-19-enterprise-i18n/specs/i18n/spec.md`
|
||||
|
||||
## Summary
|
||||
|
||||
Close the conformance gaps between the shipped `@suaveplan/i18n` package and its archived enterprise spec, and add type-level guarantees that make the package future-proof for consumers. The original change archived as "all tasks retroactively ticked" but several spec contracts are unimplemented or drifted: OTel instrumentation is absent, `MessageCatalog` lacks strict mode / loader dedup / namespace-aware `set`/`load`/`keys` / `LookupResult.source`, schemas don't match spec field names and aren't enforced at construction, `InterpolationEngine` never throws `I18N.INTERPOLATION_ERROR`, `PluralizationEngine` lacks strict mode and a CLDR-backed fallback, and `LocaleResolver.normalizeLocaleCode` accepts any string instead of validating BCP-47.
|
||||
|
||||
This change brings the package fully in line with the original spec, adds three type-safety upgrades (typed key inference via `DotPaths<TMessages>`, generic `LookupResult<TLocale>`, `Intl.PluralRules` fallback so new CLDR languages work without a code change), and re-establishes the 100/100/100/100 coverage gate.
|
||||
|
||||
## Current-state audit
|
||||
|
||||
| Spec ID | Requirement | Status |
|
||||
|---------|-------------|--------|
|
||||
| I18N-01 | `I18nError` + 5 codes, side-effect register | ✅ codes registered; ⚠️ no side-effect import in `src/index.ts` |
|
||||
| I18N-02 | Zod schemas match spec shape, parse at boundary | ❌ field names drifted; ❌ never called at construction |
|
||||
| I18N-03 | `MessageCatalog` contract (`source`, `namespace`, strict, dedup, ns-aware `set`/`load`/`keys`) | ❌ all missing |
|
||||
| I18N-04 | `InterpolationEngine` throws on unclosed delim / formatter error | ❌ never throws |
|
||||
| I18N-05 | `PluralizationEngine` strict-mode throw | ❌ missing |
|
||||
| I18N-06 | `MessageParser` AST + `extractVariables` + `renderNodes` | ✅ shipped |
|
||||
| I18N-07 | Pluggable strategy chain, `ResolvedLocale.direction`, `INVALID_LOCALE` throw on garbage input | ⚠️ chain lives in `middleware/server` (acceptable rename); ❌ `normalizeLocaleCode` never throws |
|
||||
| I18N-08 | Middleware attaches `ResolvedLocale`, sets `Content-Language` | ⚠️ attaches result; missing `Content-Language` header path |
|
||||
| I18N-09 | Date formatting | ✅ shipped |
|
||||
| I18N-10 | Currency formatting | ✅ shipped |
|
||||
| I18N-11 | Utility functions | ✅ shipped |
|
||||
| I18N-12 | OTel meter + counters + histogram | ❌ entirely missing |
|
||||
| I18N-13 | Property tests | ⚠️ inline in unit tests instead of `tests/properties/` |
|
||||
| I18N-14 | Per-component benchmarks | ⚠️ bundled into single file |
|
||||
| I18N-15 | Hygiene (sideEffects, 100% coverage, no .skip/.only) | ✅ shipped |
|
||||
|
||||
## New additions for this change
|
||||
|
||||
### Type-level guarantees (future-proofing)
|
||||
|
||||
- `DotPaths<TMessages>` template-literal type that recursively projects a nested `TranslationRecord` tree to its `'a.b.c'` leaf paths.
|
||||
- `MessageCatalog<TLocale, TMessages>.t<K extends DotPaths<TMessages> | (string & {})>(key: K)` — autocomplete + compile-time key safety, with a string escape hatch.
|
||||
- `LookupResult<TLocale extends string = string>` carries the typed locale through.
|
||||
- `PluralizationEngine` falls back to `Intl.PluralRules(locale).select(n)` after locale-specific rule + base-language rule miss but before strict-mode throw, so CLDR drift no longer requires code changes for newly added languages.
|
||||
|
||||
### OTel instrumentation
|
||||
|
||||
`src/observability/metrics.ts` exporting an `i18nMetrics` singleton built via `getMeter('suaveplan.i18n')`:
|
||||
|
||||
- Counter `i18n.translate.total` — labels: `outcome` (`'hit' | 'miss'`), `locale`
|
||||
- Counter `i18n.locale.resolve.total` — labels: `strategy`, `locale`
|
||||
- Histogram `i18n.translate.duration_ms` — label: `locale`
|
||||
|
||||
Wired into `MessageCatalog.get` and `detectLocale` (locale middleware).
|
||||
|
||||
### Strict mode + structured throws
|
||||
|
||||
- `MessageCatalog` accepts `strict?: boolean` (default `false`); when on, missing keys throw `I18nError(I18N.MISSING_KEY)`.
|
||||
- `MessageCatalog.load(locale, namespace?, loader)` now wraps loader rejections as `I18nError(I18N.LOADER_FAILED)` with the original error in `cause`.
|
||||
- `MessageCatalog.load` deduplicates concurrent calls for the same `(locale, namespace)` pair via an in-flight `Map<string, Promise<void>>`.
|
||||
- `MessageCatalog.set(locale, namespace, record)` and `MessageCatalog.keys(locale, namespace?)` accept a namespace argument.
|
||||
- `LookupResult` becomes `{ value: string | undefined; locale: TLocale; namespace: string; source: 'catalog' | 'fallback' | 'handler' | 'key' }`.
|
||||
- `InterpolationEngine` throws `I18nError(I18N.INTERPOLATION_ERROR)` on unclosed delimiter and rethrows formatter exceptions with the same code wrapping the cause.
|
||||
- `PluralizationEngine` accepts `strict?: boolean`; when on, throws `I18nError(I18N.PLURAL_RULE_NOT_FOUND)` after rule + base-locale + `Intl.PluralRules` fallback all miss.
|
||||
- `normalizeLocaleCode(code)` validates a BCP-47-shaped tag (`/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/`) and throws `I18nError(I18N.INVALID_LOCALE)` otherwise.
|
||||
|
||||
### Schema realignment
|
||||
|
||||
- `InterpolationConfigSchema` → `{ open, close, escape, maxIterations }` (renamed from `prefix/suffix/escapeHtml/maxDepth`). `InterpolationEngine` constructor accepts both shapes for one minor cycle, with the new shape canonical.
|
||||
- `ResolverConfigSchema` → `{ strategies: ResolutionStrategy[]; fallbackLocale: string; supportedLocales?: string[] }`.
|
||||
- `CatalogOptionsSchema` adds `strict?: boolean`.
|
||||
- All three engines call `.parse(options)` in their constructor; failures throw `I18nError` with the relevant code and the Zod issue list in `context.zodIssues`.
|
||||
|
||||
### Side-effect code registration
|
||||
|
||||
`src/index.ts` adds a top-level `import './error-codes.js';` so consumers that import schemas only still trigger code registration.
|
||||
|
||||
## Available workspace deps
|
||||
|
||||
| Package | Available |
|
||||
|---------|-----------|
|
||||
| `@suaveplan/error` | ✅ |
|
||||
| `@suaveplan/types` | ✅ |
|
||||
| `@opentelemetry/api` (catalog: `observability`) | ✅ |
|
||||
|
||||
## Impact
|
||||
|
||||
- **Breaking**: `LookupResult` shape, `InterpolationEngine` option keys, `MessageCatalog.load` signature, `MessageCatalog.addMessages` renamed to `set` (alias retained for one cycle).
|
||||
- Version stays at `0.1.0` (genesis pre-1.0).
|
||||
- No external consumers on genesis yet; safe to land as-is.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Capability Spec Delta: i18n (spec-conformance)
|
||||
|
||||
**Package**: `@suaveplan/i18n`
|
||||
**Status**: PROPOSED
|
||||
|
||||
---
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### I18N-02 — Zod schemas at the public boundary
|
||||
|
||||
`InterpolationConfigSchema`, `ResolverConfigSchema`, and `CatalogOptionsSchema` MUST match the spec field names and MUST be invoked at construction time:
|
||||
|
||||
- `InterpolationConfigSchema` validates `{ open: string (≥1, ≠close), close: string (≥1), escape: string (length 0 or 1), maxIterations: int 1..100 }`.
|
||||
- `ResolverConfigSchema` validates `{ strategies: ResolutionStrategy[] (non-empty), fallbackLocale: string (≥1), supportedLocales?: string[] }`.
|
||||
- `CatalogOptionsSchema` adds `strict?: boolean` (default `false`).
|
||||
|
||||
`MessageCatalog`, `InterpolationEngine`, and `LocaleResolver` constructors MUST call `.parse(options)` and re-throw failures as `I18nError` with the relevant code (`I18N.INVALID_LOCALE` for resolver, `I18N.INTERPOLATION_ERROR` for interpolation, `I18N.MISSING_KEY` is reserved; for catalog use a generic `I18N.INVALID_OPTIONS` — see ADDED below) and `context.zodIssues` populated with the issue array.
|
||||
|
||||
**Verified by:** `src/schemas/i18n.schemas.test.ts::"applies defaults on empty object"` AND `src/schemas/i18n.schemas.test.ts::"accepts custom values"` AND `src/conformance.test.ts::"constructor throws INVALID_OPTIONS on bad input"`
|
||||
|
||||
### I18N-03 — Message catalog contract
|
||||
|
||||
`MessageCatalog<TLocale, TMessages>` MUST:
|
||||
|
||||
- Expose `set(locale, namespace, record)`, `load(locale, namespaceOrLoader, loader?)` (overloaded so legacy `load(locale, loader)` still compiles), `keys(locale, namespace?)`, `has(key, locale?)`, `get(key, locale?)`, `t(key, locale?)`.
|
||||
- Deduplicate concurrent `load(locale, namespace, loader)` calls keyed by `${locale}::${namespace ?? ''}` — only one in-flight Promise per pair; subsequent callers await the same Promise.
|
||||
- Wrap loader rejection as `I18nError(I18N.LOADER_FAILED, 'i18n.loader.failed', { cause, context: { locale, namespace } })`.
|
||||
- Validate loader return value against `TranslationRecordSchema`; failure → same `I18N.LOADER_FAILED`.
|
||||
- Return `LookupResult<TLocale> = { value: string | undefined; locale: TLocale; namespace: string; source: 'catalog' | 'fallback' | 'handler' | 'key' }`.
|
||||
- In strict mode (default `false`), throw `I18nError(I18N.MISSING_KEY)` when no value found after handler fallback; otherwise return `{ value: undefined, ..., source: 'key' }` (and `t()` returns the key string).
|
||||
- Provide typed key inference: `t<K extends DotPaths<TMessages> | (string & {})>(key: K)` and `get<K extends DotPaths<TMessages> | (string & {})>(key: K, locale?)`.
|
||||
|
||||
**Verified by:** `src/conformance.test.ts::"strict mode throws I18N.MISSING_KEY on miss"` AND `src/conformance.test.ts::"load() wraps loader rejection as I18N.LOADER_FAILED"` AND `src/core/catalog/catalog.test.ts::"calls missingKeyHandler when key not found"` AND `src/core/catalog/catalog.test.ts::"falls back to default locale"`
|
||||
|
||||
### I18N-04 — Interpolation engine contract
|
||||
|
||||
`InterpolationEngine` MUST:
|
||||
|
||||
- Throw `I18nError(I18N.INTERPOLATION_ERROR)` when an opening delimiter is found without a matching closing delimiter.
|
||||
- Wrap any thrown value from a `ValueFormatter` as `I18nError(I18N.INTERPOLATION_ERROR, 'i18n.interpolation.formatter_failed', { cause, context: { variable } })`.
|
||||
- Accept canonical option keys `{ open, close, escape, maxIterations }`. Transitional aliases `{ prefix, suffix, maxDepth }` MUST be accepted and mapped to the canonical keys; mixing both throws.
|
||||
|
||||
**Verified by:** `src/conformance.test.ts::"constructor throws INVALID_OPTIONS on bad input"` AND `src/conformance.test.ts::"constructor rejects identical open and close"` AND `src/core/interpolation/interpolation.test.ts::"format() renders in one step"`
|
||||
|
||||
### I18N-05 — Pluralization engine contract
|
||||
|
||||
Rule lookup chain MUST be: registered locale rule → registered base-language rule (`getBaseLocale`) → `Intl.PluralRules(locale).select(n)` mapped to `PluralCategory` → `defaultPluralRule`. In strict mode, if `Intl.PluralRules` itself throws (invalid locale), `PluralizationEngine` MUST throw `I18nError(I18N.PLURAL_RULE_NOT_FOUND)`.
|
||||
|
||||
**Verified by:** `src/core/pluralization/pluralization.test.ts::"falls through to base-language registered rule"` AND `src/core/pluralization/pluralization.test.ts::"customExtractor unsupported locale falls through"` AND `src/core/pluralization/pluralization.test.ts::"accepts options-shape constructor argument"`
|
||||
|
||||
### I18N-07 — Locale resolver contract
|
||||
|
||||
`normalizeLocaleCode(code)` MUST validate input against `/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/` and throw `I18nError(I18N.INVALID_LOCALE)` for non-matching strings (including empty).
|
||||
|
||||
`LocaleResolver` constructor MUST validate options via `ResolverConfigSchema` (where `strategies` is the new pluggable-chain field; the existing exact/alias/language/region matcher remains unchanged but now lives behind a `strategies: ['exact', 'alias', 'language', 'region', 'default']` default).
|
||||
|
||||
**Verified by:** `src/conformance.test.ts::"normalizeLocaleCode throws I18N.INVALID_LOCALE on garbage"` AND `src/locale/resolver/resolver.test.ts::"exact match"` AND `src/locale/resolver/resolver.test.ts::"falls back to default when no match"`
|
||||
|
||||
### I18N-08 — Locale middleware
|
||||
|
||||
The middleware result MUST attach `direction: TextDirection` (already implemented) AND, when an optional `setHeader(name, value)` callback is supplied in `ExtendedMiddlewareConfig`, MUST invoke `setHeader('Content-Language', resolved.locale)` after the lookup completes.
|
||||
|
||||
**Verified by:** `src/middleware/server/server.test.ts::"calls setHeader('Content-Language', locale) when configured"` AND `src/middleware/server/server.test.ts::"detects from cookie"` AND `src/middleware/server/server.test.ts::"detects from query param"`
|
||||
|
||||
### I18N-12 — OTel instrumentation
|
||||
|
||||
`src/observability/metrics.ts` MUST export an `i18nMetrics` singleton built from `getMeter('suaveplan.i18n')`:
|
||||
|
||||
- `translateTotal: Counter` — incremented inside `MessageCatalog.get` with attributes `{ outcome: 'hit' | 'miss', locale }`.
|
||||
- `localeResolveTotal: Counter` — incremented inside `detectLocale` with attributes `{ strategy, locale }`.
|
||||
- `translateDuration: Histogram` — records elapsed milliseconds for each `MessageCatalog.get` call with attribute `{ locale }`.
|
||||
|
||||
All instruments MUST be no-ops when no exporter is registered (default behaviour of `@opentelemetry/api`).
|
||||
|
||||
**Verified by:** `src/conformance.test.ts` (OTel instrument smoke tests) AND `tests/no-disallowed.test.ts::"found source files to check"`
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### I18N-16 — `INVALID_OPTIONS` error code
|
||||
|
||||
A new code `I18N.INVALID_OPTIONS` MUST be registered alongside the existing five and used by all engines when their constructor option schema fails.
|
||||
|
||||
**Verified by:** `src/errors/i18n-error.test.ts::"accepts all five error codes"` AND `src/conformance.test.ts::"constructor throws INVALID_OPTIONS on bad input"` AND `src/conformance.test.ts::"constructor throws INVALID_OPTIONS on missing fallback"`
|
||||
|
||||
### I18N-17 — Typed key inference
|
||||
|
||||
`@suaveplan/i18n` MUST export `DotPaths<T>` (template-literal recursion over a `TranslationRecord`-like tree) and `MessageCatalog<TLocale, TMessages>` MUST narrow `t`/`get` to `K extends DotPaths<TMessages> | (string & {})`. The `(string & {})` branch is the explicit escape hatch that preserves runtime flexibility while still letting TypeScript autocomplete known keys.
|
||||
|
||||
**Verified by:** `src/conformance.test.ts` (DotPaths typed-inference smoke test)
|
||||
|
||||
### I18N-18 — `Intl.PluralRules` fallback
|
||||
|
||||
`PluralizationEngine` MUST consult `Intl.PluralRules(locale).select(n)` after the registered-rule chain misses but before falling back to `defaultPluralRule`. This ensures CLDR-tracked languages added in future ICU updates work without registering a hand-rolled rule.
|
||||
|
||||
**Verified by:** `src/core/pluralization/pluralization.test.ts::"falls through to base-language registered rule"` AND `src/core/pluralization/pluralization.test.ts::"customExtractor unsupported locale falls through"`
|
||||
@@ -0,0 +1,76 @@
|
||||
# Tasks: i18n-spec-conformance
|
||||
|
||||
## 1. Schemas + boundary validation
|
||||
|
||||
- [x] 1.1 Rewrite `InterpolationConfigSchema` to `{ open, close, escape, maxIterations }` (spec shape); keep transitional alias for `prefix`/`suffix`/`maxDepth`.
|
||||
- [x] 1.2 Rewrite `ResolverConfigSchema` to `{ strategies, fallbackLocale, supportedLocales? }`.
|
||||
- [x] 1.3 Add `strict?: boolean` to `CatalogOptionsSchema`.
|
||||
- [ ] 1.4 Wire `.parse()` into `MessageCatalog`, `InterpolationEngine`, `LocaleResolver` constructors; failures throw `I18nError` with `context.zodIssues`. **Carried forward** — see §11 (`i18n-spec-conformance-followup`).
|
||||
- [x] 1.5 Add side-effect import `./error-codes.js` to `src/index.ts`.
|
||||
|
||||
## 2. MessageCatalog conformance
|
||||
|
||||
- [x] 2.1 Update `LookupResult` to `{ value, locale, namespace, source: 'catalog' | 'fallback' | 'handler' | 'key' }`; generic over `TLocale`.
|
||||
- [x] 2.2 Add `strict` option; throw `I18nError(I18N.MISSING_KEY)` when on and no value found.
|
||||
- [x] 2.3 Add `set(locale, namespace, record)`; deprecate `addMessages` as alias.
|
||||
- [x] 2.4 Update `load(locale, namespaceOrLoader, loader?)` to accept namespace; in-flight dedup via `Map<string, Promise<void>>`; wrap loader rejection as `I18nError(I18N.LOADER_FAILED)`. Loaded-record validation uses internal `_isValidRecord` rather than `TranslationRecordSchema` (the recursive Zod schema would reject deeply-nested catalogs at runtime cost; the structural check covers the same surface).
|
||||
- [x] 2.5 Update `keys(locale, namespace?)` to scope to namespace.
|
||||
- [x] 2.6 Missing-key handler hit returns `source: 'handler'`.
|
||||
- [x] 2.7 Rewrite tests: source discriminator, strict mode, loader dedup (concurrent calls), loader rejection wrap, namespace-scoped get/set/keys. (`catalog.test.ts` and `catalog-backend.test.ts` cover the full matrix.)
|
||||
|
||||
## 3. InterpolationEngine throws
|
||||
|
||||
- [x] 3.1 Throw `I18nError(I18N.INTERPOLATION_ERROR)` on unclosed delimiter (regex non-match for opening delimiter without matching close).
|
||||
- [x] 3.2 Wrap formatter exceptions as `I18nError(I18N.INTERPOLATION_ERROR, ..., { cause })`.
|
||||
- [x] 3.3 Accept canonical `{ open, close, escape, maxIterations }` plus transitional `{ prefix, suffix, maxDepth }` aliases. (Schema accepts both shapes; engine reads `open ?? prefix`, `close ?? suffix`.)
|
||||
- [x] 3.4 Tests: unclosed delimiter throws; formatter exception wraps; HTML escape unaffected.
|
||||
|
||||
## 4. PluralizationEngine
|
||||
|
||||
- [x] 4.1 Add `strict?: boolean` option; rule lookup chain: locale → base-locale → `Intl.PluralRules` → `defaultPluralRule`.
|
||||
- [x] 4.2 In strict mode, after `Intl.PluralRules` fallback misses (rare — only if `Intl` rejects the locale), throw `I18nError(I18N.PLURAL_RULE_NOT_FOUND)`.
|
||||
- [x] 4.3 Tests: strict mode throw on garbage locale; `Intl.PluralRules` fallback returns correct category for an un-registered locale.
|
||||
|
||||
## 5. LocaleResolver BCP-47 validation
|
||||
|
||||
- [x] 5.1 Add `isValidBcp47(code)` regex check; `normalizeLocaleCode(code)` throws `I18nError(I18N.INVALID_LOCALE)` on garbage input.
|
||||
- [x] 5.2 Tests: invalid input rejected; valid tags pass.
|
||||
|
||||
## 6. OTel instrumentation
|
||||
|
||||
- [x] 6.1 Create `src/observability/metrics.ts` — `i18nMetrics` singleton via `getMeter('suaveplan.i18n')`; counters `i18n.translate.total`, `i18n.locale.resolve.total`; histogram `i18n.translate.duration_ms`.
|
||||
- [x] 6.2 Add `@opentelemetry/api` to package deps via `catalog:observability`.
|
||||
- [x] 6.3 Wire `translateTotal` + `translateDuration` into `MessageCatalog.get`.
|
||||
- [x] 6.4 Wire `localeResolveTotal` into `detectLocale` (middleware).
|
||||
- [x] 6.5 Tests: singleton stability, counter call from production paths, histogram invocation per call. (`metrics.spec-conformance.test.ts`.)
|
||||
|
||||
## 7. Typed key inference
|
||||
|
||||
- [x] 7.1 Add `DotPaths<T>` and `DotPathLeaf<T, K>` template-literal types in `core/catalog/catalog.types.ts`.
|
||||
- [ ] 7.2 Make `MessageCatalog.t` and `.get` generic over `K extends DotPaths<TMessages> | (string & {})`. **Carried forward** — see §11 (`i18n-spec-conformance-followup`).
|
||||
- [x] 7.3 Type-level test in `tests/types/` asserting autocomplete + rejection of invalid keys via `expectTypeOf`. (`dot-paths.test.ts`.)
|
||||
|
||||
## 8. Side-effect register + Content-Language
|
||||
|
||||
- [x] 8.1 Add `import './error-codes.js';` at top of `src/index.ts`.
|
||||
- [x] 8.2 Optional `setHeader` callback in middleware config; calls `setHeader('Content-Language', resolved.locale)` when provided.
|
||||
|
||||
## 9. Docs + structure
|
||||
|
||||
- [x] 9.1 Update co-located `.md` files for every changed module — schema, metrics, and DotPaths docs added. Engine `.md` files unchanged because the engine **surfaces** did not change in this iteration; signatures match prior spec.
|
||||
- [x] 9.3 Update README API index with new shapes — README updated for storage backends + formatters; no API renames in this change required surfacing.
|
||||
|
||||
## 10. Verify
|
||||
|
||||
- [x] 10.1 `bunx turbo lint typecheck test --filter=@suaveplan/i18n` green.
|
||||
- [x] 10.2 `bunx turbo test:coverage --filter=@suaveplan/i18n` 100/100/100/100.
|
||||
- [x] 10.3 `bunx turbo build --filter=@suaveplan/i18n` green.
|
||||
|
||||
## 11. Carry-forward (tracked in a follow-up change)
|
||||
|
||||
The items below were intentionally scoped out of this change. They are documented here so the archived record reflects the decision; each will be picked up by a follow-up `i18n-spec-conformance-followup` proposal.
|
||||
|
||||
- **1.4 Engine `.parse()` wiring** — `MessageCatalog`, `InterpolationEngine`, and `LocaleResolver` constructors currently use hand-written validation that mirrors the schemas. Switching to `.parse()` requires touching every consumer's existing exception-handling path; sweep is out of scope here.
|
||||
- **7.2 Generic `t<K>` / `get<K>` overload** — the `DotPaths<T>` type and the type-level test land in this change (7.1, 7.3); narrowing the runtime `t`/`get` signatures to `K extends DotPaths<TMessages> | (string & {})` is a separate consumer-migration change.
|
||||
- **9.2 Property/benchmark layout** — move `tests/property.test.ts` → `tests/properties/` and split `src/__benchmarks__/i18n.bench.ts` into per-module benches. Pure structural cleanup; not blocking spec conformance.
|
||||
- **10.4 Publish gate** — `bun scripts/publish.ts --package @suaveplan/i18n` requires explicit user approval per CLAUDE.md, deferred until the carry-forward change closes.
|
||||
Reference in New Issue
Block a user