Lesson 40 of 5026 min read

Internationalization in Next.js for Multi-Language Apps

Learn how to build multi-language Next.js applications, covering i18n routing patterns, setting up next-intl, and building a language switcher.

Author: CodersNexus

Internationalization in Next.js for Multi-Language Apps

Reaching a global audience means presenting content in more than one language, and Internationalization (commonly abbreviated i18n — 'i', then 18 letters, then 'n') is the practice of structuring an application to support this. Rather than building an entirely separate application per language, i18n typically means structuring URLs by locale (like /en/about and /fr/about), extracting all user-facing text into translation files, and providing a clean way for visitors to switch between supported languages.

This lesson covers implementing i18n in a Next.js App Router application using next-intl, one of the most popular libraries for this purpose, covering locale-based routing, organizing and using translation strings, and building a functional language switcher — while also touching on how this connects back to the middleware and rewrite concepts from Lesson 6.1.

Learning Objectives

  • Explain the core concepts of internationalization: locales, translation files, and locale-based routing.
  • Set up next-intl in a Next.js App Router project.
  • Structure routes to include a locale segment, such as /en/... or /fr/....
  • Organize and use translation strings within Server and Client Components.
  • Build a language switcher that navigates between locale-prefixed versions of the current page.

Core Definitions

  • Internationalization (i18n): The practice of designing and building an application so it can support multiple languages and regional formats.
  • Locale: An identifier representing a specific language and, often, region — such as 'en' (English), 'fr' (French), or 'en-GB' (British English).
  • Locale-based routing: A routing pattern where the URL itself includes a locale segment, like /en/about or /fr/about, determining which language's content to display.
  • Translation file: A file (commonly JSON) mapping translation keys to their actual translated text for a specific locale, such as { "welcome": "Welcome" } for English versus { "welcome": "Bienvenue" } for French.
  • next-intl: A popular internationalization library for Next.js, providing locale routing helpers, translation management, and formatting utilities for dates, numbers, and more.

Detailed Explanation

The foundation of i18n in the Next.js App Router is locale-based routing: rather than a single app/about/page.tsx serving all visitors regardless of language, the routing structure includes a dynamic locale segment — app/[locale]/about/page.tsx — so /en/about and /fr/about are genuinely distinct URLs, each capable of resolving to different, appropriately translated content, while still sharing the exact same underlying page component and logic.

Setting up next-intl involves a middleware.ts file (connecting directly back to Lesson 6.1) that detects a visitor's preferred locale — from the URL itself, a cookie, or the browser's Accept-Language header — and handles routing them to the correct locale-prefixed version of a page, including redirecting a visitor who lands on a locale-less URL to their detected, most appropriate locale's version. This is paired with a request configuration file establishing which locales your application supports and loading the appropriate translation file for the currently active locale.

Translation files are typically organized as JSON, one file per locale, with a consistent set of keys across all of them: messages/en.json might contain `{ "HomePage": { "title": "Welcome", "description": "..." } }`, while messages/fr.json contains the same key structure with French translations, `{ "HomePage": { "title": "Bienvenue", "description": "..." } }`. Inside a Server Component, next-intl's `getTranslations()` function retrieves the currently active locale's translations, and calling `t('HomePage.title')` returns the correctly translated string for whichever locale the current request is serving — the same component code runs for every locale, with only the actual translated content differing based on which locale's translation file was loaded.

For Client Components needing reactive access to translations (or to display a language switcher), next-intl provides a corresponding useTranslations hook, following the same general pattern established by other client-vs-server hook pairs throughout this course (like the auth() function versus useSession() from Lesson 5.4).

A language switcher, letting visitors manually change their active locale, is typically built using the Link component (from Lesson 1.8) with next-intl's locale-aware routing helpers, constructing a URL for the SAME current page but with a different locale prefix — clicking a 'Français' link while viewing /en/about navigates to /fr/about, the same logical page, now serving French content, demonstrating how locale-based routing and standard Next.js navigation patterns work together seamlessly once properly configured.

How a Request Resolves to Locale-Specific Content

{"heading":"How a Request Resolves to Locale-Specific Content","description":"Visualize the locale detection and routing flow:\n\n[Visitor requests '/'] --> [middleware.ts detects preferred locale (URL, cookie, or Accept-Language header)]\n --> [Redirects to the detected locale's version, e.g., '/fr/']\n\n[Request reaches app/[locale]/page.tsx with locale='fr']\n --> [getTranslations() loads messages/fr.json for this request]\n --> [t('HomePage.title') returns 'Bienvenue' instead of 'Welcome']\n\n[User clicks language switcher: 'English'] --> [Navigates to '/en' — SAME page, now serving messages/en.json]"}

Next.js Practical Example

// messages/en.json
{
  "HomePage": {
    "title": "Welcome",
    "description": "This is our homepage."
  }
}

// messages/fr.json
{
  "HomePage": {
    "title": "Bienvenue",
    "description": "Ceci est notre page d'accueil."
  }
}

// app/[locale]/page.tsx — a Server Component using translations
import { getTranslations } from 'next-intl/server';

export default async function HomePage() {
  const t = await getTranslations('HomePage');
  return (
    <div>
      <h1>{t('title')}</h1>
      <p>{t('description')}</p>
    </div>
  );
}

// components/LanguageSwitcher.tsx — navigating between locale versions of the current page
'use client';
import { Link, usePathname } from '@/i18n/navigation'; // next-intl-aware Link/usePathname

export default function LanguageSwitcher() {
  const pathname = usePathname();
  return (
    <div>
      <Link href={pathname} locale="en">English</Link>
      <Link href={pathname} locale="fr">Français</Link>
    </div>
  );
}

Both en.json and fr.json share the exact same key structure (HomePage.title, HomePage.description), only the actual text differing per language — this consistency is what lets HomePage's component code remain completely unchanged across every supported locale. HomePage calls getTranslations('HomePage') to retrieve the currently active locale's translations for this specific request, then uses t('title') and t('description') to render the correctly translated strings — whether a French or English visitor requests this page, the exact same component runs, with only the loaded translation file differing. LanguageSwitcher uses next-intl's locale-aware Link component, passing the current pathname alongside an explicit target locale, letting a visitor switch from /en/about to /fr/about (or whichever page they're currently on) while remaining on that same logical page, just in a different language.

How Global Products Handle Internationalization

  • E-commerce platforms operating across many countries use locale-based routing extensively, not just for language but often for region-specific pricing, currency, and available products tied to each locale.
  • SaaS products expanding internationally typically start with a small set of core supported locales (e.g., English, Spanish, French, German) and use a translation management workflow to keep translation files updated as new features are added.
  • News and media sites serving multiple countries use i18n routing to serve genuinely distinct, region-appropriate homepages and article sets per locale, rather than simply translating a single global homepage's content.
  • Documentation sites for popular open-source projects and developer tools commonly support multiple languages, with community-contributed translation files maintained alongside the primary English documentation.
  • Government and public-sector websites, often legally required to support multiple official languages, rely heavily on structured, consistent locale-based routing and translation file organization for compliance and accessibility.

Common Mistakes to Avoid

  • Hardcoding user-facing text directly in components instead of extracting it into translation files from the start, making later internationalization much more labor-intensive.
  • Allowing translation files across different locales to drift out of sync with inconsistent keys, causing missing translations or runtime errors for certain locales.
  • Forgetting to configure middleware for locale detection and redirection, leaving visitors without an automatic, sensible default locale experience.
  • Confusing getTranslations() (server-side) with useTranslations() (client-side) and using the wrong one for a given component type.
  • Not considering that some languages have significantly longer translated text than English, potentially breaking layouts designed only with English text lengths in mind.

Interview Notes

  • Internationalization (i18n) typically uses locale-based routing, with a dynamic locale segment making each language's page a distinct, routable URL.
  • Middleware detects a visitor's preferred locale and handles routing/redirection to the correct locale-prefixed version of a page.
  • Translation files (commonly JSON, one per locale) share a consistent key structure, letting the same component code run unchanged across all supported languages.
  • getTranslations() is used server-side in Server Components; useTranslations() is the corresponding client-side hook for Client Components.
  • A language switcher navigates to the same current page with a different locale, typically using a locale-aware Link component.

Key Takeaways

  • Locale-based routing, making each language a genuinely distinct URL, is the structural foundation of i18n in the Next.js App Router.
  • Organizing translation files with consistent keys across locales is what allows a single set of components to serve every supported language.
  • next-intl's server/client function pairing (getTranslations/useTranslations) mirrors patterns established elsewhere in the course for handling server versus client contexts.
  • Middleware, first covered in Lesson 6.1, plays a direct, practical role in detecting and routing based on locale preference.

Summary

Internationalization in the Next.js App Router is built on locale-based routing, using a dynamic locale segment (like app/[locale]/about/page.tsx) to make each supported language's version of a page a genuinely distinct, routable URL. Using next-intl, middleware detects a visitor's preferred locale from the URL, a cookie, or the browser's Accept-Language header, routing them to the correct locale-prefixed version of a page. Translation strings are organized as JSON files, one per locale, sharing a consistent key structure so the exact same component code can run unchanged regardless of which language is active — retrieved server-side via getTranslations() in Server Components, or reactively via the useTranslations() hook in Client Components. A language switcher, built using next-intl's locale-aware Link component, lets visitors navigate to the exact same current page under a different locale, demonstrating how locale-based routing and Next.js's standard navigation patterns work together to deliver a complete, multi-language application experience.