09-nextjsTermsLevel_10Internationalization (i18n)

Internationalization (i18n)

Level 10 — Advanced Architecture The design patterns and routing structures used to configure a web application to support multiple languages and localizations, typically by capturing the locale from the URL segment.


1. Prerequisites


2. Term Category

Routing & Layouts (Internationalization Routing Architecture): Internationalization (i18n) routes locale sub-paths (/en/docs, /fr/docs) using App Router route groups and middleware.


3. Explanation

Environment Context

  • Universal (Middleware checks request headers on the server to redirect the client browser to the localized path).

(1) Design Motivation — "Why did we design this?"

If your web application has users worldwide, you need to offer translations in multiple languages (e.g. English, Spanish, French). Serving the same content to all visitors is poor user experience and hurts international SEO ranking.

Internationalization (i18n) is the architectural pattern of structuring your application to support dynamic locale configurations.

In the older Pages Router, Next.js had a built-in i18n routing config option. In the App Router, Next.js does not have built-in i18n routing configurations; instead, it provides standard routing primitives (Dynamic Routes and Middleware) so you can build flexible locale routing yourself.


(2) Sub-path Routing Pattern

The standard i18n practice is to wrap your entire router structure under a dynamic folder parameter representing the language locale: app/[lang]/.

app/
├── [lang]/
│   ├── layout.tsx
│   ├── page.tsx
│   └── about/
│       └── page.tsx
└── middleware.ts

If a visitor requests /en/about, Next.js maps lang to 'en'. If they request /es/about, it maps to 'es'. Both routes resolve to the exact same page files, but with different parameters.


(3) Translation Dictionaries

To render the correct translated text, you write simple dictionary loader functions that fetch localized static JSON files based on the active lang parameter:

// app/[lang]/dictionaries.ts
const dictionaries: Record<string, () => Promise<any>> = {
  en: () => import('./locales/en.json').then((module) => module.default),
  es: () => import('./locales/es.json').then((module) => module.default),
};

export const getDictionary = async (locale: string) => {
  // Fallback to English if the requested locale is not found
  const loader = dictionaries[locale] || dictionaries.en;
  return loader();
};

You can then load this dictionary directly inside an asynchronous Server Component:

// app/[lang]/page.tsx
import React from 'react';
import { getDictionary } from './dictionaries';

interface PageProps {
  params: { lang: string };
}

export default async function HomePage({ params }: PageProps) {
  const dict = await getDictionary(params.lang);

  return (
    <main>
      <h1>{dict.welcome_title}</h1>
      <p>{dict.welcome_subtitle}</p>
    </main>
  );
}

4. Common Mistakes & Pitfalls

Mistake 1: Passing the entire translation dictionary to Client Components

The mistake: Fetching the translation dictionary in a Server Component and passing it down wholesale as props to a "use client" Component:

// app/[lang]/page.tsx
// BAD: Sends the entire raw JSON text over the network boundary!
const dict = await getDictionary(params.lang);
return <ClientHero dictionary={dict} />;

Why it's wrong: Translation files can be extremely large. Passing the entire dictionary to Client Components forces the browser to download massive JSON structures, increasing network bundle sizes and slowing page hydration.

Golden Rule: Only pass the specific, individual text strings that the Client Component actually needs to render its state, or use a client-side lightweight context manager.


Mistake 2: Hardcoding UI Text Strings Directly in Component JSX Templates

The mistake: Writing <h1>Welcome to our store</h1> in JSX templates.

Why it's wrong: Hardcoded text strings prevent internationalized localization (i18n). Use translation keys (t('welcome')) with dictionary dictionaries.

Incorrect:

<h1>Welcome to our store</h1> <!--Hardcoded English text! -->

Fix:

<h1>{t('welcome')}</h1> <!-- Dynamic translation key lookup -->

Mistake 3: Forgetting Locale Sub-Path Rewrites in Middleware for Internationalization

The mistake: Building i18n routing without handling locale prefix redirection (/en/about vs /fr/about) in middleware.ts.

Why it's wrong: Without middleware locale detection, visiting root /about fails to resolve the user's preferred browser language. Use middleware for locale detection.

Incorrect:

/* Missing middleware locale detector for root URL routes */

Fix:

/* Use middleware.ts to detect Accept-Language headers and redirect to /en/about or /fr/about */

5. Practice Exercises

Exercise 1: Structuring Locale Route Directories app/[lang]/

Scenario: Create an internationalized directory structure app/[lang]/page.tsx supporting /en/about and /fr/about.

Requirements:

  1. Define dynamic [lang] folder segment under app/.
Answer

Implementation

// app/[lang]/page.tsx
export default async function I18nPage({
  params
}: {
  params: Promise<{ lang: string }>;
}) {
  const { lang } = await params;

  return (
    <main className="p-6">
      <h1>Active Locale: {lang}</h1>
    </main>
  );
}

Technical Explanation

  1. Dynamic route parameter [lang] encapsulates the locale string in the URL path.
  2. Allows rendering translated content on the server based on params.lang.
  3. Standard App Router internationalization directory design.

Exercise 2: Negotiating Locales in Middleware

Scenario: Create middleware.ts to detect user Accept-Language headers and redirect un-prefixed URLs (/about) to default locale (/en/about).

Requirements:

  1. Redirect un-prefixed paths to /en/path.
Answer

Implementation

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

const locales = ["en", "fr", "es"];
const defaultLocale = "en";

export function middleware(req: NextRequest) {
  const pathname = req.nextUrl.pathname;
  const pathnameHasLocale = locales.some(
    (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );

  if (!pathnameHasLocale) {
    return NextResponse.redirect(
      new URL(`/${defaultLocale}${pathname}`, req.url)
    );
  }
}

Technical Explanation

  1. Middleware inspects incoming URL paths before route resolution.
  2. Redirects un-prefixed paths to locale-prefixed URLs (/en/about).
  3. Centralized locale redirection guard.

Exercise 3: Generating Static Parameters for Supported Locales

Scenario: Prerender supported locales (en, fr, es) at build time using generateStaticParams().

Requirements:

  1. Export generateStaticParams() returning array of { lang: string }.
Answer

Implementation

// app/[lang]/layout.tsx
export async function generateStaticParams() {
  return [{ lang: "en" }, { lang: "fr" }, { lang: "es" }];
}

export default function I18nLayout({
  children,
  params
}: {
  children: React.ReactNode;
  params: { lang: string };
}) {
  return (
    <html lang={params.lang}>
      <body>{children}</body>
    </html>
  );
}

Technical Explanation

  1. generateStaticParams() pre-generates static HTML layouts for all supported locale parameters at build time.
  2. Delivers fast localized page loads from CDN edge networks.
  3. High performance internationalization pattern.


7. Key Takeaways

  • i18n is the practice of supporting multiple languages and regions.
  • Next.js App Router relies on Dynamic Routes ([lang]) to handle locale sub-paths.
  • Use Middleware to read the Accept-Language headers and redirect visitors to their preferred locales.
  • Keep translation files stored as static JSON modules loaded dynamically using import().
  • Do not serialize entire translation dictionaries across the network boundary to Client Components.
Built with LogoFlowershow