feat(i18n): redesign nav locale switcher from binary toggle to multi-locale dropdown

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 14:11:17 -04:00
parent 23a84cd31b
commit 890f2184e1

View File

@@ -115,11 +115,20 @@ export default function Nav() {
} }
}, [pathname]) }, [pathname])
const otherLocale = locales.find((l) => l !== currentLocale) ?? 'fr' const [localeMenuOpen, setLocaleMenuOpen] = useState(false)
const LOCALE_DISPLAY: Record<string, { flag: string; label: string; short: string }> = {
en: { flag: '🇬🇧', label: 'English', short: 'EN' },
fr: { flag: '🇫🇷', label: 'Français', short: 'FR' },
it: { flag: '🇮🇹', label: 'Italiano', short: 'IT' },
es: { flag: '🇪🇸', label: 'Español', short: 'ES' },
}
const isHomePage = pathname === '/' const isHomePage = pathname === '/'
function handleLocaleSwitch() { function handleLocaleSwitch(targetLocale: string) {
router.replace(pathname as any, { locale: otherLocale as any }) router.replace(pathname as any, { locale: targetLocale as any })
setLocaleMenuOpen(false)
} }
// Prevent body scroll when mobile menu is open // Prevent body scroll when mobile menu is open
@@ -183,15 +192,59 @@ export default function Nav() {
{/* ── Desktop right controls ── */} {/* ── Desktop right controls ── */}
<div className="hidden lg:flex items-center gap-4 shrink-0"> <div className="hidden lg:flex items-center gap-4 shrink-0">
{/* Language toggle */} {/* Language dropdown */}
<div className="relative">
<button <button
onClick={handleLocaleSwitch} onClick={() => setLocaleMenuOpen((prev) => !prev)}
className="label-md text-on-surface/60 hover:text-on-surface transition-colors duration-200 px-2 py-1 rounded focus-visible:outline-2 focus-visible:outline-primary focus-visible:outline-offset-2" className="label-md text-on-surface/60 hover:text-on-surface transition-colors duration-200 px-2 py-1 rounded focus-visible:outline-2 focus-visible:outline-primary focus-visible:outline-offset-2"
aria-label={`Switch to ${otherLocale === 'fr' ? 'French' : 'English'}`} aria-label="Switch language"
aria-expanded={localeMenuOpen}
aria-haspopup="listbox"
> >
{otherLocale.toUpperCase()} {LOCALE_DISPLAY[currentLocale]?.short ?? currentLocale.toUpperCase()}
</button> </button>
{/* Transparent overlay to close on outside click */}
{localeMenuOpen && (
<div
className="fixed inset-0 z-[40]"
aria-hidden="true"
onClick={() => setLocaleMenuOpen(false)}
/>
)}
<AnimatePresence>
{localeMenuOpen && (
<motion.ul
role="listbox"
aria-label="Select language"
className="absolute right-0 top-full mt-2 z-[41] bg-surface-high rounded-xl shadow-lg border border-outline-variant/20 overflow-hidden min-w-[140px]"
initial={{ opacity: 0, y: -6, scale: 0.97 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -6, scale: 0.97 }}
transition={{ duration: 0.18, ease: [0.25, 0.46, 0.45, 0.94] }}
>
{locales
.filter((l) => l !== currentLocale)
.map((l) => {
const display = LOCALE_DISPLAY[l]
return (
<li key={l} role="option" aria-selected={false}>
<button
onClick={() => handleLocaleSwitch(l)}
className="w-full flex items-center gap-2.5 px-4 py-2.5 label-md text-on-surface/70 hover:text-on-surface hover:bg-surface-low transition-colors duration-150 text-left"
>
<span aria-hidden="true">{display?.flag}</span>
<span>{display?.label}</span>
</button>
</li>
)
})}
</motion.ul>
)}
</AnimatePresence>
</div>
{/* Start a Project CTA */} {/* Start a Project CTA */}
<a <a
href="#configure" href="#configure"
@@ -207,7 +260,7 @@ export default function Nav() {
{/* ── Mobile hamburger ── */} {/* ── Mobile hamburger ── */}
<button <button
className="lg:hidden p-2 -mr-2 text-on-surface focus-visible:outline-2 focus-visible:outline-primary focus-visible:outline-offset-2 rounded" className="lg:hidden p-2 -mr-2 text-on-surface focus-visible:outline-2 focus-visible:outline-primary focus-visible:outline-offset-2 rounded"
onClick={() => setMobileOpen(true)} onClick={() => { setMobileOpen(true); setLocaleMenuOpen(false) }}
aria-label="Open navigation menu" aria-label="Open navigation menu"
aria-expanded={mobileOpen} aria-expanded={mobileOpen}
aria-controls="mobile-menu" aria-controls="mobile-menu"
@@ -300,16 +353,27 @@ export default function Nav() {
{/* Bottom controls */} {/* Bottom controls */}
<div className="px-6 pb-8 pt-4 flex flex-col gap-3"> <div className="px-6 pb-8 pt-4 flex flex-col gap-3">
{/* Language toggle */} {/* Language options */}
<div className="flex flex-col gap-1">
{locales
.filter((l) => l !== currentLocale)
.map((l) => {
const display = LOCALE_DISPLAY[l]
return (
<button <button
key={l}
onClick={() => { onClick={() => {
handleLocaleSwitch() handleLocaleSwitch(l)
setMobileOpen(false) setMobileOpen(false)
}} }}
className="w-full py-3 label-md text-on-surface/60 hover:text-on-surface transition-colors duration-200 text-left focus-visible:outline-2 focus-visible:outline-primary focus-visible:outline-offset-2 rounded" className="w-full py-2.5 label-md text-on-surface/60 hover:text-on-surface transition-colors duration-200 text-left focus-visible:outline-2 focus-visible:outline-primary focus-visible:outline-offset-2 rounded flex items-center gap-2.5"
> >
{otherLocale === 'fr' ? '🇫🇷 Français' : '🇬🇧 English'} <span aria-hidden="true">{display?.flag}</span>
<span>{display?.label}</span>
</button> </button>
)
})}
</div>
{/* CTA */} {/* CTA */}
<a <a