13 KiB
createReactI18nRegistry
(no submodules)
- built-in — (no submodules)- built-in — (no submodules) - built-in — (no submodules)- post-processors — (no submodules) - post-processors — (no submodules)- registry-context — Module-scoped React context that carries the active
ReactI18nRegistryto descendants of<RegistryProvider>. Direct… - registry-provider —
<RegistryProvider>is the React-context surface forReactI18nRegistry. It mounts a fresh registry per tree, registe… - registry — (no submodules)- built-in — _(no submodules)…
- registry.schemas — Strict-object Zod schemas for every plugin kind accepted by
ReactI18nRegistry. The schemas are the boundary at which… - use-registry — These hooks read the active
ReactI18nRegistryfrom React context. The strict variantuseReactI18nRegistry()throws… - built-in — (no submodules)- built-in — (no submodules)- built-in — (no submodules)- post-processors — (no submodules)- post-processors — (no submodules)- registry-context — Module-scoped React context that carries the active
ReactI18nRegistryto descendants of<RegistryProvider>. Direct… - registry-provider —
<RegistryProvider>is the React-context surface forReactI18nRegistry. It mounts a fresh registry per tree, registe… - registry — (no submodules)- registry.schemas — Strict-object Zod schemas for every plugin kind accepted by
ReactI18nRegistry. The schemas are the boundary at which… - use-registry — These hooks read the active
ReactI18nRegistryfrom React context. The strict variantuseReactI18nRegistry()throws… - built-in — (no submodules)- built-in — (no submodules)- built-in — (no submodules)- post-processors — (no submodules)- post-processors — (no submodules)- registry-context — Module-scoped React context that carries the active
ReactI18nRegistryto descendants of<RegistryProvider>. Direct… - registry-provider —
<RegistryProvider>is the React-context surface forReactI18nRegistry. It mounts a fresh registry per tree, registe… - registry — (no submodules)- built-in — <!-- begin:auto-d…
- registry.schemas — Strict-object Zod schemas for every plugin kind accepted by
ReactI18nRegistry. The schemas are the boundary at which… - use-registry — These hooks read the active
ReactI18nRegistryfrom React context. The strict variantuseReactI18nRegistry()throws… - built-in — (no submodules)- built-in — (no submodules)- built-in — (no submodules)- post-processors — (no submodules)- post-processors — (no submodules)- registry-context — Module-scoped React context that carries the active
ReactI18nRegistryto descendants of<RegistryProvider>. Direct… - registry-provider —
<RegistryProvider>is the React-context surface forReactI18nRegistry. It mounts a fresh registry per tree, registe… - registry — (no submodules)- built-in — _(no submodules…
- registry.schemas — Strict-object Zod schemas for every plugin kind accepted by
ReactI18nRegistry. The schemas are the boundary at which… - use-registry — These hooks read the active
ReactI18nRegistryfrom React context. The strict variantuseReactI18nRegistry()throws… - built-in — (no submodules)- built-in — (no submodules) - built-in — (no submodules)- post-processors — (no submodules) - post-processors — (no submodules)- registry-context — Module-scoped React context that carries the active
ReactI18nRegistryto descendants of<RegistryProvider>. Direct… - registry-provider —
<RegistryProvider>is the React-context surface forReactI18nRegistry. It mounts a fresh registry per tree, registe… - registry — (no submodules)- registry.schemas — Strict-object Zod schemas for every plugin kind accepted by
ReactI18nRegistry. The schemas are the boundary at which… - use-registry — These hooks read the active
ReactI18nRegistryfrom React context. The strict variantuseReactI18nRegistry()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 sameidmay 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(orDETECTOR_PLUGIN_DUPLICATE_IDfor detector kind). - Schema-driven shape validation at registration time — malformed plugins throw
REACT_I18N.PLUGIN_INVALID(orDETECTOR_PLUGIN_INVALID) with the Zod issue list attached ascause. - Reversible registration through
Disposablehandles. 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.getand the registry holds no React state itself.
Usage: Basic Example
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:
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:
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:
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. acurrencyformatter and acurrencylocale adapter) is intentionally legal — kinds are partitioned namespaces. - Schema validation uses
.strict()Zod schemas — extra keys on a plugin triggerREACT_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:
kindis one of four enum values,outcomeis one of four enum values. The cardinality of thereact_i18n.registry.plugins_totalcounter is at most 16. Pluginidis 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
priorityis done bydefaultDetectorChain()(consumer side) and ordering post-processors is done byrunPostProcessors()(consumer side). The registry is purely a store. - Re-registering the same instance returns a fresh
Disposablerather 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).