Next.js 16 doesn't give you a built-in i18n router like the old Pages Router did. You have to build locale routing yourself using directory structures, middleware, and metadata APIs. Here is how to construct a reliable setup for locale segments, hreflang headers, and metadata generation that works cleanly in production.
Setting Up Dynamic Locale Segments in App Router
In Next.js 16 and React 19, internationalization relies on the app/[lang] directory structure. Every route sits under the [lang] dynamic segment. If a user hits /en/products or /fr/products, Next.js passes lang into route params for layouts and pages.
Where developers often run into problems is root path handling. When someone requests /, you want your middleware to detect their preferred browser language using headers like Accept-Language and redirect them to /en or /fr.
Here is a middleware implementation using Negotiator and @formatjs/intl-localematcher:
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';
const locales = ['en', 'fr', 'es', 'de'];
const defaultLocale = 'en';
function getLocale(request: NextRequest): string {
const headers = { 'accept-language': request.headers.get('accept-language') || '' };
const languages = new Negotiator({ headers }).languages();
return match(languages, locales, defaultLocale);
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Skip internal asset paths and API routes
if (
pathname.startsWith('/_next') ||
pathname.startsWith('/api') ||
pathname.includes('.')
) {
return NextResponse.next();
}
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (pathnameHasLocale) return NextResponse.next();
const locale = getLocale(request);
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}
export const config = {
matcher: ['/((?!_next|api|favicon.ico).*)'],
};Checking pathnameHasLocale before running negotiator functions is critical. Running header negotiation on every static asset request wastes server CPU cycles and adds about 8ms of unnecessary latency per request.
Generating Translated Metadata and Hreflang Tags
Google cares deeply about hreflang attributes when indexing multi-language sites. If you don't explicitly declare alternate language URLs in your HTML head, search engine bots will treat translated pages as duplicate content. That destroys your search ranking across localized domains.
Next.js 16 provides the generateMetadata function inside layout or page files. Instead of manually writing <link rel="alternate"> elements, you return an alternates configuration object from generateMetadata. Next.js automatically outputs HTML tags with matching canonical and language references.
Here is how you set this up in app/[lang]/layout.tsx:
import type { Metadata } from 'next';
const locales = ['en', 'fr', 'es', 'de'];
const domain = 'https://example.com';
type Props = {
params: Promise<{ lang: string }>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { lang } = await params;
// Build language alternates mapping
const languageAlternates: Record<string, string> = {};
for (const l of locales) {
languageAlternates[l] = `${domain}/${l}`;
}
// Set default fallback tag x-default
languageAlternates['x-default'] = `${domain}/en`;
return {
title: {
default: lang === 'fr' ? 'Accueil - Tech Verse' : 'Home - Tech Verse',
template: '%s | Tech Verse',
},
alternates: {
canonical: `${domain}/${lang}`,
languages: languageAlternates,
},
};
}
export default async function RootLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ lang: string }>;
}) {
const { lang } = await params;
return (
<html lang={lang}>
<body>{children}</body>
</html>
);
}In Next.js 16 and React 19, params in layouts and pages is a Promise, so you must await params before reading properties. Skipping this step causes runtime errors during static site generation builds.
Pay attention to the x-default entry in languageAlternates. Skipping x-default causes Google Search Console to raise warnings because it can't determine where to send users whose language isn't explicitly targeted.
Syncing API Requests with Laravel 12 Backend
If your Next.js frontend connects to a PHP 8.3 API built with Laravel 12, your backend needs to know which locale to render or fetch from database models. Passing the lang parameter in every single backend endpoint URL gets messy. Instead, send the route locale inside an Accept-Language header in your server fetch client.
Here is a server-side fetch wrapper in Next.js that passes the target locale to Laravel:
import { cache } from 'react';
export const fetchFromLaravel = cache(async (endpoint: string, lang: string) => {
const baseUrl = process.env.LARAVEL_API_URL || 'https://api.example.com';
const res = await fetch(`${baseUrl}/api/v1/${endpoint}`, {
headers: {
'Accept-Language': lang,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
next: { revalidate: 3600 },
});
if (!res.ok) {
throw new Error(`Laravel API return error: ${res.statusText}`);
}
return res.json();
});On the Laravel 12 side, add HTTP middleware to assign the application locale globally based on that incoming header:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Symfony\Component\HttpFoundation\Response;
class SetApiLocale
{
public function handle(Request $request, Closure $next): Response
{
$locale = $request->header('Accept-Language', config('app.locale'));
$supportedLocales = ['en', 'fr', 'es', 'de'];
if (in_array($locale, $supportedLocales, true)) {
App::setLocale($locale);
} else {
App::setLocale(config('app.fallback_locale', 'en'));
}
return $next($request);
}
}This keeps Laravel translation strings, validation messages, and packages like spatie/laravel-translatable synchronized with Next.js without polluting API endpoints with duplicate query parameters.
Gotchas: Caching, Hardcoded Links, and Static Exports
Dynamic Rendering Triggers in Middleware
Reading headers or cookies inside middleware without strict route matchers turns off automatic static optimization for downstream routes. If you want fast response times on static pages, return NextResponse.next() immediately for already-localized URLs.
Hardcoded Link Paths
Using raw standard link components like <Link href="/about"> strips away active locale context. When a user browsing /fr/ clicks <Link href="/about">, Next.js sends them back to /about, triggering another round of middleware redirects. Build a wrapper component that appends the current locale param to internal paths.
Static Site Generation Export Compatibility
If you run Next.js with output: 'export', middleware logic is completely ignored during output generation. In SSG mode, define generateStaticParams() inside combination layouts to generate localized path pages during build time:
export async function generateStaticParams() {
return [
{ lang: 'en' },
{ lang: 'fr' },
{ lang: 'es' },
{ lang: 'de' }
];
}Without generateStaticParams(), Next.js renders static pages only for default fallbacks, yielding 404 responses for secondary locale paths in production exports.











