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

101 lines
8.5 KiB
Markdown

# `@suaveplan/i18n-backend-http` — fetch-based translation backend
**Package**: `@suaveplan/i18n-backend-http` (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-http/`
## Why
The Wave 5 backend split lifts each `Backend` implementation from the `@suaveplan/i18n` monolith into its own sibling package. The HTTP backend is the canonical production source for browser-tier consumers — it fetches `<loadPath>` per `(locale, namespace)`, maps HTTP 404 to `BACKEND_LOCALE_NOT_AVAILABLE` (chained-backend fallthrough signal) and HTTP 5xx / network errors to `BACKEND_READ_FAILED` (chain-stopping signal), supports retry-policy injection (structurally compatible with `@suaveplan/retry`'s `RetryPolicy`), and accepts custom payload parsers (JSON default; YAML / MessagePack / schema-validating parsers are caller-pluggable).
The `Backend` interface itself moves to `@suaveplan/types/i18n` in Wave 1 (zero-runtime types extension). `@suaveplan/i18n-backend-http` implements `Backend` from that location and does NOT depend on `@suaveplan/i18n` core. Runtime atoms (`I18nError`, `I18N_CODES`) come from `@suaveplan/i18n-shared`.
## What changes
### New package: `@suaveplan/i18n-backend-http`
- ADDED: `packages/i18n/i18n-backend-http/` package scaffold (package.json, tsconfig.json, vite.config.ts, README.md).
- ADDED: `src/backend.ts` — `HttpBackend` class implementing `Backend` from `@suaveplan/types/i18n`. Source: `packages/i18n/i18n/src/core/backend/http/http-backend.ts` L80-L240. Capabilities: `canRead: true`, `canReadMulti: false`, `canSave: false`, `canTrackModification: false`, `canWatch: false`. Read-only by design — write operations (`saveMissing` on the catalog side) are not surfaced.
- ADDED: `src/backend.types.ts` — `HttpBackendOptions`, `ParsePayloadFn`, `HttpHeadersInit`, `CustomHeadersFn`, `HttpBackendRetry`. Source: monolith L8-L70.
- ADDED: `src/backend.schemas.ts` — `HttpBackendOptionsSchema` Zod schema. Source: monolith `http-backend.schemas.ts` (40 lines).
- ADDED: `src/factory.ts` — `httpBackend(options: HttpBackendOptions): HttpBackend` factory. Source: monolith L266-L268.
- ADDED: `src/observability.ts` — emits `i18n.backend.http.read_duration_ms` (histogram), `i18n.backend.http.read_total{outcome}` (counter), `i18n.backend.http.read_errors_total{code,status?}` (counter). Histogram + counter pattern follows existing `recordRead` helper in monolith.
- ADDED: `src/url-builder.ts` — `buildUrl(loadPath, locale, namespace)` extracted as a standalone helper for tree-shaking + isolated testing. Source: monolith L220-L224 (`#buildUrl`).
- ADDED: `src/payload-parser.ts` — `defaultParsePayload` standalone helper (JSON parser with non-object rejection). Source: monolith L254-L263.
- ADDED: `src/backend.test.ts`, `src/backend.spec-conformance.test.ts`, `src/factory.test.ts`, `src/url-builder.test.ts`, `src/payload-parser.test.ts`, `src/index.test.ts` — Vitest suites verbatim-ported from monolith plus new spec-conformance suites.
- ADDED: `src/backend.md`, `src/factory.md`, `src/url-builder.md`, `src/payload-parser.md` — co-located docs.
- ADDED: `src/index.ts` — explicit named re-exports.
### Subpath exports
- `.` — root barrel
- `./backend` — `HttpBackend` class only
- `./factory` — `httpBackend` factory function only
- `./parser` — `defaultParsePayload` helper (consumers building custom parsers)
- `./url-builder` — `buildUrl` helper (consumers customising URL templating)
### Dependencies
- `@suaveplan/i18n-shared workspace:^` — for `I18N_CODES`, `I18nError`
- `@suaveplan/types workspace:^` — for `Backend`, `BackendCapabilities`, `TranslationRecord` from `@suaveplan/types/i18n`
- `@suaveplan/error workspace:^`
- `@opentelemetry/api catalog:observability`
- `tslib catalog:build`
- `zod catalog:forms` — for `HttpBackendOptionsSchema`
- **No** dep on `@suaveplan/i18n` core. **No** dep on any other backend package. **No** dep on `@suaveplan/retry` (the `HttpBackendRetry` interface is structural so consumers can pass `@suaveplan/retry`'s `RetryPolicy` directly without us taking a hard dep).
### Tier
Universal. Uses `globalThis.fetch` which is available in browsers, Bun, and Node.js >= 18. The `defaultFetch()` helper throws `BACKEND_READ_FAILED` with a clear message when `globalThis.fetch` is unavailable, prompting consumers on older runtimes to inject `fetchFn` explicitly.
## Out of scope
- **Removing the HTTP backend from `packages/i18n/i18n/src/core/backend/http/`** — happens in Wave 6.
- **Server-Sent Events / WebSocket-driven HTTP backend** — out of scope; consumers wire `EventBus` from `@suaveplan/events` against a separate transport.
- **HTTP-level caching (`Cache-Control` / `ETag` / `Last-Modified`)** — out of scope; consumers wire `ChainedBackend` with a `MemoryBackend` cache layer.
- **`save` / `lastModified` implementations** — explicit non-goals; the HTTP backend is read-only by design. Consumers who need write-back wire `MultiFileFsBackend` or `StorageBackend` behind the catalog.
## Source mapping
| Requirement | Source file | Source lines | Notes |
|---|---|---|---|
| `HttpBackend` class + capabilities matrix | `packages/i18n/i18n/src/core/backend/http/http-backend.ts` | L80-L116 | Verbatim port; rewire imports to `@suaveplan/i18n-shared` and `@suaveplan/types/i18n` |
| `HttpBackend.read` (404 → `BACKEND_LOCALE_NOT_AVAILABLE`, 5xx / network → `BACKEND_READ_FAILED`) | same file | L118-L205 | Verbatim port |
| `#fetchOnce` network-error classifier | same file | L207-L218 | Verbatim port |
| `#buildUrl` (lifted to `src/url-builder.ts`) | same file | L220-L224 | Extracted to standalone helper |
| `#buildInit` (request-init builder) | same file | L226-L239 | Inline private method on class |
| `defaultFetch()` — globalThis.fetch fallback | same file | L242-L252 | Verbatim port |
| `defaultParsePayload` (lifted to `src/payload-parser.ts`) | same file | L254-L263 | Extracted to standalone helper |
| `HttpBackendOptions`, `ParsePayloadFn`, `HttpHeadersInit`, `CustomHeadersFn`, `HttpBackendRetry` types | same file | L8-L70 | Verbatim port to `backend.types.ts` |
| `HttpBackendOptionsSchema` Zod schema | `packages/i18n/i18n/src/core/backend/http/http-backend.schemas.ts` | full file (40 lines) | Verbatim port |
| `httpBackend` factory | `packages/i18n/i18n/src/core/backend/http/http-backend.ts` | L266-L268 | Lifted to `factory.ts` |
| `recordRead` instrumentation pattern | `packages/i18n/i18n/src/observability/backend-metrics.ts` | full file | Reimplemented inline; metric names re-prefixed `i18n.backend.http.` |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| Subpath exports `./backend`, `./factory`, `./parser`, `./url-builder` | Tree-shaking — consumers customising URL or payload parsing should pull only the helper they extend |
| `AbortSignal` parameter on `read` | Cancellation parity across backends; existing monolith does NOT thread `signal` into `fetch`, so this is a small surface improvement landing in the split |
## Verification approach
Each requirement carries a `**Verified by:**` line in the spec delta. Concrete test files:
- `packages/i18n/i18n-backend-http/src/backend.test.ts` — full `Backend` contract; 200 OK round-trip with stubbed `fetchFn`; 404 → `BACKEND_LOCALE_NOT_AVAILABLE`; 500 → `BACKEND_READ_FAILED`; network failure (TypeError from `fetchFn`) → `BACKEND_READ_FAILED`; custom `parsePayload` invoked; `customHeaders` callback invoked; `crossDomain: true` sets credentials + mode; injected `retry.run` wraps every fetch
- `packages/i18n/i18n-backend-http/src/backend.spec-conformance.test.ts` — every spec requirement mapped 1:1 to an `it(...)` title
- `packages/i18n/i18n-backend-http/src/factory.test.ts` — `httpBackend()` returns `HttpBackend` instance; defaults `name` to `"http"`
- `packages/i18n/i18n-backend-http/src/url-builder.test.ts` — `buildUrl` substitutes `{locale}` and `{namespace}`; URL-encodes both
- `packages/i18n/i18n-backend-http/src/payload-parser.test.ts` — `defaultParsePayload` accepts JSON object; rejects array, null, primitive
- `packages/i18n/i18n-backend-http/src/index.test.ts` — every documented public export resolves
## 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 `HttpBackend` and `httpBackend`.
- `MANIFEST.md` regenerated; `CLAUDE.md` package listing updated with a one-line entry.