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:
parent
61e603b5d7
commit
d352a7d496
13 changed files with 540 additions and 177 deletions
39
app/[lang]/layout.tsx
Normal file
39
app/[lang]/layout.tsx
Normal 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
39
app/[lang]/page.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
24
app/page.tsx
24
app/page.tsx
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue