8.5 KiB
@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—HttpBackendclass implementingBackendfrom@suaveplan/types/i18n. Source:packages/i18n/i18n/src/core/backend/http/http-backend.tsL80-L240. Capabilities:canRead: true,canReadMulti: false,canSave: false,canTrackModification: false,canWatch: false. Read-only by design — write operations (saveMissingon 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—HttpBackendOptionsSchemaZod schema. Source: monolithhttp-backend.schemas.ts(40 lines). - ADDED:
src/factory.ts—httpBackend(options: HttpBackendOptions): HttpBackendfactory. Source: monolith L266-L268. - ADDED:
src/observability.ts— emitsi18n.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 existingrecordReadhelper 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—defaultParsePayloadstandalone 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—HttpBackendclass only./factory—httpBackendfactory function only./parser—defaultParsePayloadhelper (consumers building custom parsers)./url-builder—buildUrlhelper (consumers customising URL templating)
Dependencies
@suaveplan/i18n-shared workspace:^— forI18N_CODES,I18nError@suaveplan/types workspace:^— forBackend,BackendCapabilities,TranslationRecordfrom@suaveplan/types/i18n@suaveplan/error workspace:^@opentelemetry/api catalog:observabilitytslib catalog:buildzod catalog:forms— forHttpBackendOptionsSchema- No dep on
@suaveplan/i18ncore. No dep on any other backend package. No dep on@suaveplan/retry(theHttpBackendRetryinterface is structural so consumers can pass@suaveplan/retry'sRetryPolicydirectly 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
EventBusfrom@suaveplan/eventsagainst a separate transport. - HTTP-level caching (
Cache-Control/ETag/Last-Modified) — out of scope; consumers wireChainedBackendwith aMemoryBackendcache layer. save/lastModifiedimplementations — explicit non-goals; the HTTP backend is read-only by design. Consumers who need write-back wireMultiFileFsBackendorStorageBackendbehind 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— fullBackendcontract; 200 OK round-trip with stubbedfetchFn; 404 →BACKEND_LOCALE_NOT_AVAILABLE; 500 →BACKEND_READ_FAILED; network failure (TypeError fromfetchFn) →BACKEND_READ_FAILED; customparsePayloadinvoked;customHeaderscallback invoked;crossDomain: truesets credentials + mode; injectedretry.runwraps every fetchpackages/i18n/i18n-backend-http/src/backend.spec-conformance.test.ts— every spec requirement mapped 1:1 to anit(...)titlepackages/i18n/i18n-backend-http/src/factory.test.ts—httpBackend()returnsHttpBackendinstance; defaultsnameto"http"packages/i18n/i18n-backend-http/src/url-builder.test.ts—buildUrlsubstitutes{locale}and{namespace}; URL-encodes bothpackages/i18n/i18n-backend-http/src/payload-parser.test.ts—defaultParsePayloadaccepts JSON object; rejects array, null, primitivepackages/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
HttpBackendandhttpBackend. MANIFEST.mdregenerated;CLAUDE.mdpackage listing updated with a one-line entry.