95 lines
8.2 KiB
Markdown
95 lines
8.2 KiB
Markdown
# `@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.
|