# `createReactI18nRegistry` _(no submodules)_ - [built-in](./formatters/built-in/built-in.md) — _(no submodules)_- [built-in](./locale-adapters/built-in/built-in.md) — _(no submodules)_ - [built-in](./locale-adapters/built-in/built-in.md) — _(no submodules)_- [post-processors](./post-processors/post-processors.md) — _(no submodules)_ - [post-processors](./post-processors/post-processors.md) — _(no submodules)_- [registry-context](./registry-context.md) — Module-scoped React context that carries the active `ReactI18nRegistry` to descendants of ``. Direct… - [registry-provider](./registry-provider.md) — `` is the React-context surface for `ReactI18nRegistry`. It mounts a fresh registry per tree, registe… - [registry](./registry.md) — _(no submodules)_- built-in — _(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) — _(no submodules)_- [built-in](./locale-adapters/built-in/built-in.md) — _(no submodules)_- [built-in](./locale-adapters/built-in/built-in.md) — _(no submodules)_- [post-processors](./post-processors/post-processors.md) — _(no submodules)_- [post-processors](./post-processors/post-processors.md) — _(no submodules)_- [registry-context](./registry-context.md) — Module-scoped React context that carries the active `ReactI18nRegistry` to descendants of ``. Direct… - [registry-provider](./registry-provider.md) — `` is the React-context surface for `ReactI18nRegistry`. It mounts a fresh registry per tree, registe… - [registry](./registry.md) — _(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) — _(no submodules)_- [built-in](./locale-adapters/built-in/built-in.md) — _(no submodules)_- [built-in](./locale-adapters/built-in/built-in.md) — _(no submodules)_- [post-processors](./post-processors/post-processors.md) — _(no submodules)_- [post-processors](./post-processors/post-processors.md) — _(no submodules)_- [registry-context](./registry-context.md) — Module-scoped React context that carries the active `ReactI18nRegistry` to descendants of ``. Direct… - [registry-provider](./registry-provider.md) — `` is the React-context surface for `ReactI18nRegistry`. It mounts a fresh registry per tree, registe… - [registry](./registry.md) — _(no submodules)_- built-in — _(no submodules)_- [built-in](./locale-adapters/built-in/built-in.md) — _(no submodules)_- [built-in](./locale-adapters/built-in/built-in.md) — _(no submodules)_- [post-processors](./post-processors/post-processors.md) — _(no submodules)_- [post-processors](./post-processors/post-processors.md) — _(no submodules)_- [registry-context](./registry-context.md) — Module-scoped React context that carries the active `ReactI18nRegistry` to descendants of ``. Direct… - [registry-provider](./registry-provider.md) — `` is the React-context surface for `ReactI18nRegistry`. It mounts a fresh registry per tree, registe… - [registry](./registry.md) — _(no submodules)_- built-in — _(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) — _(no submodules)_- [built-in](./locale-adapters/built-in/built-in.md) — _(no submodules)_ - [built-in](./locale-adapters/built-in/built-in.md) — _(no submodules)_- [post-processors](./post-processors/post-processors.md) — _(no submodules)_ - [post-processors](./post-processors/post-processors.md) — _(no submodules)_- [registry-context](./registry-context.md) — Module-scoped React context that carries the active `ReactI18nRegistry` to descendants of ``. Direct… - [registry-provider](./registry-provider.md) — `` is the React-context surface for `ReactI18nRegistry`. It mounts a fresh registry per tree, registe… - [registry](./registry.md) — _(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 `` 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`, `PostProcessorPlugin`, `LocaleAdapterPlugin` — 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(plugin: FormatterPlugin): 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 `` after ICU interpolation. ### `ReactI18nRegistry.registerLocaleAdapter(plugin: LocaleAdapterPlugin): Disposable` Registers a locale-adapter plugin used by `react-ui`'s `` 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(id): FormatterPlugin | undefined` Constant-time lookup. Returns `undefined` when the id is not registered. ### `ReactI18nRegistry.getLocaleAdapter(id): LocaleAdapterPlugin | 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`. 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).