4.9 KiB
<LanguageSwitcher>
(no submodules)
Purpose
<LanguageSwitcher> is a headless render-prop component that exposes the
language-switching surface to consumer-controlled UI. It renders no DOM
markup of its own — the caller controls the visual treatment via the
render-prop child. The component bundles the bookkeeping that every
language switcher needs: it normalises the supplied locale list into
LocaleDescriptor objects with derived text-direction, subscribes to
languageChanged / loaded events through the active Provider's
I18nInstance, wraps the locale-change call in useTransition so the
namespace load triggered by the switch never blocks urgent updates, and
exposes isPending so the caller can render a busy state.
Features
- Headless: zero DOM, zero CSS — render any UI in the render prop
- Locale normalisation: accepts string codes or
LocaleDescriptorobjects - Auto-derives
direction(ltr/rtl) viagetTextDirectionfrom@suaveplan/i18n change(locale): Promise<void>returns when the language change resolves so callers can chainawait change(...)isPendingaggregates theuseTransitionflag and the awaitable in-flight flag so both render-time pendingness and async resolution are surfaced- Throws
ReactI18nError(REACT_I18N.NO_PROVIDER)outside a Provider
Usage: Basic Example
import { TranslationProvider } from "@suaveplan/react-i18n";
import { LanguageSwitcher } from "@suaveplan/react-i18n/components/language-switcher";
<TranslationProvider i18n={i18n}>
<LanguageSwitcher locales={["en-GB", "fr-FR", "ar-AE"]}>
{({ locales, current, change, isPending }) => (
<select
value={current}
disabled={isPending}
onChange={(e) => change(e.target.value)}
>
{locales.map((l) => (
<option key={l.code} value={l.code}>
{l.code}
</option>
))}
</select>
)}
</LanguageSwitcher>
</TranslationProvider>;
Usage: Advanced Example
A typed dropdown with custom labels, RTL-aware ordering, and loading spinner:
import { LanguageSwitcher } from "@suaveplan/react-i18n/components/language-switcher";
<LanguageSwitcher
locales={[
{ code: "en-GB", name: "English", nativeName: "English" },
{ code: "fr-FR", name: "French", nativeName: "Français" },
{ code: "ar-AE", name: "Arabic", nativeName: "العربية", direction: "rtl" },
]}
>
{({ locales, current, change, isPending }) => (
<div role="group" aria-label="Language">
{locales.map((l) => (
<button
key={l.code}
type="button"
aria-pressed={l.code === current}
aria-busy={isPending && l.code !== current}
dir={l.direction}
onClick={() => {
void change(l.code);
}}
>
{l.nativeName ?? l.code}
</button>
))}
</div>
)}
</LanguageSwitcher>;
API Reference
LanguageSwitcher(props: LanguageSwitcherProps): ReactElement | null
Props:
locales: ReadonlyArray<string | LocaleDescriptor>— locales to surface to the render-prop. Strings are upgraded to{ code, direction }descriptors viagetTextDirection.children: (state: LanguageSwitcherState) => ReactNode— render-prop child. Receives the resolved state object.
LanguageSwitcherState
locales: readonly LocaleDescriptor[]current: stringchange: (locale: string) => Promise<void>— wrapped inuseTransition; returns when the underlyingi18n.changeLanguageresolvesisPending: boolean— true while a transition or awaitable change is in flight
LocaleDescriptor
code: stringname?: string— Latin-script display namenativeName?: string— native-script display namedirection?: "ltr" | "rtl"— derived when not supplied
Throws ReactI18nError(REACT_I18N.NO_PROVIDER) outside a
TranslationProvider.
Implementation Notes
- The component uses
useSyncExternalStoresocurrentreflects the active locale across concurrent renders. useTransitionalone surfaces pendingness only during a render. The component layers an explicit awaiting flag so anawait change(...)in user code observes the entire async lifecycle (start → load → end) rather than only the React-scheduled portion.getTextDirectionis invoked once per render viauseMemokeyed onprops.locales. Stable input arrays therefore avoid re-deriving direction on every render.- The render-prop is invoked as a function, not as JSX (
children(state)), so React does not treat the function as a component instance — nodisplayNamewarnings, no Strict Mode double-invocation surprise.