# `@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 `` 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.