Next.js Internationalization

Implement i18n in Next.js with routing, translations, locale detection, and multi-language support.

TL;DR

  1. 01Use locale-prefixed routing like /en, /es, /fr in the app directory.
  2. 02Organize translation files by locale and reference them in components.
  3. 03Detect locale from URL, user preference, or browser language.

Tips

  1. 01Use next-intl library for complex i18n needs like pluralization, date formatting, and locale-aware routing, as it handles many edge cases automatically.
  2. 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

  1. 01Always set the HTML lang attribute correctly for each locale, since screen readers and search engines rely on it for language detection.
  2. 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 — params is an async Promise that 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-intl for 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 useTranslations hook 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.ts to 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 lang on 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