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 { LanguageCode, type Language } from "@bindings/i18n/models.js"; import { HelpText } from "@/components/typography/HelpText"; import { Label } from "@/components/typography/Label"; 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); useEffect(() => { let cancelled = false; loadLanguages() .then((list) => { if (!cancelled) setLanguages(list); }) .catch(() => {}); 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 select = async (code: string) => { if (busy || code === i18n.language) { setOpen(false); 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); setOpen(false); } }; return (
{t("settings.general.language.help")}
e.preventDefault()} className={cn( "w-[var(--radix-popover-trigger-width)]", "rounded-lg border border-nb-gray-850 bg-nb-gray-920 shadow-lg p-1 z-50", "data-[side=bottom]:origin-top data-[side=top]:origin-bottom", "data-[state=open]:animate-in data-[state=closed]:animate-out", "data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0", "data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95", "data-[side=bottom]:slide-in-from-top-1", "data-[side=top]:slide-in-from-bottom-1", "duration-150 ease-out", )} >
{t("settings.general.language.empty")}
{sorted.map((lang) => { const checked = lang.code === i18n.language; return ( void select(lang.code)} className={cn( "flex items-center gap-2 px-2 py-2 rounded-md cursor-default outline-none my-0.5", "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 && ( )} ); })}
); }