Next.js i18n Routing Setup

This configuration establishes the foundational routing layer within the localization pipeline, translating static locale definitions into dynamic, framework-native route structures. It bridges initial Next.js initialization with downstream CI/CD validation, translation extraction, and edge-level request routing, ensuring deterministic locale propagation across SSR, SSG, and ISR workflows.

1. Routing Architecture & Ecosystem Context

Next.js supports two primary routing strategies: subpath (/en/about) and domain-based (en.example.com). Subpath routing is the default for enterprise pipelines due to simplified DNS management, unified CDN caching, and straightforward TMS webhook routing. Domain routing requires separate Vercel project deployments or advanced reverse-proxy configurations, increasing pipeline overhead.

Pages Router configuration is centralized in next.config.js. Disabling native localeDetection is recommended for pipeline-controlled routing, as it delegates negotiation to Edge Middleware for deterministic fallback chains.

/** @type {import('next').NextConfig} */
const nextConfig = {
  i18n: {
    locales: ['en', 'de', 'ja'],
    defaultLocale: 'en',
    localeDetection: false, // Pipeline-managed via middleware
  },
};

export default nextConfig;

App Router does not support the i18n key in next.config.js. With the App Router, locale routing is handled entirely through dynamic [lang] segments and middleware.ts. See the Next.js App Router i18n Middleware Configuration resource for the App Router implementation.

Directory scaffolding dictates component resolution. The App Router requires app/[lang]/ for dynamic locale segments, while the legacy Pages Router uses pages/[lang]/. This structural baseline integrates directly into the broader Frontend Framework i18n & Component Routing ecosystem.

2. App Router vs Pages Router Implementation

The App Router leverages React Server Components (RSC) to resolve locale context at the edge before hydration, eliminating client-side routing flicker. Pages Router relies on getStaticProps/getServerSideProps context injection, which increases bundle size and hydration latency. For component-level message injection, the architectural approach mirrors the Vue I18n Composition API Guide, particularly around reactive translation loading and scoped message namespaces.

Server Component Locale Resolution (App Router)

// app/[lang]/layout.tsx
import { notFound } from 'next/navigation';

const SUPPORTED = ['en', 'de', 'ja'] as const;
type Locale = (typeof SUPPORTED)[number];

export function generateStaticParams() {
  return SUPPORTED.map((lang) => ({ lang }));
}

export default function RootLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: { lang: string };
}) {
  if (!SUPPORTED.includes(params.lang as Locale)) notFound();

  return (
    <html lang={params.lang}>
      <body>{children}</body>
    </html>
  );
}

Client Component Context Wrapper

// components/LocaleProvider.tsx
'use client';
import { createContext, useContext } from 'react';

const LocaleContext = createContext<string>('en');

export function LocaleProvider({
  locale,
  children,
}: {
  locale: string;
  children: React.ReactNode;
}) {
  return <LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>;
}

export const useLocale = () => useContext(LocaleContext);

3. Edge Middleware & Request Interception

middleware.ts intercepts incoming requests before route resolution. It parses Accept-Language headers, validates against the supported locale allowlist, persists locale preference via cookies, and applies NextResponse.rewrite() to serve the correct route without client-side redirects. This routing negotiation strategy aligns with the SvelteKit Internationalization Basics approach to server-side locale resolution, adapted for Vercel Edge runtime constraints and sub-50ms execution limits.

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

const SUPPORTED = ['en', 'de', 'ja'];
const DEFAULT = 'en';
const COOKIE_NAME = 'NEXT_LOCALE';

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const localeInPath = SUPPORTED.find((l) => pathname.startsWith(`/${l}/`) || pathname === `/${l}`);

  if (localeInPath) return NextResponse.next();

  const cookieLocale = request.cookies.get(COOKIE_NAME)?.value;
  const acceptLang = request.headers.get('accept-language')?.split(',')[0]?.split('-')[0];
  let locale = DEFAULT;
  if (cookieLocale && SUPPORTED.includes(cookieLocale)) locale = cookieLocale;
  else if (acceptLang && SUPPORTED.includes(acceptLang)) locale = acceptLang;

  const url = request.nextUrl.clone();
  url.pathname = `/${locale}${pathname}`;

  const response = NextResponse.rewrite(url);
  response.cookies.set(COOKIE_NAME, locale, { maxAge: 31536000, path: '/' });
  return response;
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico|.*\\..*).*)'],
};

4. Advanced Middleware & Pipeline Integration

Production middleware must handle SEO metadata injection, TMS synchronization, and CI validation gates. Injecting hreflang headers at the edge prevents duplicate content penalties and ensures search engines index localized routes correctly.

Dynamic Hreflang & Cache Invalidation

// middleware.ts (extended — add inside middleware function after resolving locale)
const pathname = request.nextUrl.pathname.replace(/^\/[a-z]{2}/, '') || '/';
const hreflangLinks = SUPPORTED.map((l) =>
  `<${request.nextUrl.origin}/${l}${pathname}>; rel="alternate"; hreflang="${l}"`
).join(', ');

response.headers.set('Link', hreflangLinks);
response.headers.set('Vary', 'Accept-Language, Cookie');

CI/CD Validation Gate

#!/bin/bash
# ci/validate-locales.sh
SUPPORTED=("en" "de" "ja")
for locale in "${SUPPORTED[@]}"; do
  if ! grep -q "\"$locale\"" next.config.js 2>/dev/null && \
     ! grep -q "'$locale'" middleware.ts 2>/dev/null; then
    echo "FAIL: Locale $locale missing from config"
    exit 1
  fi
done
echo "PASS: Locale configuration validated"

5. Audit Workflow & Performance Validation

Route integrity must be verified pre-deployment. Automated Playwright/Cypress suites simulate locale switching, validate translation fallback behavior, and assert route parameter consistency.

Playwright Multi-Locale Routing Test

// tests/i18n-routing.spec.ts
import { test, expect } from '@playwright/test';

const LOCALES = ['en', 'de', 'ja'];

test.describe('Locale Routing Integrity', () => {
  for (const locale of LOCALES) {
    test(`validates ${locale} route hydration`, async ({ page }) => {
      await page.goto(`/${locale}/about`);
      await expect(page).toHaveURL(new RegExp(`^/${locale}/about`));
      await expect(page.locator('html')).toHaveAttribute('lang', locale);
    });
  }
});

Bundle Chunk Validation

// scripts/analyze-bundles.js
const { readdirSync, statSync } = require('fs');
const path = require('path');

const chunksDir = path.join(process.cwd(), '.next/static/chunks');
const localeFiles = readdirSync(chunksDir).filter((f) => f.includes('-locale-'));

localeFiles.forEach((file) => {
  const size = statSync(path.join(chunksDir, file)).size / 1024;
  if (size > 50) console.warn(`WARN: Locale chunk ${file} exceeds 50KB (${size.toFixed(1)}KB)`);
});

Engineering Pitfalls & Audit Checklist

Risk Vector Impact Mitigation & Audit Step
Hardcoded locale prefixes in navigation Cross-locale link breakage, 404 spikes Replace static paths with dynamic route parameters (/${locale}/path). Audit: Run automated link crawlers across all supported locales to verify route parameter consistency.
Missing middleware matcher Unnecessary edge execution, increased latency Exclude static assets, API routes, and Next.js internals from matcher. Audit: Profile middleware execution logs to confirm static asset bypass.
Absent hreflang injection SEO cannibalization, duplicate content penalties Inject Link headers or <link> tags dynamically per route. Audit: Validate hreflang values against Google’s Search Console for indexing errors.
Full dictionary hydration Increased TTFB, bloated initial load Implement route-level code splitting and lazy-load translation payloads. Audit: Profile bundle size per locale route with @next/bundle-analyzer.