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:
@@ -115,11 +115,20 @@ export default function Nav() {
|
||||
}
|
||||
}, [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 === '/'
|
||||
|
||||
function handleLocaleSwitch() {
|
||||
router.replace(pathname as any, { locale: otherLocale as any })
|
||||
function handleLocaleSwitch(targetLocale: string) {
|
||||
router.replace(pathname as any, { locale: targetLocale as any })
|
||||
setLocaleMenuOpen(false)
|
||||
}
|
||||
|
||||
// Prevent body scroll when mobile menu is open
|
||||
@@ -183,14 +192,58 @@ export default function Nav() {
|
||||
|
||||
{/* ── Desktop right controls ── */}
|
||||
<div className="hidden lg:flex items-center gap-4 shrink-0">
|
||||
{/* Language toggle */}
|
||||
<button
|
||||
onClick={handleLocaleSwitch}
|
||||
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'}`}
|
||||
>
|
||||
{otherLocale.toUpperCase()}
|
||||
</button>
|
||||
{/* Language dropdown */}
|
||||
<div className="relative">
|
||||
<button
|
||||
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"
|
||||
aria-label="Switch language"
|
||||
aria-expanded={localeMenuOpen}
|
||||
aria-haspopup="listbox"
|
||||
>
|
||||
{LOCALE_DISPLAY[currentLocale]?.short ?? currentLocale.toUpperCase()}
|
||||
</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 */}
|
||||
<a
|
||||
@@ -207,7 +260,7 @@ export default function Nav() {
|
||||
{/* ── Mobile hamburger ── */}
|
||||
<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"
|
||||
onClick={() => setMobileOpen(true)}
|
||||
onClick={() => { setMobileOpen(true); setLocaleMenuOpen(false) }}
|
||||
aria-label="Open navigation menu"
|
||||
aria-expanded={mobileOpen}
|
||||
aria-controls="mobile-menu"
|
||||
@@ -300,16 +353,27 @@ export default function Nav() {
|
||||
|
||||
{/* Bottom controls */}
|
||||
<div className="px-6 pb-8 pt-4 flex flex-col gap-3">
|
||||
{/* Language toggle */}
|
||||
<button
|
||||
onClick={() => {
|
||||
handleLocaleSwitch()
|
||||
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"
|
||||
>
|
||||
{otherLocale === 'fr' ? '🇫🇷 Français' : '🇬🇧 English'}
|
||||
</button>
|
||||
{/* Language options */}
|
||||
<div className="flex flex-col gap-1">
|
||||
{locales
|
||||
.filter((l) => l !== currentLocale)
|
||||
.map((l) => {
|
||||
const display = LOCALE_DISPLAY[l]
|
||||
return (
|
||||
<button
|
||||
key={l}
|
||||
onClick={() => {
|
||||
handleLocaleSwitch(l)
|
||||
setMobileOpen(false)
|
||||
}}
|
||||
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"
|
||||
>
|
||||
<span aria-hidden="true">{display?.flag}</span>
|
||||
<span>{display?.label}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<a
|
||||
|
||||
Reference in New Issue
Block a user