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
I18nInstancefromTranslationContext. - Dev-mode (NODE_ENV !== "production"): throws
ReactI18nError(REACT_I18N.NO_PROVIDER)outside a Provider. - Production-mode: returns
nullplus a one-shotconsole.warn. - The Provider re-renders subscribers on
languageChanged/loadedviauseSyncExternalStore, so hooks readingi18n.localesee 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:
I18nInstancewhen a<TranslationProvider>ancestor is in scope;nulloutside a Provider in production. - Throws:
ReactI18nError(REACT_I18N.NO_PROVIDER)outside a Provider whenprocess.env.NODE_ENV !== "production".
Implementation Notes
- The
nullreturn 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
useSyncExternalStoresubscription; descendants re-render via that subscription's change-notification cascade. This keeps the hook's per-render cost equal to a singleuseContextread. process.env.NODE_ENVis read at call-time (viaisProductionEnv()) so test suites that swap the env between cases see the swap. Bundler-stamped literals also work becauseglobalThis.process.env.NODE_ENVis the lookup target.