Files
suaveplan/openspec/archive/2026-05-09-i18n-backend-memory/proposal.md
2026-05-27 23:29:53 +01:00

8.2 KiB

@suaveplan/i18n-backend-memory — in-memory translation backend

Package: @suaveplan/i18n-backend-memory (NEW) Status: PROPOSED Type: feature (new universal-tier package; verbatim port from monolith) Date: 2026-05-05 Wave: 5 Design doc: openspec/cross-cutting-designs/2026-05-05-i18n-modular-split.md Path: packages/i18n/i18n-backend-memory/

Why

The Wave 5 backend split lifts each Backend implementation from the @suaveplan/i18n monolith into its own sibling package so consumers can install only the backends they need (matching the crypto-* / drive-* / network-plugin-* granularity established elsewhere in the monorepo). The in-memory backend is the simplest and most universal of the five — backed by Map<locale, Map<namespace, TranslationRecord>>, it ships the full Backend contract (read / readMulti / save / lastModified), is the canonical seed source for tests, and is the standard cache layer for ChainedBackend. Because it has zero external runtime deps, it is the cleanest candidate to land first in the Wave 5 batch.

The Backend interface itself moves to @suaveplan/types/i18n in Wave 1 (zero-runtime types extension). Every backend in this wave implements Backend from that location — @suaveplan/i18n-backend-memory does NOT depend on @suaveplan/i18n core. The runtime atoms (I18nError, I18N_CODES) come from @suaveplan/i18n-shared (Wave 1b).

What changes

New package: @suaveplan/i18n-backend-memory

  • ADDED: packages/i18n/i18n-backend-memory/ package scaffold (package.json, tsconfig.json, vite.config.ts, README.md).
  • ADDED: src/backend.ts — MemoryBackend class implementing Backend from @suaveplan/types/i18n. Source: packages/i18n/i18n/src/core/backend/memory/memory-backend.ts L36-L133. Backed by Map<locale, Map<namespace, TranslationRecord>> plus a Map<string, number> modification ledger keyed by ${locale} ${namespace}. Capabilities: canRead: true, canReadMulti: true, canSave: true, canTrackModification: true, canWatch: false.
  • ADDED: src/backend.types.ts — MemorySeed = Record<string, Record<string, TranslationRecord>> and MemoryBackendOptions ({ seed?, name?, now? }). Source: monolith L11-L26.
  • ADDED: src/backend.schemas.ts — Zod schema MemoryBackendOptionsSchema validating the constructor options at the public boundary.
  • ADDED: src/factory.ts — memoryBackend(options?: MemoryBackendOptions): MemoryBackend factory function. Source: monolith L136-L138.
  • ADDED: src/observability.ts — emits i18n.backend.memory.read_duration_ms (histogram), i18n.backend.memory.read_total{outcome} (counter), i18n.backend.memory.read_errors_total{code} (counter), i18n.backend.memory.write_duration_ms (histogram), i18n.backend.memory.write_total{outcome} (counter). The histogram pattern follows the existing recordRead / recordWrite helpers in the monolith (packages/i18n/i18n/src/observability/backend-metrics.ts) but is inlined here so the leaf does not depend on the monolith.
  • ADDED: src/backend.test.ts, src/backend.spec-conformance.test.ts, src/factory.test.ts, src/index.test.ts — Vitest suites verbatim-ported from monolith memory-backend.test.ts plus new spec-conformance suites.
  • ADDED: src/backend.md, src/factory.md — co-located docs; root README.md links them.
  • ADDED: src/index.ts — explicit named re-exports (MemoryBackend, memoryBackend, type MemoryBackendOptions, type MemorySeed).

Subpath exports

  • . — root barrel (MemoryBackend class + memoryBackend factory + types)
  • ./backend — MemoryBackend class only
  • ./factory — memoryBackend factory function only

Dependencies

  • @suaveplan/i18n-shared workspace:^ — for I18N_CODES, I18nError
  • @suaveplan/types workspace:^ — for Backend, BackendCapabilities, TranslationRecord from @suaveplan/types/i18n
  • @suaveplan/error workspace:^ — for defineCodes-derived consumer-side helpers (if needed by tests)
  • @opentelemetry/api catalog:observability — for histogram + counter emission
  • tslib catalog:build
  • zod catalog:forms — for MemoryBackendOptionsSchema
  • No dep on @suaveplan/i18n core. No dep on any other backend package.

Tier

Universal. Runs in browser AND server with no platform-specific imports. The Map-backed store and Date.now fallback work identically across runtimes.

Out of scope

  • Removing the in-memory backend from packages/i18n/i18n/src/core/backend/memory/ — happens in Wave 6 (i18n-core-slim). For Wave 5, both copies coexist; the monolith remains green.
  • Persistent variants of the in-memory store — WeakMap / TTL-eviction / LRU bounds are explicit non-goals; consumers who need eviction wire MemoryBackend behind a ChainedBackend cache layer or use StorageBackend against MemoryStorage.
  • Async seed loading — the constructor seed is synchronous; consumers who load seeds asynchronously call save() post-construction.
  • Cross-instance event broadcasting — the in-memory backend is per-instance by design; consumers who need broadcast wire the EventBus from @suaveplan/events themselves.

Source mapping

Requirement Source file Source lines Notes
MemoryBackend class + capabilities matrix packages/i18n/i18n/src/core/backend/memory/memory-backend.ts L36-L60 Verbatim port; rewire imports to @suaveplan/i18n-shared and @suaveplan/types/i18n
MemoryBackend.read (hit + miss paths, BACKEND_LOCALE_NOT_AVAILABLE on miss) same file L62-L84 Verbatim port
MemoryBackend.readMulti same file L86-L100 Verbatim port
MemoryBackend.save same file L102-L109 Verbatim port
MemoryBackend.lastModified same file L111-L116 Verbatim port
MemoryBackend.clear test-only helper same file L119-L122 Verbatim port
MemorySeed, MemoryBackendOptions types same file L11-L26 Verbatim port to backend.types.ts
memoryBackend factory same file L136-L138 Lifted into factory.ts
recordRead / recordWrite instrumentation pattern packages/i18n/i18n/src/observability/backend-metrics.ts full file Reimplemented inline in src/observability.ts; same metric names with i18n.backend.memory. prefix

Requirements with no source line

Requirement Justification
Subpath exports ./backend and ./factory Tree-shaking — consumers who construct via the factory should not pull the full Backend class transitively; consumers who subclass should not pull factory wrappers
MemoryBackendOptionsSchema Zod schema Boundary validation — every backend in Wave 5 validates options at the public boundary, matching the pattern in HttpBackend (already does this)
AbortSignal parameter on read / readMulti / save Cancellation parity across backends — every Wave 5 backend accepts an optional AbortSignal so chained / multi-backend orchestration can propagate cancellation

Verification approach

Each requirement carries a **Verified by:** line in the spec delta. Concrete test files:

  • packages/i18n/i18n-backend-memory/src/backend.test.ts — full Backend contract (read / readMulti / save / lastModified) round-trip, hit + miss paths, capabilities matrix, custom name / now injection
  • packages/i18n/i18n-backend-memory/src/backend.spec-conformance.test.ts — every spec requirement mapped 1:1 to an it(...) title for traceability
  • packages/i18n/i18n-backend-memory/src/factory.test.ts — memoryBackend() returns MemoryBackend instance; passes seed through; defaults name to "memory"
  • packages/i18n/i18n-backend-memory/src/index.test.ts — every documented public export resolves through every documented subpath

Impact

  • New universal-tier package — joins the i18n family under packages/i18n/.
  • No downstream consumer changes needed in Wave 5 — the slim core (Wave 6) re-exports MemoryBackend and memoryBackend from the new package, preserving every existing import { MemoryBackend } from "@suaveplan/i18n" call site.
  • MANIFEST.md regenerated via bun scripts/folder-reorg/generate-manifest.ts after the package lands.
  • CLAUDE.md package listing updated with a one-line entry under the i18n cluster.