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).
This commit is contained in:
SkateSpots 2026-10-08 18:16:13 +07:00
parent bd9eaf9b5f
commit 281a4c6ba5
3 changed files with 50 additions and 3 deletions

View file

@ -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 (
<div className={cn('flex flex-col', className)}>
{languages.map((lang) => (
<div className="px-1 pb-2">
<input
value={query}
onChange={(e) => 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"
/>
</div>
{filteredLanguages.map((lang) => (
<button
key={lang.code}
type="button"
@ -90,6 +110,11 @@ export function LanguageToggle({ compact = false, inline = false, className, var
{language === lang.code && <span className="ml-auto text-primary">✓</span>}
</button>
))}
{filteredLanguages.length === 0 && (
<div className="px-2 py-1.5 text-sm text-muted-foreground">
{t('nav.noLanguages')}
</div>
)}
</div>
);
}
@ -116,7 +141,20 @@ export function LanguageToggle({ compact = false, inline = false, className, var
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
{languages.map((lang) => (
<div
className="sticky top-0 z-10 -mx-1 -mt-1 mb-1 border-b bg-popover p-1"
onKeyDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
<input
autoFocus
value={query}
onChange={(e) => 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"
/>
</div>
{filteredLanguages.map((lang) => (
<DropdownMenuItem
key={lang.code}
onClick={() => setLanguage(lang.code)}
@ -137,6 +175,11 @@ export function LanguageToggle({ compact = false, inline = false, className, var
)}
</DropdownMenuItem>
))}
{filteredLanguages.length === 0 && (
<div className="px-2 py-1.5 text-sm text-muted-foreground">
{t('nav.noLanguages')}
</div>
)}
</DropdownMenuContent>
</DropdownMenu>
);

View file

@ -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',

View file

@ -58,6 +58,8 @@ const es = {
// Navigation
nav: {
searchLanguages: 'Buscar idiomas...',
noLanguages: 'No se encontraron idiomas',
home: 'Inicio',
map: 'Spots',
lab: 'El Lab',