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.
Configuration is centralized in next.config.js. Disabling native localeDetection is mandatory for pipeline-controlled routing, as it delegates negotiation to Edge Middleware for deterministic fallback chains and CI validation.
/** @type {import('next').NextConfig} */
const nextConfig = {
i18n: {
locales: ['en', 'de', 'ja'],
defaultLocale: 'en',
localeDetection: false, // Pipeline-managed via middleware
},
};
export default nextConfig;
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, enforcing standardized locale negotiation patterns that align with enterprise l10n pipelines and automated route generation.
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/[lang]/layout.tsx
import { notFound } from 'next/navigation';
const SUPPORTED = ['en', 'de', 'ja'];
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)) 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();
let locale = request.cookies.get(COOKIE_NAME)?.value || DEFAULT;
const acceptLang = request.headers.get('accept-language')?.split(',')[0]?.split('-')[0];
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. TMS webhook triggers can be routed through API endpoints to invalidate route caches (revalidatePath) when translation payloads update. For granular control over App Router edge functions and route rewriting rules, the Next.js App Router i18n Middleware Configuration resource provides extended implementation patterns.
Dynamic Hreflang & Cache Invalidation
// middleware.ts (extended)
const hreflangLinks = SUPPORTED.map((l) => {
const path = pathname.replace(/^\/[a-z]{2}/, '');
return `<${request.nextUrl.origin}/${l}${path}>; 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; 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. Lighthouse audits track localized Core Web Vitals, while Webpack/Rspack bundle analyzers confirm translation dictionaries are route-split and lazy-loaded.
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 and 404 fallback behavior. |
| Missing middleware matcher | Unnecessary edge execution, increased latency/costs | 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 middleware fallback logic to ensure graceful degradation when locale cookies are missing or invalid. |
| 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 to confirm translation payloads are correctly split and not bloating initial load. |