Internationalization

Subpath Routes via Middleware + Dictionaries

Internationalization

App Router does not ship a built-in i18n config. The recommended pattern is middleware-based locale detection plus per-locale dictionaries loaded in the route.

5 min read Level 3/5 #nextjs#i18n#intl
What you'll learn
  • Detect the locale in `middleware.ts`
  • Redirect to `/[lang]/...`
  • Load a dictionary per route

Next 13’s i18n config did not survive the App Router migration. The official pattern now is to handle locales yourself with middleware and a [lang] dynamic segment.

Detect the Locale

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

const LOCALES = ['en', 'fr', 'es']
const DEFAULT = 'en'

function pickLocale(req: NextRequest) {
  const header = req.headers.get('accept-language') ?? ''
  for (const tag of header.split(',')) {
    const lang = tag.split(';')[0].split('-')[0].trim()
    if (LOCALES.includes(lang)) return lang
  }
  return DEFAULT
}

export function middleware(req: NextRequest) {
  const { pathname } = req.nextUrl
  if (LOCALES.some((l) => pathname.startsWith(`/${l}/`) || pathname === `/${l}`)) {
    return NextResponse.next()
  }
  const locale = pickLocale(req)
  return NextResponse.redirect(new URL(`/${locale}${pathname}`, req.url))
}

export const config = { matcher: ['/((?!_next|api|favicon).*)'] }

The Lang Segment

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

export default async function Page({
  params,
}: {
  params: Promise<{ lang: string }>
}) {
  const { lang } = await params
  const t = await getDictionary(lang)
  return <h1>{t.welcome}</h1>
}

Dictionaries

// app/[lang]/dictionaries.ts
import 'server-only'

const dictionaries = {
  en: () => import('./dictionaries/en.json').then((m) => m.default),
  fr: () => import('./dictionaries/fr.json').then((m) => m.default),
  es: () => import('./dictionaries/es.json').then((m) => m.default),
}

export const getDictionary = (lang: string) =>
  dictionaries[lang as keyof typeof dictionaries]?.() ?? dictionaries.en()

For a richer setup (plurals, dates, currencies), reach for next-intl — it builds on this same pattern and adds the formatters.

Testing — Vitest, Playwright, Jest →