From 281a4c6ba5265206082b8562a317c4a58a30c0fc Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Thu, 8 Oct 2026 18:16:13 +0700 Subject: [PATCH] feat(i18n): add search/filter to the language picker The language list now has a search input in both the header dropdown (pinned above the scrolling list) and the inline list used in the mobile nav sheet. Filtering matches the English name, native name, or locale code, and shows a localized empty state. Adds nav.searchLanguages and nav.noLanguages to en/es (other locales fall back to English). --- src/components/LanguageToggle.tsx | 49 +++++++++++++++++++++++++++++-- src/i18n/en.ts | 2 ++ src/i18n/es.ts | 2 ++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/components/LanguageToggle.tsx b/src/components/LanguageToggle.tsx index add2b1d..34f554b 100644 --- a/src/components/LanguageToggle.tsx +++ b/src/components/LanguageToggle.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { Languages } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { @@ -61,14 +62,33 @@ interface LanguageToggleProps { * Language toggle dropdown for switching between English and Spanish */ export function LanguageToggle({ compact = false, inline = false, className, variant = 'ghost' }: LanguageToggleProps) { - const { language, setLanguage } = useTranslation(); + const { language, setLanguage, t } = useTranslation(); + const [query, setQuery] = useState(''); + + const safeQuery = query.trim().toLowerCase(); + const filteredLanguages = safeQuery + ? languages.filter( + (l) => + l.name.toLowerCase().includes(safeQuery) || + l.nativeName.toLowerCase().includes(safeQuery) || + l.code.toLowerCase().includes(safeQuery) + ) + : languages; const currentLanguage = languages.find(l => l.code === language) || languages[0]; if (inline) { return (
- {languages.map((lang) => ( +
+ setQuery(e.target.value)} + placeholder={t('nav.searchLanguages')} + className="h-9 w-full rounded-lg border border-border bg-background px-3 text-sm outline-none focus:border-primary" + /> +
+ {filteredLanguages.map((lang) => ( ))} + {filteredLanguages.length === 0 && ( +
+ {t('nav.noLanguages')} +
+ )}
); } @@ -116,7 +141,20 @@ export function LanguageToggle({ compact = false, inline = false, className, var - {languages.map((lang) => ( +
e.stopPropagation()} + onPointerDown={(e) => e.stopPropagation()} + > + setQuery(e.target.value)} + placeholder={t('nav.searchLanguages')} + className="h-8 w-full rounded-sm bg-transparent px-2 text-sm outline-none placeholder:text-muted-foreground" + /> +
+ {filteredLanguages.map((lang) => ( setLanguage(lang.code)} @@ -137,6 +175,11 @@ export function LanguageToggle({ compact = false, inline = false, className, var )} ))} + {filteredLanguages.length === 0 && ( +
+ {t('nav.noLanguages')} +
+ )}
); diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 3a90b16..376d182 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -57,6 +57,8 @@ const en = { // Navigation nav: { + searchLanguages: 'Search languages...', + noLanguages: 'No languages found', home: 'Home', // Kept to one word so the mobile tab bar labels stay on a single line. map: 'Spots', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 47844aa..e018747 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -58,6 +58,8 @@ const es = { // Navigation nav: { + searchLanguages: 'Buscar idiomas...', + noLanguages: 'No se encontraron idiomas', home: 'Inicio', map: 'Spots', lab: 'El Lab',