Next.js Internationalization

Add multiple languages to Next.js with locale routing, translation dictionaries, and automatic locale detection.

TL;DR

  1. Put routes under app/[lang] so every URL carries its locale.
  2. Load translations from JSON dictionaries with a getDictionary() helper.
  3. Redirect visitors to their preferred locale from proxy.ts.

Locale Routing

    app/[lang]

    Nest every route under a dynamic segment so the URL includes the locale.

    app/
      [lang]/
        layout.tsx
        page.tsx          // /en, /es
        about/page.tsx    // /en/about
    params.lang

    Read the current locale in a page by awaiting the route params.

    export default async function Page(
      props: PageProps<'/[lang]'>
    ) {
      const { lang } = await props.params;
      return <p>Current locale: {lang}</p>;
    }
    generateStaticParams()

    List the supported locales so each one is prebuilt as a static page.

    export async function generateStaticParams() {
      return [{ lang: 'en' }, { lang: 'es' }];
    }
    html lang

    Set the document language from the route so browsers and crawlers can read it.

    export default async function Layout(
      props: LayoutProps<'/[lang]'>
    ) {
      const { lang } = await props.params;
      return (
        <html lang={lang}>
          <body>{props.children}</body>
        </html>
      );
    }

Translations

    Dictionary Files

    Keep the text for each language in its own JSON file.

    // dictionaries/es.json
    {
      "nav": { "home": "Inicio" },
      "cart": { "add": "Agregar al carrito" }
    }
    getDictionary()

    Load only the dictionary for the requested locale, on the server.

    import 'server-only';
    
    const dictionaries = {
      en: () =>
        import('./en.json').then((m) => m.default),
      es: () =>
        import('./es.json').then((m) => m.default),
    };
    
    export const getDictionary = (lang: 'en' | 'es') =>
      dictionaries[lang]();
    Use a Translation

    Fetch the dictionary in a Server Component and read keys from it.

    const dict = await getDictionary(lang);
    
    return <button>{dict.cart.add}</button>;

Locale Detection

    locales

    Define the supported locales and a default in one shared place.

    export const locales = ['en', 'es'];
    export const defaultLocale = 'en';
    Accept-Language

    Pick the visitor's preferred language from the request header.

    function getLocale(request: NextRequest) {
      const h = request.headers.get('accept-language');
      const preferred = h?.split(',')[0].slice(0, 2);
      return locales.includes(preferred ?? '')
        ? preferred
        : defaultLocale;
    }
    proxy.ts

    Redirect requests without a locale prefix to the detected locale.

    export function proxy(request: NextRequest) {
      const { pathname } = request.nextUrl;
      const hasLocale = locales.some((l) =>
        pathname.startsWith(`/${l}`)
      );
      if (hasLocale) return;
    
      const locale = getLocale(request);
      request.nextUrl.pathname = `/${locale}${pathname}`;
      return NextResponse.redirect(request.nextUrl);
    }

Formatting and Switching

    Intl.DateTimeFormat

    Format a date for the current locale without any extra library.

    const date = new Intl.DateTimeFormat(lang, {
      dateStyle: 'long',
    }).format(new Date());
    Intl.NumberFormat

    Format prices and numbers with the right separators and currency symbol.

    const price = new Intl.NumberFormat(lang, {
      style: 'currency',
      currency: 'EUR',
    }).format(19.99);
    Language Switcher

    Link to the same page in another language by swapping the locale prefix.

    import Link from 'next/link';
    
    export function Switcher({ path }: { path: string }) {
      return (
        <nav>
          <Link href={`/en${path}`}>English</Link>
          <Link href={`/es${path}`}>Español</Link>
        </nav>
      );
    }
    alternates.languages

    Tell search engines which URL serves each language version of a page.

    export const metadata: Metadata = {
      alternates: {
        languages: {
          en: '/en',
          es: '/es',
        },
      },
    };

Tips

  1. Prebuild every language with generateStaticParams(), so each locale is served as a fast static page.
  2. Set the lang attribute on <html> from the route param, so screen readers and search engines know the language.

Warnings

  1. Importing every dictionary into a Client Component ships all languages to the browser, so load them on the server with getDictionary().
  2. A locale redirect in proxy.ts must skip paths that already have a locale, or it will redirect in a loop.

In Practice

FAQ