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,158 @@
# `createReactI18nRegistry`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
- [built-in](./formatters/built-in/built-in.md) — <!-- begin:auto-docs --> _(no submodules)_- [built-in](./locale-adapters/built-in/built-in.md) — <!-- begin:auto-docs --> _(no submodules)_ - [built-in](./locale-adapters/built-in/built-in.md) — <!-- begin:auto-docs --> _(no submodules)_- [post-processors](./post-processors/post-processors.md) — <!-- begin:auto-docs --> _(no submodules)_ - [post-processors](./post-processors/post-processors.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry-context](./registry-context.md) — Module-scoped React context that carries the active `ReactI18nRegistry` to descendants of `<RegistryProvider>`. Direct…
- [registry-provider](./registry-provider.md) — `<RegistryProvider>` is the React-context surface for `ReactI18nRegistry`. It mounts a fresh registry per tree, registe…
- [registry](./registry.md) — <!-- begin:auto-docs --> _(no submodules)_- built-in — <!-- begin:auto-docs --> _(no submodules)…
- [registry.schemas](./registry.schemas.md) — Strict-object Zod schemas for every plugin kind accepted by `ReactI18nRegistry`. The schemas are the boundary at which…
- [use-registry](./use-registry.md) — These hooks read the active `ReactI18nRegistry` from React context. The strict variant `useReactI18nRegistry()` throws…
- [built-in](./formatters/built-in/built-in.md) — <!-- begin:auto-docs --> _(no submodules)_- [built-in](./locale-adapters/built-in/built-in.md) — <!-- begin:auto-docs --> _(no submodules)_- [built-in](./locale-adapters/built-in/built-in.md) — <!-- begin:auto-docs --> _(no submodules)_- [post-processors](./post-processors/post-processors.md) — <!-- begin:auto-docs --> _(no submodules)_- [post-processors](./post-processors/post-processors.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry-context](./registry-context.md) — Module-scoped React context that carries the active `ReactI18nRegistry` to descendants of `<RegistryProvider>`. Direct…
- [registry-provider](./registry-provider.md) — `<RegistryProvider>` is the React-context surface for `ReactI18nRegistry`. It mounts a fresh registry per tree, registe…
- [registry](./registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry.schemas](./registry.schemas.md) — Strict-object Zod schemas for every plugin kind accepted by `ReactI18nRegistry`. The schemas are the boundary at which…
- [use-registry](./use-registry.md) — These hooks read the active `ReactI18nRegistry` from React context. The strict variant `useReactI18nRegistry()` throws…
- [built-in](./formatters/built-in/built-in.md) — <!-- begin:auto-docs --> _(no submodules)_- [built-in](./locale-adapters/built-in/built-in.md) — <!-- begin:auto-docs --> _(no submodules)_- [built-in](./locale-adapters/built-in/built-in.md) — <!-- begin:auto-docs --> _(no submodules)_- [post-processors](./post-processors/post-processors.md) — <!-- begin:auto-docs --> _(no submodules)_- [post-processors](./post-processors/post-processors.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry-context](./registry-context.md) — Module-scoped React context that carries the active `ReactI18nRegistry` to descendants of `<RegistryProvider>`. Direct…
- [registry-provider](./registry-provider.md) — `<RegistryProvider>` is the React-context surface for `ReactI18nRegistry`. It mounts a fresh registry per tree, registe…
- [registry](./registry.md) — <!-- begin:auto-docs --> _(no submodules)_- built-in — <!-- begin:auto-d…
- [registry.schemas](./registry.schemas.md) — Strict-object Zod schemas for every plugin kind accepted by `ReactI18nRegistry`. The schemas are the boundary at which…
- [use-registry](./use-registry.md) — These hooks read the active `ReactI18nRegistry` from React context. The strict variant `useReactI18nRegistry()` throws…
- [built-in](./formatters/built-in/built-in.md) — <!-- begin:auto-docs --> _(no submodules)_- [built-in](./locale-adapters/built-in/built-in.md) — <!-- begin:auto-docs --> _(no submodules)_- [built-in](./locale-adapters/built-in/built-in.md) — <!-- begin:auto-docs --> _(no submodules)_- [post-processors](./post-processors/post-processors.md) — <!-- begin:auto-docs --> _(no submodules)_- [post-processors](./post-processors/post-processors.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry-context](./registry-context.md) — Module-scoped React context that carries the active `ReactI18nRegistry` to descendants of `<RegistryProvider>`. Direct…
- [registry-provider](./registry-provider.md) — `<RegistryProvider>` is the React-context surface for `ReactI18nRegistry`. It mounts a fresh registry per tree, registe…
- [registry](./registry.md) — <!-- begin:auto-docs --> _(no submodules)_- built-in — <!-- begin:auto-docs --> _(no submodules…
- [registry.schemas](./registry.schemas.md) — Strict-object Zod schemas for every plugin kind accepted by `ReactI18nRegistry`. The schemas are the boundary at which…
- [use-registry](./use-registry.md) — These hooks read the active `ReactI18nRegistry` from React context. The strict variant `useReactI18nRegistry()` throws…
- [built-in](./formatters/built-in/built-in.md) — <!-- begin:auto-docs --> _(no submodules)_- [built-in](./locale-adapters/built-in/built-in.md) — <!-- begin:auto-docs --> _(no submodules)_ - [built-in](./locale-adapters/built-in/built-in.md) — <!-- begin:auto-docs --> _(no submodules)_- [post-processors](./post-processors/post-processors.md) — <!-- begin:auto-docs --> _(no submodules)_ - [post-processors](./post-processors/post-processors.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry-context](./registry-context.md) — Module-scoped React context that carries the active `ReactI18nRegistry` to descendants of `<RegistryProvider>`. Direct…
- [registry-provider](./registry-provider.md) — `<RegistryProvider>` is the React-context surface for `ReactI18nRegistry`. It mounts a fresh registry per tree, registe…
- [registry](./registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry.schemas](./registry.schemas.md) — Strict-object Zod schemas for every plugin kind accepted by `ReactI18nRegistry`. The schemas are the boundary at which…
- [use-registry](./use-registry.md) — These hooks read the active `ReactI18nRegistry` from React context. The strict variant `useReactI18nRegistry()` throws…
## Purpose
The plugin registry is the single substrate every extension surface in `@suaveplan/react-i18n` shares. It accepts four plugin kinds — detectors, formatters, post-processors, and locale-adapters — through a uniform registration / disposal / lookup API. The registry is the answer to the question "how does a third-party package extend the React i18n surface without forking?": instead of exposing N independent registration points (one per detector, one per formatter, …) the registry funnels every extension through one validated, observable, disposable contract. A `<RegistryProvider>` mounts a fresh registry per React tree, auto-registers built-in formatters and locale adapters, and exposes the registry to descendants through `useReactI18nRegistry()`.
## Features
- Four plugin kinds — `DetectorPlugin`, `FormatterPlugin<TArgs, TResult>`, `PostProcessorPlugin`, `LocaleAdapterPlugin<TLocale>` — partitioned independently so the same `id` may legitimately appear under two kinds.
- Idempotent re-registration of the same plugin instance.
- Duplicate-id detection (different instance under same kind+id) throws a stable `REACT_I18N.PLUGIN_DUPLICATE_ID` (or `DETECTOR_PLUGIN_DUPLICATE_ID` for detector kind).
- Schema-driven shape validation at registration time — malformed plugins throw `REACT_I18N.PLUGIN_INVALID` (or `DETECTOR_PLUGIN_INVALID`) with the Zod issue list attached as `cause`.
- Reversible registration through `Disposable` handles. Disposal is idempotent.
- OTel observability: `react_i18n.registry.plugins_total{kind,outcome}` counter increments on every registered/disposed/duplicate/invalid event.
- Zero per-render cost: lookup is O(1) `Map.get` and the registry holds no React state itself.
## Usage: Basic Example
```ts
import { createReactI18nRegistry } from "@suaveplan/react-i18n/registry";
const registry = createReactI18nRegistry();
const handle = registry.registerFormatter({
id: "duration",
format(locale, args: { ms: number }) {
return new Intl.NumberFormat(locale).format(args.ms / 1000) + "s";
},
});
const plugin = registry.getFormatter<{ ms: number }, string>("duration");
plugin?.format("en-GB", { ms: 1500 }); // "1.5s"
handle.dispose();
```
## Usage: Advanced Example
Detector plugin with priority-driven splice into the default chain, plus a post-processor pipeline:
```ts
import { createReactI18nRegistry } from "@suaveplan/react-i18n/registry";
const registry = createReactI18nRegistry();
registry.registerDetector({
id: "ab-bucket",
priority: 0, // run before every built-in detector
detect: {
name: "ab-bucket",
environment: "browser",
detect: () => sessionStorage.getItem("ab.locale") ?? null,
},
});
registry.registerPostProcessor({
id: "pseudo",
priority: 100,
process: (msg) => `[${msg}]`,
});
registry.registerPostProcessor({
id: "redact-pii",
priority: 50, // runs before pseudo
process: (msg) => msg.replace(/\d{16}/g, "****-****-****-****"),
});
```
Re-registering the same plugin instance is a no-op:
```ts
const plugin = { id: "x", priority: 1, detect: { name: "x", environment: "both" } };
registry.registerDetector(plugin);
registry.registerDetector(plugin); // idempotent — listDetectors() length stays 1
```
Schema validation rejects malformed plugins at registration:
```ts
try {
registry.registerFormatter({ id: "bad" } as never);
} catch (err) {
err.code; // "REACT_I18N.PLUGIN_INVALID"
err.cause; // ZodError carrying the issue list
}
```
## API Reference
### `createReactI18nRegistry(): ReactI18nRegistry`
Factory returning a fresh registry instance. No global state — each call returns an independent registry with its own four `Map`-backed stores.
### `ReactI18nRegistry.registerDetector(plugin: DetectorPlugin): Disposable`
Registers a detector plugin under `plugin.id`. Throws `REACT_I18N.DETECTOR_PLUGIN_INVALID` on schema failure or `REACT_I18N.DETECTOR_PLUGIN_DUPLICATE_ID` when a different plugin instance is registered with the same id. Idempotent for the same instance.
### `ReactI18nRegistry.registerFormatter<TArgs, TResult>(plugin: FormatterPlugin<TArgs, TResult>): Disposable`
Registers a formatter plugin. Throws `REACT_I18N.PLUGIN_INVALID` on schema failure or `REACT_I18N.PLUGIN_DUPLICATE_ID` on duplicate id.
### `ReactI18nRegistry.registerPostProcessor(plugin: PostProcessorPlugin): Disposable`
Registers a post-processor plugin executed inside `<Trans>` after ICU interpolation.
### `ReactI18nRegistry.registerLocaleAdapter<TLocale>(plugin: LocaleAdapterPlugin<TLocale>): Disposable`
Registers a locale-adapter plugin used by `react-ui`'s `<Calendar>` and similar third-party-locale-bound primitives.
### `ReactI18nRegistry.listDetectors() / listFormatters() / listPostProcessors() / listLocaleAdapters()`
Snapshot accessors returning the currently-registered plugins per kind. Order matches insertion order (`Map` iteration).
### `ReactI18nRegistry.getFormatter<TArgs, TResult>(id): FormatterPlugin<TArgs, TResult> | undefined`
Constant-time lookup. Returns `undefined` when the id is not registered.
### `ReactI18nRegistry.getLocaleAdapter<TLocale>(id): LocaleAdapterPlugin<TLocale> | undefined`
Constant-time lookup for locale adapters.
### `Disposable.dispose(): void`
Removes the plugin from the registry. Idempotent — calling on a previously-disposed handle is a no-op and emits no second `disposed` event.
## Implementation Notes
- The four stores are independent `Map<string, PluginEntry>`. Same id under two kinds (e.g. a `currency` formatter and a `currency` locale adapter) is intentionally legal — kinds are partitioned namespaces.
- Schema validation uses `.strict()` Zod schemas — extra keys on a plugin trigger `REACT_I18N.PLUGIN_INVALID`. The detect/format/process/resolve function shapes are validated structurally; semantic correctness is the test surface's responsibility.
- OTel attributes are bounded: `kind` is one of four enum values, `outcome` is one of four enum values. The cardinality of the `react_i18n.registry.plugins_total` counter is at most 16. Plugin `id` is NOT a counter attribute — it lives on the formatter-specific histograms (`react_i18n.formatter.duration_ms`) where the cardinality is bounded by the registered-plugin count.
- The registry does not synchronise plugin priorities — splicing detectors by `priority` is done by `defaultDetectorChain()` (consumer side) and ordering post-processors is done by `runPostProcessors()` (consumer side). The registry is purely a store.
- Re-registering the same instance returns a fresh `Disposable` rather than the original handle. This means a caller that re-registers and then disposes either handle removes the plugin once; the second handle's dispose is a no-op (the entry is already gone).