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

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 LocaleDescriptor objects
  • Auto-derives direction (ltr / rtl) via getTextDirection from @suaveplan/i18n
  • change(locale): Promise<void> returns when the language change resolves so callers can chain await change(...)
  • isPending aggregates the useTransition flag 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 via getTextDirection.
  • children: (state: LanguageSwitcherState) => ReactNode — render-prop child. Receives the resolved state object.

LanguageSwitcherState

  • locales: readonly LocaleDescriptor[]
  • current: string
  • change: (locale: string) => Promise<void> — wrapped in useTransition; returns when the underlying i18n.changeLanguage resolves
  • isPending: boolean — true while a transition or awaitable change is in flight

LocaleDescriptor

  • code: string
  • name?: string — Latin-script display name
  • nativeName?: string — native-script display name
  • direction?: "ltr" | "rtl" — derived when not supplied

Throws ReactI18nError(REACT_I18N.NO_PROVIDER) outside a TranslationProvider.

Implementation Notes

  • The component uses useSyncExternalStore so current reflects the active locale across concurrent renders.
  • useTransition alone surfaces pendingness only during a render. The component layers an explicit awaiting flag so an await change(...) in user code observes the entire async lifecycle (start → load → end) rather than only the React-scheduled portion.
  • getTextDirection is invoked once per render via useMemo keyed on props.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 — no displayName warnings, no Strict Mode double-invocation surprise.