Next.js Internationalization
Implement i18n in Next.js with routing, translations, locale detection, and multi-language support.
TL;DR
- 01Use locale-prefixed routing like /en, /es, /fr in the app directory.
- 02Organize translation files by locale and reference them in components.
- 03Detect locale from URL, user preference, or browser language.
Tips
- 01Use next-intl library for complex i18n needs like pluralization, date formatting, and locale-aware routing, as it handles many edge cases automatically.
- 02Use next-intl's <code>createNavigation</code> helpers — <code>Link</code>, <code>redirect</code>, and <code>usePathname</code> — instead of <code>next/link</code> directly so navigation always stays locale-aware.
Warnings
- 01Always set the HTML lang attribute correctly for each locale, since screen readers and search engines rely on it for language detection.
- 02Forgetting to include locale-prefixed paths in your middleware <code>matcher</code> config can cause static assets and API routes to get caught in a redirect loop.
Locale-Based Routing
- One dynamic segment can serve every language on your site — nest all routes inside a
[locale]folder to organize them by language.app/ [locale]/ layout.tsx page.tsx about/ page.tsx - This creates URLs like
/en,/es,/en/about,/es/about, etc. - Access the locale parameter in your layouts and pages —
paramsis an asyncPromisethat must be awaited.export default async function Layout({ children, params }: { children: React.ReactNode; params: Promise<{ locale: string }>; }) { const { locale } = await params; return <html lang={locale}>{children}</html>; } - Use middleware to redirect to the user's preferred locale.
- Generate static params for all supported locales.
Translation Files
- Hardcoded strings scattered across components are a rewrite waiting to happen — centralize them in JSON files, one per locale.
locales/ en.json es.json fr.json - Structure translations hierarchically for nested content.
{ "common": { "welcome": "Welcome", "goodbye": "Goodbye" }, "home": { "title": "Home Page", "description": "Welcome to our site" } } - Load translations based on the current locale in your component.
import translations from "@/locales/en.json"; const message = translations.common.welcome; - Use a translation library like
next-intlfor complex needs.
Using the next-intl Library
- Rolling your own locale detection, pluralization, and date formatting adds up fast — next-intl bundles all three in one package.
npm install next-intl - Create a configuration file for supported locales and messages.
import { getRequestConfig } from "next-intl/server"; export default getRequestConfig(async ({ requestLocale }) => { const locale = await requestLocale; return { locale, messages: (await import(`./messages/${locale}.json`)).default }; }); - Use the
useTranslationshook to access translations in components."use client"; import { useTranslations } from "next-intl"; export default function HomePage() { const t = useTranslations("home"); return <h1>{t("title")}</h1>; } - next-intl handles formatting, pluralization, and date localization.
Locale Detection
- A browser sends its preferred language on every request — read the Accept-Language header in
middleware.tsto redirect users to the right locale automatically.// middleware.ts import { NextRequest, NextResponse } from "next/server"; export function middleware(request: NextRequest) { const { pathname } = request.nextUrl; const acceptLang = request.headers.get("accept-language") ?? "en"; const locale = acceptLang.split(",")[0].split("-")[0]; const supported = ["en", "es", "fr"]; const chosen = supported.includes(locale) ? locale : "en"; if (!pathname.startsWith(`/${chosen}`)) { return NextResponse.redirect(new URL(`/${chosen}${pathname}`, request.url)); } return NextResponse.next(); } - Check a locale cookie first so returning users keep their saved preference.
const cookieLocale = request.cookies.get("locale")?.value; const chosen = (cookieLocale && supported.includes(cookieLocale)) ? cookieLocale : detectedLocale; - Allow users to manually select their language and store it in a cookie.
"use client"; import { useRouter } from "next/navigation"; export function LocaleSwitcher({ current }: { current: string }) { const router = useRouter(); const switchLocale = (locale: string) => { document.cookie = `locale=${locale};path=/`; router.push(`/${locale}`); }; return ( <select value={current} onChange={(e) => switchLocale(e.target.value)}> <option value="en">English</option> <option value="es">Español</option> <option value="fr">Français</option> </select> ); } - Always validate locale values against an allowlist to prevent open redirect attacks.
- Use
useLocale()from next-intl to read the active locale in client components.
SEO and Hreflang
- Screen readers and search engines both rely on one attribute to know what language they're reading — set
langon the HTML element per locale.<html lang={locale}> - Add hreflang links to alternate language versions for SEO.
export async function generateMetadata({ params }) { const alternates = { languages: { en: `https://example.com/en/page`, es: `https://example.com/es/page`, fr: `https://example.com/fr/page` } }; return { alternates }; } - Generate sitemaps for each locale to help search engines index them.
- Use canonical URLs to prevent duplicate content issues.
FAQ
Create a [locale] dynamic segment at the root of your app directory (e.g., app/[locale]/page.tsx) and configure supported locales in middleware.ts to redirect users and validate locale params. This gives you clean URLs like /en/about and /fr/about automatically.
Use next-intl when you need pluralization rules, number/date formatting, or ICU message syntax, since the built-in Next.js i18n only handles routing. next-intl also provides React hooks like useTranslations that make accessing nested keys and interpolating variables much cleaner.
By default, Next.js detects locale from the Accept-Language request header and redirects accordingly. You can override this in middleware.ts by reading cookies, session data, or custom headers before the redirect, giving you full control over the detection priority.
Keep one JSON file per locale per namespace under a messages or locales directory (e.g., messages/en.json, messages/es.json) and split by feature or page to avoid loading all strings upfront. With next-intl, pass the relevant namespace to unstable_setRequestLocale and useTranslations('namespace') to scope your keys.
Export a generateMetadata function in each page and return an alternates.languages object mapping each locale to its full URL (e.g., { en: 'https://example.com/en/page', es: 'https://example.com/es/page' }). Next.js renders these as tags in the
automatically.