barebones

Supporting multiple languages (aka Internationalization)

Next.js supports multiple languages through content localization (i18n) and internationalized routing. While there are several ways to implement this, we'll explore how to set it up using the sub-path approach, in which we prepend the language to every route (e.g., /en/products).

  1. Install the dependencies:

    pnpm add negotiator @formatjs/intl-localematcher
  2. Install the development-only dependencies:

    pnpm add -D @types/negotiator
  3. Create a config.ts as the source of truth for you supported locales

    i18n/config.ts

    export const i18n = {
      defaultLocale: "en",
      locales: ["en", "da"],
    } as const;
    
    export type Locale = (typeof i18n.locales)[number];
  4. Now we can create a function to check the if the incoming request has a supported locale in the URL

    i18n/verify-locale.ts

    import "server-only";
    import type { NextRequest } from "next/server";
    import { match } from "@formatjs/intl-localematcher";
    import Negotiator from "negotiator";
    import { i18n } from "./config.ts";
    const getLocale = (request: NextRequest) => {
      const languages = new Negotiator({
        headers: {
          "accept-language": request.headers.get("accept-language") ?? "",
        },
      }).languages();
      return match(languages, i18n.locales, i18n.defaultLocale);
    };
    const isLocaleSupported = (input: string) => {
      if (input.length !== 2) return false;
      const isNotSupported = i18n.locales.every((locale) => locale !== input);
      return !isNotSupported;
    };
    export const verifyLocale = (request: NextRequest) => {
      const { pathname } = request.nextUrl;
      const [firstSegment, ...segments] = pathname.split("/").toSpliced(0, 1);
      return isLocaleSupported(firstSegment)
        ? { needsRedirect: false, redirectPath: "" }
        : {
            needsRedirect: true,
            redirectPath: `/${getLocale(request)}/${firstSegment}/${segments.join("/")}`,
          };
    };
  5. You will then use the function inside the middleware.ts file to check every URL for the locale:

    /*[caption:{root}/middleware.ts] [showLineNumbers]*/
      import { NextRequest, NextResponse } from "next/server";
    
      import { verifyLocale } from "./middleware/check-locale";
    
      export const middleware = async (request: NextRequest) => {
        const { needsRedirect, redirectPath } = verifyLocale(request);
    
        if (needsRedirect) {
          request.nextUrl.pathname = redirectPath;
          return NextResponse.redirect(request.nextUrl);
        }
      };
    
      export const config = {
        matcher: [
          "/((?!api|_next).*)",
          // Optional: only run on root (/) URL
          // "/",
        ],
      };
  6. And to make sure you can easily grab the locale, move all the routes from app/ to app/[lang].

  7. For the localization, you need to create an English dictionary:

    {
      "products": {
        "cart": "Add to Cart"
      }
    }
  8. And mirror it across the other supported languages (Danish in this case):

    {
      "products": {
        "cart": "Tilføj til kurv"
      }
    }
  9. Finally, create a function to load the translations for the requested locale:

    i18n/get-dictionary.ts

    import "server-only";
     import type { Locale } from "./config.ts";
    
     type Dictionary = { landing: { hello: string } };
    
     const dictionaries: { [key in Locale]: () => Promise<Dictionary> } = {
       en: () =>
         import("@/i18n/dictionaries/en.json").then((module) => module.default),
       da: () =>
         import("@/i18n/dictionaries/da.json").then((module) => module.default),
     };
    
     export const getDictionary = async (locale: Locale) =>
       dictionaries[locale]() ?? dictionaries.en();
  10. Starting now, you can use the getDictionary function (preferably inside server components):

import { getDictionary, type Locale } from "@/dictionaries/get-dictionary";

export default async function Home({
  params,
}: {
  params: Promise<{
    lang: Locale;
  }>;
}) {
  const locale = (await params).lang;
  const { marketing } = await getDictionary(locale);

  return (
    <main>
      <h1>{marketing.title}</h1>
      ...
    </main>
  );
}