import { useState, useRef, useEffect } from "react"; import { Button } from "@nous-research/ui/ui/components/button"; import { BottomPickSheet } from "@/components/BottomPickSheet"; import { Typography } from "@/components/NouiTypography"; import { useBelowBreakpoint } from "@/hooks/useBelowBreakpoint"; import { useI18n } from "@/i18n/context"; import { LOCALE_META } from "@/i18n"; import type { Locale } from "@/i18n"; import { cn } from "@/lib/utils"; /** * Language picker — shows the current language's endonym, opens a dropdown * of all supported locales when clicked. Persists choice to localStorage via * the I18n context. * * Replaces the older two-state EN↔ZH toggle now that we ship 16 locales * (en, zh, zh-hant, ja, de, es, fr, tr, uk, af, ko, it, ga, pt, ru, hu). * * No country flags by design — languages aren't countries, and flag pairings * inevitably create political mismappings (e.g. Mandarin variants ≠ any single * jurisdiction, English ≠ GB, Portuguese ≠ PT). Endonyms are unambiguous. * * When placed at the bottom of the sidebar (next to ThemeSwitcher), pass * `dropUp` so the list opens above the trigger and avoids clipping below the * viewport / overflow ancestors. Below the `sm` breakpoint, `dropUp` uses a * bottom sheet portaled to `document.body` instead of an anchored dropdown. */ export function LanguageSwitcher({ dropUp = false }: LanguageSwitcherProps) { const { locale, setLocale, t } = useI18n(); const [open, setOpen] = useState(false); const containerRef = useRef(null); const narrowViewport = useBelowBreakpoint(640); const useMobileSheet = Boolean(dropUp && narrowViewport); useEffect(() => { if (!open) return; function onKey(e: KeyboardEvent) { if (e.key === "Escape") setOpen(false); } document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [open]); // Outside-click closing only for anchored dropdown — sheet uses backdrop + portal. useEffect(() => { if (!open || useMobileSheet) return; function onPointerDown(e: PointerEvent) { if (!containerRef.current) return; if (!containerRef.current.contains(e.target as Node)) { setOpen(false); } } document.addEventListener("pointerdown", onPointerDown); return () => document.removeEventListener("pointerdown", onPointerDown); }, [open, useMobileSheet]); const current = LOCALE_META[locale]; const allLocales = Object.entries(LOCALE_META) as Array<[Locale, typeof current]>; const sheetTitle = t.language.switchTo; return (
{useMobileSheet && ( setOpen(false)} open={open} title={sheetTitle} >
)} {open && !useMobileSheet && (
)}
); } function LanguageSwitcherOptions({ allLocales, locale, setLocale, setOpen, }: LanguageSwitcherOptionsProps) { return ( <> {allLocales.map(([code, meta]) => { const selected = code === locale; return ( ); })} ); } interface LanguageSwitcherOptionsProps { allLocales: Array<[Locale, (typeof LOCALE_META)[Locale]]>; locale: Locale; setLocale: (code: Locale) => void; setOpen: (open: boolean) => void; } interface LanguageSwitcherProps { dropUp?: boolean; }