import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import * as Popover from "@radix-ui/react-popover"; import * as ScrollArea from "@radix-ui/react-scroll-area"; import { Command } from "cmdk"; import { CheckIcon, ChevronDown, LanguagesIcon, Search } from "lucide-react"; import { Preferences } from "@bindings/services"; import { type LanguageCode, type Language } from "@bindings/i18n/models.js"; import { HelpText } from "@/components/typography/HelpText"; import { Label } from "@/components/typography/Label"; import { useFocusVisible } from "@/hooks/useFocusVisible"; import { loadLanguages } from "@/lib/i18n"; import { cn } from "@/lib/cn"; import { errorDialog, formatErrorMessage } from "@/lib/errors"; // No flag icons: flags represent countries, not languages. https://www.flagsarenotlanguages.com/blog/ const labelFor = (lang: Language): string => lang.englishName && lang.englishName !== lang.displayName ? `${lang.displayName} (${lang.englishName})` : lang.displayName; export function LanguagePicker() { const { t, i18n } = useTranslation(); const [languages, setLanguages] = useState([]); const [open, setOpen] = useState(false); const [busy, setBusy] = useState(false); const isFocusVisible = useFocusVisible(); useEffect(() => { let cancelled = false; loadLanguages() .then((list) => { if (!cancelled) setLanguages(list); }) .catch((err: unknown) => console.error("load languages failed", err)); return () => { cancelled = true; }; }, []); const sorted = useMemo( () => [...languages].sort((a, b) => a.displayName.localeCompare(b.displayName)), [languages], ); const current = useMemo( () => languages.find((l) => l.code === i18n.language) ?? languages.find((l) => l.code === "en"), [languages, i18n.language], ); const handleTriggerKeyDown = (e: React.KeyboardEvent) => { if (open) return; if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault(); setOpen(true); } }; const select = async (code: string) => { setOpen(false); if (busy || code === i18n.language) return; setBusy(true); try { await Preferences.SetLanguage(code as LanguageCode); } catch (e) { await errorDialog({ Title: t("settings.error.saveTitle"), Message: formatErrorMessage(e), }); } finally { setBusy(false); } }; return (
{t("settings.general.language.help")}
{t("settings.general.language.empty")}
{sorted.map((lang) => { const checked = lang.code === i18n.language; return ( void select(lang.code)} className={cn( "my-0.5 flex cursor-default items-center gap-2 rounded-md px-2 py-2 outline-none", "text-xs font-semibold text-nb-gray-200", "data-[selected=true]:bg-nb-gray-850 data-[selected=true]:text-nb-gray-50", )} > {labelFor(lang)} {checked && ( )} ); })}
); }