Files
2026-05-27 23:47:53 +01:00

4.0 KiB

useI18n

(no submodules)

Purpose

useI18n() is the foundation hook of @suaveplan/react-i18n. It returns the active I18nInstance carried by the nearest <TranslationProvider> ancestor. Every other hook in the package (useTranslation, useLocale, useDate, useNumber, useCurrency, useRelativeTime, useList, usePlural, useDirection, useErrorMessage) destructures from this base so the rules about missing-provider behaviour live in one place.

The hook intentionally splits its behaviour by environment so the configuration error has different escalation paths in the developer's day-to-day loop and in production traffic. In dev (and tests) the hook throws ReactI18nError(REACT_I18N.NO_PROVIDER) so the bug is visible immediately. In production the hook returns null and emits a single warn-once-per-process log entry — never throwing while real users are trying to read your UI.

Features

  • Returns the active I18nInstance from TranslationContext.
  • Dev-mode (NODE_ENV !== "production"): throws ReactI18nError(REACT_I18N.NO_PROVIDER) outside a Provider.
  • Production-mode: returns null plus a one-shot console.warn.
  • The Provider re-renders subscribers on languageChanged / loaded via useSyncExternalStore, so hooks reading i18n.locale see the update on the same React commit cycle as the bus emission.
  • Zero allocations per render in the common (in-Provider) path.

Usage: Basic Example

import { useI18n, TranslationProvider } from "@suaveplan/react-i18n";
import { createI18n } from "@suaveplan/i18n/plugin";

function HelloLocale() {
    const i18n = useI18n();
    return <span>{i18n?.locale ?? "no-provider"}</span>;
}

const i18n = createI18n({
    defaultLocale: "en-GB",
    messages: { "en-GB": { greeting: "Hello" } },
});

export function App() {
    return (
        <TranslationProvider i18n={i18n}>
            <HelloLocale />
        </TranslationProvider>
    );
}

Usage: Advanced Example

useI18n returns the live instance, so a downstream hook can access plugin slots, subscribe to bus events directly, or call i18n.changeLanguage without going through useLocale. This is the recommended composition path for custom hooks layered on top of the package.

import { useI18n } from "@suaveplan/react-i18n";
import { useEffect, useState } from "react";

export function useNamespaceLoadCount(namespace: string): number {
    const i18n = useI18n();
    const [count, setCount] = useState(0);
    useEffect(() => {
        if (i18n === null) return;
        return i18n.events.on("loaded", (payload) => {
            if (payload.namespace === namespace) {
                setCount((prev) => prev + 1);
            }
        });
    }, [i18n, namespace]);
    return count;
}

API Reference

useI18n(): I18nInstance | null

Returns the active I18nInstance from TranslationContext.

  • Returns: I18nInstance when a <TranslationProvider> ancestor is in scope; null outside a Provider in production.
  • Throws: ReactI18nError(REACT_I18N.NO_PROVIDER) outside a Provider when process.env.NODE_ENV !== "production".

Implementation Notes

  • The null return in production is intentional. Higher-level hooks (useTranslation, useLocale, …) destructure this and switch to identity / no-op fallbacks so end-users never see a runtime crash from a missing Provider.
  • The dedup key for the warn-once-per-process call is "react-i18n.use-i18n.no-provider" — module-level so even multiple call sites and multiple components share the same one-shot.
  • The hook intentionally does NOT subscribe to the bus itself. The Provider holds the only useSyncExternalStore subscription; descendants re-render via that subscription's change-notification cascade. This keeps the hook's per-render cost equal to a single useContext read.
  • process.env.NODE_ENV is read at call-time (via isProductionEnv()) so test suites that swap the env between cases see the swap. Bundler-stamped literals also work because globalThis.process.env.NODE_ENV is the lookup target.