Files
skills/docs/localization/i18n/react-i18n/src/registry/registry.md
2026-05-29 08:20:56 +01:00

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 ReactI18nRegistry to descendants of <RegistryProvider>. Direct…
  • registry-provider — <RegistryProvider> is the React-context surface for ReactI18nRegistry. 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 ReactI18nRegistry from React context. The strict variant useReactI18nRegistry() 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 ReactI18nRegistry to descendants of <RegistryProvider>. Direct…
  • registry-provider — <RegistryProvider> is the React-context surface for ReactI18nRegistry. 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 ReactI18nRegistry from React context. The strict variant useReactI18nRegistry() 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 ReactI18nRegistry to descendants of <RegistryProvider>. Direct…
  • registry-provider — <RegistryProvider> is the React-context surface for ReactI18nRegistry. 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 ReactI18nRegistry from React context. The strict variant useReactI18nRegistry() 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 ReactI18nRegistry to descendants of <RegistryProvider>. Direct…
  • registry-provider — <RegistryProvider> is the React-context surface for ReactI18nRegistry. 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 ReactI18nRegistry from React context. The strict variant useReactI18nRegistry() 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 ReactI18nRegistry to descendants of <RegistryProvider>. Direct…
  • registry-provider — <RegistryProvider> is the React-context surface for ReactI18nRegistry. 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 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

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. 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).