Add i18n support with NL/EN language switching (ST-001)

- Add English translations to cv-data.ts with getCvData(lang) helper
- Create app/[lang]/ routing with static generation for nl and en
- Add language switcher in nav (NL / EN toggle)
- Add middleware for Accept-Language auto-redirect on root path
- Root layout reads x-lang header to set <html lang> correctly
- All components accept lang prop and render translated content

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-29 15:31:13 +02:00
parent 61e603b5d7
commit d352a7d496
13 changed files with 540 additions and 177 deletions

39
app/[lang]/layout.tsx Normal file
View file

@ -0,0 +1,39 @@
import type { Metadata } from "next";
export async function generateStaticParams() {
return [{ lang: "nl" }, { lang: "en" }];
}
export async function generateMetadata({
params,
}: {
params: Promise<{ lang: string }>;
}): Promise<Metadata> {
const { lang } = await params;
const isEn = lang === "en";
return {
title: "Janpeter Visser — Software Engineer",
description: isEn
? "Personal website of Janpeter Visser. Allround software engineer with 30 years of experience in full-stack development, from C++ to Angular and .NET."
: "Persoonlijke website van Janpeter Visser. Allround software engineer met 30 jaar ervaring in full-stack development, van C++ tot Angular en .NET.",
metadataBase: new URL("https://jp-visser.nl"),
openGraph: {
title: "Janpeter Visser — Software Engineer",
description: isEn
? "Allround software engineer with 30 years of experience in full-stack development."
: "Allround software engineer met 30 jaar ervaring in full-stack development.",
url: `https://jp-visser.nl/${lang}`,
siteName: "Janpeter Visser",
locale: isEn ? "en_GB" : "nl_NL",
type: "website",
},
};
}
export default function LangLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

39
app/[lang]/page.tsx Normal file
View file

@ -0,0 +1,39 @@
import { redirect } from "next/navigation";
import type { Lang } from "@/lib/cv-data";
import { Nav } from "@/components/nav";
import { Hero } from "@/components/hero";
import { MotivationSection } from "@/components/motivation";
import { ExperienceSection } from "@/components/experience";
import { SkillsSection } from "@/components/skills";
import { AppsSection } from "@/components/apps";
import { ContactSection } from "@/components/contact";
import { Footer } from "@/components/footer";
const VALID_LANGS: Lang[] = ["nl", "en"];
export default async function LangPage({
params,
}: {
params: Promise<{ lang: string }>;
}) {
const { lang } = await params;
if (!VALID_LANGS.includes(lang as Lang)) {
redirect("/nl");
}
const currentLang = lang as Lang;
return (
<>
<Nav lang={currentLang} />
<Hero lang={currentLang} />
<MotivationSection lang={currentLang} />
<ExperienceSection lang={currentLang} />
<SkillsSection lang={currentLang} />
<AppsSection lang={currentLang} />
<ContactSection lang={currentLang} />
<Footer />
</>
);
}

View file

@ -1,5 +1,6 @@
import type { Metadata } from "next";
import { DM_Sans } from "next/font/google";
import { headers } from "next/headers";
import "./globals.css";
const dmSans = DM_Sans({
@ -9,28 +10,19 @@ const dmSans = DM_Sans({
});
export const metadata: Metadata = {
title: "Janpeter Visser — Software Engineer",
description:
"Persoonlijke website van Janpeter Visser. Allround software engineer met 30 jaar ervaring in full-stack development, van C++ tot Angular en .NET.",
metadataBase: new URL("https://jp-visser.nl"),
openGraph: {
title: "Janpeter Visser — Software Engineer",
description:
"Allround software engineer met 30 jaar ervaring in full-stack development.",
url: "https://jp-visser.nl",
siteName: "Janpeter Visser",
locale: "nl_NL",
type: "website",
},
};
export default function RootLayout({
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const headersList = await headers();
const lang = headersList.get("x-lang") ?? "nl";
return (
<html lang="nl" className={dmSans.variable}>
<html lang={lang} className={dmSans.variable}>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link

View file

@ -1,23 +1,5 @@
import { Nav } from "@/components/nav";
import { Hero } from "@/components/hero";
import { MotivationSection } from "@/components/motivation";
import { ExperienceSection } from "@/components/experience";
import { SkillsSection } from "@/components/skills";
import { AppsSection } from "@/components/apps";
import { ContactSection } from "@/components/contact";
import { Footer } from "@/components/footer";
import { redirect } from "next/navigation";
export default function Home() {
return (
<>
<Nav />
<Hero />
<MotivationSection />
<ExperienceSection />
<SkillsSection />
<AppsSection />
<ContactSection />
<Footer />
</>
);
export default function RootPage() {
redirect("/nl");
}