Skip to content

Commit bccab7b

Browse files
committed
add language switcher
1 parent db8c3ce commit bccab7b

File tree

6 files changed

+106
-3
lines changed

6 files changed

+106
-3
lines changed

app/[lang]/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Projects } from '@/components/sections/projects';
66
import { Navbar } from '@/components/sections/navbar';
77
import { getDictionary, type Locale } from '@/lib/i18n';
88
import { notFound } from 'next/navigation';
9+
import { LangRedirect } from '@/components/lang-redirect';
910

1011
type Params = { lang: Locale };
1112

@@ -20,7 +21,8 @@ export default function LangPage({ params }: { params: Params }) {
2021
const dict = getDictionary(params.lang);
2122
return (
2223
<main className="min-h-screen">
23-
<Navbar content={dict.nav} />
24+
<LangRedirect currentLocale={params.lang} />
25+
<Navbar content={dict.nav} currentLocale={params.lang} />
2426
<div className="bg-page-gradient">
2527
<Hero content={dict.hero} />
2628
<About content={dict.about} />

app/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import { Hero } from '@/components/sections/hero';
55
import { Projects } from '@/components/sections/projects';
66
import { Navbar } from '@/components/sections/navbar';
77
import { getDictionary } from '@/lib/i18n';
8+
import { LangRedirect } from '@/components/lang-redirect';
89

910
export default function HomePage() {
1011
const dict = getDictionary('es');
1112
return (
1213
<main className="min-h-screen">
13-
<Navbar content={dict.nav} />
14+
<LangRedirect currentLocale="es" />
15+
<Navbar content={dict.nav} currentLocale="es" />
1416
<div className="bg-page-gradient">
1517
<Hero content={dict.hero} />
1618
<About content={dict.about} />

components/lang-redirect.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use client';
2+
3+
import { normalizeLocale, type Locale } from '@/lib/i18n';
4+
import { useEffect } from 'react';
5+
6+
const STORAGE_KEY = 'preferred-lang';
7+
8+
function pathForLocale(locale: Locale): string {
9+
return locale === 'es' ? '/' : `/${locale}`;
10+
}
11+
12+
export function LangRedirect({ currentLocale }: { currentLocale: Locale }) {
13+
useEffect(() => {
14+
const stored = localStorage.getItem(STORAGE_KEY);
15+
if (stored === 'en' || stored === 'es') {
16+
if (stored !== currentLocale) {
17+
window.location.assign(pathForLocale(stored));
18+
}
19+
return;
20+
}
21+
22+
const navigatorLang =
23+
typeof navigator !== 'undefined' ? navigator.language || navigator.languages?.[0] : undefined;
24+
const detected = normalizeLocale(navigatorLang);
25+
26+
if (detected !== currentLocale) {
27+
localStorage.setItem(STORAGE_KEY, detected);
28+
window.location.assign(pathForLocale(detected));
29+
}
30+
}, [currentLocale]);
31+
32+
return null;
33+
}

components/lang-switcher.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use client';
2+
3+
import type { Locale } from '@/lib/i18n';
4+
import { cn } from '@/lib/utils';
5+
import { useEffect, useState } from 'react';
6+
import { useRouter } from 'next/navigation';
7+
8+
const STORAGE_KEY = 'preferred-lang';
9+
10+
function pathForLocale(locale: Locale): string {
11+
return locale === 'es' ? '/' : `/${locale}`;
12+
}
13+
14+
type Props = {
15+
current: Locale;
16+
className?: string;
17+
};
18+
19+
export function LangSwitcher({ current, className }: Props) {
20+
const router = useRouter();
21+
const [active, setActive] = useState<Locale>(current);
22+
23+
useEffect(() => {
24+
setActive(current);
25+
}, [current]);
26+
27+
function handleChange(locale: Locale) {
28+
if (locale === active) return;
29+
setActive(locale);
30+
localStorage.setItem(STORAGE_KEY, locale);
31+
router.push(pathForLocale(locale));
32+
}
33+
34+
return (
35+
<div className={cn('flex items-center gap-1 rounded-full border border-white/10 bg-white/5 px-1 py-0.5', className)}>
36+
{(['es', 'en'] as Locale[]).map((locale) => (
37+
<button
38+
key={locale}
39+
type="button"
40+
onClick={() => handleChange(locale)}
41+
className={cn(
42+
'px-2 py-1 text-xs rounded-full transition-colors',
43+
active === locale
44+
? 'bg-accent-mint/20 text-accent-mint'
45+
: 'text-base-muted hover:text-base-text'
46+
)}
47+
aria-pressed={active === locale}
48+
>
49+
{locale.toUpperCase()}
50+
</button>
51+
))}
52+
</div>
53+
);
54+
}

components/sections/navbar.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import Link from 'next/link';
22
import type { NavContent } from '@/lib/i18n';
3+
import { LangSwitcher } from '@/components/lang-switcher';
4+
import type { Locale } from '@/lib/i18n';
35

46
type Props = {
57
content: NavContent;
8+
currentLocale: Locale;
69
};
710

8-
export function Navbar({ content }: Props) {
11+
export function Navbar({ content, currentLocale }: Props) {
912
return (
1013
<header className="sticky top-0 z-20 bg-base-bg/70 backdrop-blur-md border-b border-white/10">
1114
<div className="max-w-6xl mx-auto px-4 h-14 flex items-center justify-between">
@@ -18,6 +21,7 @@ export function Navbar({ content }: Props) {
1821
{link.label}
1922
</Link>
2023
))}
24+
<LangSwitcher current={currentLocale} />
2125
</nav>
2226
</div>
2327
</header>

lib/i18n.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
export type Locale = 'es' | 'en';
22

3+
export function normalizeLocale(input?: string): Locale {
4+
if (!input) return 'es';
5+
const lower = input.toLowerCase();
6+
if (lower.startsWith('en')) return 'en';
7+
if (lower.startsWith('es')) return 'es';
8+
return 'es';
9+
}
10+
311
export type NavContent = {
412
brand: string;
513
links: { href: string; label: string }[];

0 commit comments

Comments
 (0)