Internationalization (i18n) in Next.js
Internationalization (i18n) enables your application to support multiple languages and locales. In the Next.js App Router, i18n is implemented using localized routing (putting the locale as a dynamic route segment, e.g., /en/dashboard vs. /es/dashboard) and intercepting requests via Middleware.
1. Directory Structure for Localized Routing
To support dynamic locale routes, you wrap your root application pages inside a dynamic folder segment named [lang].
my-app/├── dictionaries/│ ├── en.json│ └── es.json├── src/│ ├── middleware.ts│ └── app/│ └── [lang]/│ ├── layout.tsx│ ├── page.tsx│ └── about/│ └── page.tsxBy nesting your pages under [lang], Next.js routes all paths (e.g., /en, /es/about) directly to the layout and pages within that segment, supplying the locale as a route param.
2. Locale Detection with Middleware
Next.js Middleware intercepts requests to decide which locale to redirect the user to if no locale is present in the URL (e.g., redirecting /about to /en/about).
We can use standard web headers (Accept-Language) and packages like @formatjs/intl-localematcher and negotiator to detect the user’s preferred language.
A. Installation
Install the language negotiation utilities:
npm install @formatjs/intl-localematcher negotiatornpm install --save-dev @types/negotiatorB. Middleware Configuration
Create a src/middleware.ts file to match locales and perform redirects:
import { NextResponse } from "next/server";import type { NextRequest } from "next/server";import { match } from "@formatjs/intl-localematcher";import Negotiator from "negotiator";
const locales = ["en", "es", "fr"];const defaultLocale = "en";
function getLocale(request: NextRequest): string { const negotiatorHeaders: Record<string, string> = {}; request.headers.forEach((value, key) => { negotiatorHeaders[key] = value; });
const languages = new Negotiator({ headers: negotiatorHeaders }).languages();
try { return match(languages, locales, defaultLocale); } catch (error) { return defaultLocale; }}
export function middleware(request: NextRequest) { const { pathname } = request.nextUrl;
// Check if pathname already contains a supported locale const pathnameHasLocale = locales.some( (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}` );
if (pathnameHasLocale) return;
// No locale present -> Redirect const locale = getLocale(request); request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(request.nextUrl);}
export const config = { // Matcher ignoring API routes, _next static files, and assets matcher: ["/((?!api|_next/static|_next/image|favicon.ico|.*\\.png$).*)"],};3. Creating and Loading Dictionaries
Dictionaries are JSON files that map keys to translations. You load the appropriate dictionary server-side based on the current [lang] param.
A. Define Translations
Create your JSON dictionaries in a root dictionaries/ folder:
{ "welcome": "Welcome to our application!", "description": "This page is internationalized."}{ "welcome": "¡Bienvenido a nuestra aplicación!", "description": "Esta página está internacionalizada."}B. Set Up the Dictionary Loader
Create a helper utility to lazy-load the appropriate JSON file:
import "server-only";
const dictionaries: Record<string, () => Promise<any>> = { en: () => import("../../../dictionaries/en.json").then((module) => module.default), es: () => import("../../../dictionaries/es.json").then((module) => module.default),};
export const getDictionary = async (locale: string) => { const loadDictionary = dictionaries[locale] || dictionaries.en; return loadDictionary();};[!NOTE] We import
"server-only"to ensure this dictionary loader only runs on the server, avoiding sending large JSON translation bundles to the client browser.
4. Applying Locales in Layouts and Pages
Within the [lang] folder, every layout and page receives the active lang segment inside the params prop.
A. The Localized Root Layout
Use the lang parameter to set the HTML document language attribute dynamically:
export default function RootLayout({ children, params,}: { children: React.ReactNode; params: { lang: string };}) { return ( <html lang={params.lang}> <body>{children}</body> </html> );}B. Rendering Localized Content in Pages
Fetch the dictionary server-side inside your page component:
import { getDictionary } from "./dictionaries";
interface PageProps { params: { lang: string };}
export default async function IndexPage({ params }: PageProps) { const dict = await getDictionary(params.lang);
return ( <main className="p-8 max-w-xl mx-auto text-center"> <h1 className="text-3xl font-bold">{dict.welcome}</h1> <p className="mt-4 text-gray-600">{dict.description}</p> </main> );}5. Changing Languages (Language Switcher)
To allow users to switch languages, you can create a Client Component that parses the current pathname, swaps the locale segment, and pushes the new path to the router.
"use client";
import { usePathname, useRouter } from "next/navigation";
export default function LanguageSwitcher() { const pathname = usePathname(); const router = useRouter();
const handleLanguageChange = (newLang: string) => { if (!pathname) return;
// Split path: e.g. "/en/about" -> ["", "en", "about"] const segments = pathname.split("/"); segments[1] = newLang; // Replace the locale
const newPath = segments.join("/"); router.push(newPath); };
return ( <select onChange={(e) => handleLanguageChange(e.target.value)} className="p-2 border rounded bg-white text-gray-800" > <option value="en">English</option> <option value="es">Español</option> </select> );}6. Optimization: Static Translation Pre-rendering
By default, routes with dynamic parameters like [lang] are rendered dynamically at request time. If you know your supported languages in advance, you can compile them to static HTML pages at build time using generateStaticParams.
import { getDictionary } from "./dictionaries";
export async function generateStaticParams() { return [{ lang: "en" }, { lang: "es" }];}
export default async function Page({ params }: { params: { lang: string } }) { const dict = await getDictionary(params.lang); return <h1>{dict.welcome}</h1>;}This ensures Next.js performs Static Site Generation (SSG) for all language variants of the page, leading to instant load speeds.
7. Internationalization Best Practices
- Leverage SEO Metadata: Always include alternative language references (
hreflangtags) inside your page metadata to help search engines index different regional versions of your site. - Handle Dynamic Content: Keep structured layouts in your code and only place translateable text nodes in JSON dictionaries. Avoid placing structural HTML nodes inside translation files.
- Persist User Choice: If a user manually changes their language, set a cookie (e.g.,
NEXT_LOCALE=es). In your Middleware, check for this cookie first before checking theAccept-Languageheader.