Skip to the solution
HardFrontend4 min read

How do you internationalize (i18n) a Next.js App Router app?

116 views

The problem

Question presented to candidate: "We need English, German and French versions of the site with locale-prefixed URLs. How do you set up i18n in the App Router?"

What a strong answer should cover:

  • The next.config i18n option is a Pages Router feature; in the App Router you build routing yourself with a [lang] dynamic segment.
  • Detect the preferred locale in Proxy from the Accept-Language header or a cookie, and redirect / to /en, /de and so on.
  • Load translation dictionaries in Server Components, so translations do not ship as client JavaScript.
  • Use generateStaticParams for the locales so each language is prerendered, and dynamicParams = false to reject unknown ones.
  • Next.js 16.3 added next/root-params to read the root lang param anywhere in Server Components (not in Client Components, Server Actions or Route Handlers); set html lang and alternates.languages for SEO.

Clarifying questions expected:

  • "Sub-paths or domains per locale?" β€” both work with Proxy.
  • "Is content translated or just the UI?" β€” decides where translations come from.

Code / implementation expected: Yes β€” a [lang] segment, a dictionary loader and the Proxy redirect.

  • nextjs
  • i18n
  • internationalization
  • routing

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

4 min read Β· 8 sections

Read the code

A locale segment, dictionaries and Proxy detection
// app/[lang]/dictionaries.ts
import "server-only";
const dictionaries = {
  en: () => import("./dictionaries/en.json").then((m) => m.default),
  de: () => import("./dictionaries/de.json").then((m) => m.default),
};
export const getDictionary = (lang: "en" | "de") => dictionaries[lang]();

// app/[lang]/page.tsx
import { getDictionary } from "./dictionaries";
export function generateStaticParams() { return [{ lang: "en" }, { lang: "de" }]; }
export const dynamicParams = false;
export default async function Home({ params }: { params: Promise<{ lang: "en" | "de" }> }) {
  const dict = await getDictionary((await params).lang);
  return <h1>{dict.home.title}</h1>;
}

// proxy.ts
import { NextRequest, NextResponse } from "next/server";
const locales = ["en", "de"];
export function proxy(req: NextRequest) {
  const { pathname } = req.nextUrl;
  if (locales.some((l) => pathname === `/${l}` || pathname.startsWith(`/${l}/`))) return;
  const preferred = req.headers.get("accept-language")?.slice(0, 2);
  const lang = locales.includes(preferred ?? "") ? preferred : "en";
  return NextResponse.redirect(new URL(`/${lang}${pathname}`, req.url));
}
export const config = { matcher: ["/((?!_next|api|favicon.ico).*)"] };

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 125 of 129 in Next.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Next.js questions
Up nextHard

How do you set security headers and a nonce-based Content Security Policy in Next.js?

Continue