first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
# @suaveplan/i18n-backend-http - Universal translation backend backed by injectable fetch. Read-only; maps HTTP 404 to BACKEND_LOCALE_NOT_AVAILABLE (chain fallthrough) and HTTP 5xx / network / parse failures to BACKEND_READ_FAILED (chain stop). Pluggable parsePayload + customHeaders + retry policy + AbortSignal cancellation, OpenTelemetry instrumentation under i18n.backend.http.*. Zero runtime dependencies on @suaveplan/i18n core.
## Summary
Universal translation backend for the `@suaveplan/i18n` family,
backed by an injectable `fetch`. Read-only by design: maps HTTP 404 to
`BACKEND_LOCALE_NOT_AVAILABLE` (the chained-backend fallthrough signal)
and HTTP 5xx, network failures, and parse failures to
`BACKEND_READ_FAILED` (a chain-stopping signal that prevents silent
degradation when the transport is broken). Pluggable `parsePayload`,
`customHeaders`, `requestOptions`, `crossDomain` shorthand, retry-policy
injection (structurally compatible with `@suaveplan/retry`'s
`RetryPolicy`), `AbortSignal` cancellation, and OpenTelemetry
instrumentation under `i18n.backend.http.*`. Zero runtime dependency on
`@suaveplan/i18n` core.
<!-- begin:auto-docs -->
## Documentation tree
- [backend](./src/backend/backend.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [backend](./src/backend/backend.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [backend](./src/backend/backend.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [backend](./src/backend/backend.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [backend](./src/backend/backend.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [backend](./src/backend/backend.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_ - [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_ - [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_
## Why this exists
The Wave 5 modular split lifts each backend out of the
`@suaveplan/i18n` monolith into its own sibling so consumers can install
only the backends they need. The HTTP backend is the canonical
production source for browser-tier and edge consumers fetching
translations from a CDN, an internal i18n service, or a pre-signed
storage URL. Keeping it in its own package avoids dragging
`@suaveplan/storage` (storage backend) or `node:fs` (multifile-fs
backend) into browser bundles.
## Install
```bash
bun add @suaveplan/i18n-backend-http
```
## Quick Start
```ts
import { httpBackend } from "@suaveplan/i18n-backend-http";
const backend = httpBackend({
loadPath: "/i18n/{locale}/{namespace}.json",
});
const record = await backend.read("en", "auth");
```
## Modules
| Subpath | Module |
| --- | --- |
| `@suaveplan/i18n-backend-http/backend` | [`HttpBackend`](./src/backend/backend.md) |
| `@suaveplan/i18n-backend-http/factory` | [`httpBackend`](./src/factory/factory.md) |
| `@suaveplan/i18n-backend-http/observability` | [OTel instruments](./src/observability/observability.md) |
## Capabilities
```ts
{
canRead: true,
canReadMulti: false,
canSave: false,
canTrackModification: false,
canWatch: false,
}
```
## Error semantics
- HTTP 404 → `I18nError(I18N.BACKEND_LOCALE_NOT_AVAILABLE)` — chained
backend SHOULD fall through.
- HTTP 5xx / non-2xx non-404 → `I18nError(I18N.BACKEND_READ_FAILED)` —
chained backend MUST propagate.
- Network failure (`TypeError` from `fetch`) →
`I18nError(I18N.BACKEND_READ_FAILED)`.
- AbortError mid-flight → `I18nError(I18N.BACKEND_TIMEOUT)`.
- Pre-aborted `signal` → `I18nError(I18N.BACKEND_TIMEOUT)` (no
network request issued).
- Parse failure → `I18nError(I18N.BACKEND_READ_FAILED)`.
- Invalid options → `I18nError(I18N.INVALID_OPTIONS)` at construction.
## Tier
Universal — runs in browsers, Bun, and Node.js >= 18. On older Node
runtimes lacking `globalThis.fetch`, `defaultFetch()` returns a stub
that throws `BACKEND_READ_FAILED` with an explicit message instructing
consumers to inject `fetchFn`.

View File

@@ -0,0 +1,119 @@
# `HttpBackend`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`HttpBackend` is the canonical browser-tier translation source for the
`@suaveplan/i18n` family. It fetches `<loadPath>` per `(locale,
namespace)` via an injectable `fetchFn` (defaulting to
`globalThis.fetch`) and parses the response body via an injectable
`parsePayload` (defaulting to a JSON-object parser that rejects arrays,
null, and primitive payloads). HTTP 404 maps to
`BACKEND_LOCALE_NOT_AVAILABLE` — the chained-backend fallthrough signal
— while HTTP 5xx, network failures, and parse failures map to
`BACKEND_READ_FAILED`, the chain-stopping signal that prevents silent
degradation when the transport itself is broken.
The backend is read-only by design (`canSave: false`,
`canTrackModification: false`, `canWatch: false`). Consumers needing
write-back wire `MultiFileFsBackend` or `StorageBackend` behind the
catalog instead. Universal — runs identically in browsers, Bun,
and Node.js >= 18; older Node runtimes get a clear
`BACKEND_READ_FAILED` from `defaultFetch()` instructing them to inject
`fetchFn`.
## Features
- 5-flag capability matrix `{ canRead: true, canReadMulti: false,
canSave: false, canTrackModification: false, canWatch: false }`.
- URL templating via `{locale}` / `{namespace}` placeholders with
`encodeURIComponent` per segment.
- Pluggable `parsePayload` accepts non-JSON bodies (YAML, MessagePack,
schema-validated JSON).
- Pluggable `customHeaders` builder per request.
- `crossDomain: true` shorthand for `credentials: "include"` +
`mode: "cors"` (preserves explicit overrides in `requestOptions`).
- Optional `retry?: HttpBackendRetry` (structurally compatible with
`@suaveplan/retry`'s `RetryPolicy`) wraps every fetch.
- Optional `signal?: AbortSignal` — pre-aborted signals throw
`BACKEND_TIMEOUT` without issuing a network request; mid-flight
AbortError surfaces as `BACKEND_TIMEOUT`; the signal is threaded into
the underlying `RequestInit` so `fetch` can cancel mid-flight.
- Zod boundary validation throws `INVALID_OPTIONS` at construction.
- Per-call OTel instrumentation under `i18n.backend.http.*`.
## Usage: Basic Example
```ts
import { HttpBackend } from "@suaveplan/i18n-backend-http";
const backend = new HttpBackend({
loadPath: "/i18n/{locale}/{namespace}.json",
});
const record = await backend.read("en", "auth");
```
## Usage: Advanced Example
```ts
import { HttpBackend } from "@suaveplan/i18n-backend-http";
const backend = new HttpBackend({
loadPath: "/i18n/{locale}/{namespace}.yaml",
crossDomain: true,
customHeaders: (locale) => ({ "x-locale": locale }),
parsePayload: async (res) => parseYaml(await res.text()),
retry: {
run: async (op) => {
for (let i = 0; i < 3; i++) {
try {
return await op();
} catch (err) {
if (i === 2) throw err;
}
}
throw new Error("unreachable");
},
},
});
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 2_000);
const record = await backend.read("en", "auth", ctrl.signal);
```
## API Reference
### `class HttpBackend implements Backend`
- `new HttpBackend(options: HttpBackendOptions)` — Zod-validated;
throws `I18nError(I18N.INVALID_OPTIONS)` on malformed input.
- `read(locale, namespace, signal?): Promise<TranslationRecord>` —
fetches and parses the URL. Throws `BACKEND_LOCALE_NOT_AVAILABLE` on
404; `BACKEND_READ_FAILED` on 5xx / network / parse failures;
`BACKEND_TIMEOUT` on signal abort.
### Helpers
- `buildUrl(loadPath, locale, namespace): string` — substitutes
`{locale}` and `{namespace}` with `encodeURIComponent`-applied
values.
- `defaultParsePayload: ParsePayloadFn` — JSON object parser. Rejects
arrays, null, and primitives as `BACKEND_READ_FAILED`.
- `HttpBackendOptionsSchema: z.ZodObject` — Zod boundary schema.
## Implementation Notes
The class delegates to a private `#fetchOnce` helper that classifies
network failures: `AbortError` / `ABORT_ERR` codes surface as
`BACKEND_TIMEOUT` (so consumers can distinguish cancellation from
transport faults), and `I18nError(I18N.BACKEND_TIMEOUT)` thrown from
inside `fetchFn` (e.g. an injected fake) propagates unchanged.
Everything else from the network surface — `TypeError`, refused
connections, DNS failures, CORS preflights — maps to
`BACKEND_READ_FAILED`. Capability flags stay `false` for write paths
because the HTTP transport has no canonical `PUT` semantics across
CDNs; consumers needing write-back wire a different backend.

View File

@@ -0,0 +1,76 @@
# `httpBackend(options)`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`httpBackend(options)` is the builder-style factory for
[`HttpBackend`](../backend/backend.md). It constructs a fresh
`HttpBackend` instance per call — no singletons, no shared state, no
caching of options or fetch bindings — and surfaces the same Zod
boundary validation and capability matrix as the constructor.
Consumers who prefer composition over `new` reach for this factory; the
two forms are interchangeable. Both exist because i18n-family packages
historically preferred named factories for ergonomic test fixtures and
dependency-injection setups.
## Features
- Zero-argument-overhead alias for `new HttpBackend(options)`.
- Returns a fresh `HttpBackend` per call (no implicit caching).
- Throws `I18nError(I18N.INVALID_OPTIONS)` on malformed input (Zod
boundary). Throws nothing else at construction time.
- Threads every option (`loadPath`, `fetchFn`, `parsePayload`,
`customHeaders`, `requestOptions`, `crossDomain`, `retry`, `name`)
unchanged into the constructor.
## Usage: Basic Example
```ts
import { httpBackend } from "@suaveplan/i18n-backend-http";
const backend = httpBackend({
loadPath: "/i18n/{locale}/{namespace}.json",
});
```
## Usage: Advanced Example
```ts
import { httpBackend } from "@suaveplan/i18n-backend-http";
const backend = httpBackend({
name: "cdn",
loadPath: "https://cdn.example/i18n/{locale}/{namespace}.json",
crossDomain: true,
customHeaders: () => ({ "x-tenant": "acme" }),
retry: {
run: async (op, _opts) => op(),
},
});
const ctrl = new AbortController();
queueMicrotask(() => ctrl.abort());
try {
await backend.read("en", "auth", ctrl.signal);
} catch (err) {
// I18nError(I18N.BACKEND_TIMEOUT)
}
```
## API Reference
### `httpBackend(options: HttpBackendOptions): HttpBackend`
Returns a new `HttpBackend`. Every option is passed verbatim. Throws
`I18nError(I18N.INVALID_OPTIONS)` when `options` fails Zod validation.
## Implementation Notes
The factory holds no module-level state. Two calls produce two
distinct `HttpBackend` instances even when the option objects are
referentially identical — this is deliberate so test fixtures can
build many cheaply-isolated backends without aliasing issues. The
factory is defined in a separate file so it can be tree-shaken away by
consumers that import the class directly via the `./backend` subpath.

View File

@@ -0,0 +1,75 @@
# `@suaveplan/i18n-backend-http/observability`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Lazy OpenTelemetry instrument factory and matched in-memory state ring
for `@suaveplan/i18n-backend-http`. Production paths in
[`backend.ts`](../backend/backend.md) call `recordRead(...)` once per
`HttpBackend.read` call; the instruments are no-ops until a
`MeterProvider` is registered against `@opentelemetry/api`, so consumers
pay zero overhead unless they wire up the SDK. The matched state ring
exists so spec-conformance tests can assert exact attribute payloads
without standing up a full SDK + `InMemoryMetricExporter` pipeline.
## Features
- `i18n.backend.http.read_duration_ms` — Histogram, milliseconds.
Attributes: `locale`, `namespace`, `outcome ∈ hit | miss | error`.
- `i18n.backend.http.read_total` — Counter. Attribute: `outcome`.
- `i18n.backend.http.read_errors_total` — Counter. Attributes: `code`
(the `I18N.*` error code), optional `status` (HTTP status code as
string when the error originated from a known HTTP response).
- Zero-cost when no MeterProvider is wired — every instrument is a
cached singleton.
- Test-only state ring + reset helpers
(`getHttpBackendMetricsState`, `resetHttpBackendMetricsState`,
`resetHttpBackendMetricsCache`).
## Usage: Basic Example
```ts
import { recordRead } from "@suaveplan/i18n-backend-http/observability";
recordRead("en", "auth", "hit", 0.42);
```
## Usage: Advanced Example
```ts
import {
getHttpBackendMetricsState,
recordRead,
resetHttpBackendMetricsState,
} from "@suaveplan/i18n-backend-http/observability";
resetHttpBackendMetricsState();
recordRead("xx", "auth", "miss", 1.2, "I18N.BACKEND_LOCALE_NOT_AVAILABLE", 404);
const state = getHttpBackendMetricsState();
console.log(state.readErrorsTotal);
// → [{ attributes: { code: "I18N.BACKEND_LOCALE_NOT_AVAILABLE", status: "404" } }]
```
## API Reference
- `getHttpBackendMetrics(): { readDurationMs, readTotal, readErrorsTotal }`
— idempotent factory.
- `recordRead(locale, namespace, outcome, durationMs, errorCode?, status?)`
— emit + ring-record.
- `getHttpBackendMetricsState(): HttpBackendMetricsState` — frozen
snapshot.
- `resetHttpBackendMetricsState()` / `resetHttpBackendMetricsCache()` —
test helpers.
## Implementation Notes
`status` is encoded as a string attribute so OTel SDK aggregators can
union it with the `code` label without numeric coercion ambiguity. The
counter cardinality is bounded by the small `I18N.*` error-code set
times a typical handful of HTTP statuses (404 / 500 / 502 / 503), which
stays well below downstream backend high-cardinality limits. The cached
instruments survive across `recordRead` calls inside one process — the
`resetHttpBackendMetricsCache` helper exists purely so tests can verify
the cache-hit path is exercised.