import { useState } from 'react' import { Button } from '@/components/ui/button' import { Command, CommandInput, CommandItem, CommandList } from '@/components/ui/command' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet' import { useIsMobile } from '@/hooks/use-mobile' import { type Locale, LOCALE_META, useI18n } from '@/i18n' import { triggerHaptic } from '@/lib/haptics' import { Check, ChevronDown, Globe } from '@/lib/icons' import { normalize } from '@/lib/text' import { cn } from '@/lib/utils' import { notifyError } from '@/store/notifications' export interface LanguageSwitcherProps { className?: string collapsed?: boolean dropUp?: boolean } interface LanguageCommandProps { allLocales: Array<[Locale, (typeof LOCALE_META)[Locale]]> autoFocus?: boolean disabled?: boolean locale: Locale noResults: string onSelect: (code: Locale) => void searchPlaceholder: string } export function LanguageSwitcher({ className, collapsed = false, dropUp = false }: LanguageSwitcherProps) { const { isSavingLocale, locale, setLocale, t } = useI18n() const [open, setOpen] = useState(false) const isMobile = useIsMobile() const useMobileSheet = Boolean(dropUp && isMobile) const current = LOCALE_META[locale] const allLocales = Object.entries(LOCALE_META) as Array<[Locale, typeof current]> const title = t.language.switchTo const selectLocale = async (code: Locale) => { if (code === locale || isSavingLocale) { setOpen(false) return } triggerHaptic('selection') try { await setLocale(code) setOpen(false) triggerHaptic('success') } catch (error) { notifyError(error, t.language.saveError) } } const trigger = ( ) if (useMobileSheet) { return ( {trigger} {title} {t.language.description} void selectLocale(code)} searchPlaceholder={t.language.searchPlaceholder} /> ) } return ( {trigger} void selectLocale(code)} searchPlaceholder={t.language.searchPlaceholder} /> ) } function LanguageCommand({ allLocales, autoFocus, disabled, locale, noResults, onSelect, searchPlaceholder }: LanguageCommandProps) { const [search, setSearch] = useState('') // Own the search term and filter manually. cmdk's built-in shouldFilter // reorders items by its fuzzy-match score (≈alphabetical with an empty // query), which destroys the curated en→zh→zh-hant→ja order. We disable it // and do a plain substring filter that preserves array order — matching // model-picker.tsx. Match against the endonym, the (hidden) English name, // and the locale code so "日本"/"japanese"/"ja" all find Japanese. const q = normalize(search) const filtered = allLocales.filter( ([code, meta]) => !q || meta.name.toLowerCase().includes(q) || meta.englishName.toLowerCase().includes(q) || code.toLowerCase().includes(q) ) return ( {filtered.length === 0 ? (
{noResults}
) : ( filtered.map(([code, meta]) => { const selected = code === locale return ( onSelect(code)} value={code} > {meta.name} {code} ) }) )}
) }