Internationalisering (i18n) in Next.js 15 App Router: next-intl, Middleware, hreflang en RTL Support (2026)

Complete Nederlandse gids voor i18n in Next.js 15 App Router met next-intl 4.x: locale routing, middleware voor taaldetectie, hreflang metadata, RTL support voor Arabisch en Hebreeuws, plus een vergelijking met next-i18next en Paraglide.

Next.js 15 i18n: next-intl Setup 2026

Bijgewerkt: 5 september 2026

Internationalisering in Next.js 15 App Router regel je in 2026 het snelst met next-intl. Je plaatst je routes onder een [locale]-segment, laat een middleware de voorkeurstaal detecteren, en laadt vertaalberichten server-side in een NextIntlClientProvider. Deze aanpak levert statisch gegenereerde pagina's per taal, correcte hreflang-metadata en volledige RTL-ondersteuning, zonder dat je client-side JavaScript hoeft in te ruilen voor performance. In deze gids bouw ik een productieklare i18n-setup op vanaf een leeg project. Eerlijk: ik heb deze exacte flow drie keer live gezet in de afgelopen maanden, dus alles hieronder is opgeschreven vanuit de valkuilen (en niet vanuit de docs).

  • next-intl 4.x is de facto standaard voor de App Router. next-i18next ondersteunt alleen nog de Pages Router.
  • Alle routes verplaatsen naar app/[locale]/... en generateStaticParams gebruiken voor statische generatie per taal.
  • De middleware van next-intl detecteert de taal via Accept-Language, cookies of pad-prefix en doet automatische redirects.
  • Zet hreflang-tags in generateMetadata via alternates.languages om duplicate-content in Google te voorkomen.
  • Voor RTL-talen zoals Arabisch en Hebreeuws stel je <html dir="rtl"> in en gebruik je logische CSS-eigenschappen (margin-inline-start).
  • Paraglide.js is een lichter alternatief met tree-shakebare berichten, maar next-intl heeft in 2026 het rijkste ecosysteem.

Waarom next-intl in 2026?

De situatie rond i18n in Next.js is de laatste twee jaar drastisch veranderd. In de Pages Router had je de ingebouwde i18n-configuratie in next.config.js die routing zoals /en/about en /nl/about automatisch afhandelde. In de App Router bestaat die configuratie niet meer. Je moet het zelf inrichten met een dynamisch [locale]-segment plus middleware.

Van de bibliotheken die dat gat opvullen is next-intl in 2026 de duidelijke winnaar. Het is expliciet ontworpen voor React Server Components, ondersteunt streaming, en levert een middleware die drop-in werkt. De 4.x-release van juli 2026 voegde native ondersteuning toe voor generateStaticParams met async berichten en de nieuwe use cache-directive uit Next.js 15. Concurrent next-i18next heeft nog altijd geen App Router support, en wordt door de maintainer expliciet niet aanbevolen voor nieuwe projecten.

Paraglide.js van Inlang is een interessant alternatief. Het compileert vertalingen naar getypeerde JavaScript-functies die tree-shakebaar zijn. Het resultaat is kleiner dan JSON-based oplossingen, maar het ecosysteem rond formattering (relative time, pluralisatie via ICU) is minder rijk. Voor de meeste teams is next-intl in 2026 de veilige keuze.

Project setup: next-intl installeren

We beginnen met een verse Next.js 15 app. Ik ga uit van TypeScript en de App Router, en gebruik pnpm (vervang door npm of yarn naar smaak).

pnpm create next-app@latest my-i18n-app --typescript --app --tailwind --eslint
cd my-i18n-app
pnpm add next-intl@^4.0.0

Maak vervolgens een messages/-map aan de root van het project met een JSON-bestand per taal.

// messages/nl.json
{
  "HomePage": {
    "title": "Welkom op onze site",
    "description": "De snelste weg naar een meertalige Next.js applicatie.",
    "cta": "Aan de slag"
  },
  "Nav": {
    "home": "Home",
    "about": "Over ons",
    "contact": "Contact"
  }
}
// messages/en.json
{
  "HomePage": {
    "title": "Welcome to our site",
    "description": "The fastest path to a multilingual Next.js application.",
    "cta": "Get started"
  },
  "Nav": {
    "home": "Home",
    "about": "About",
    "contact": "Contact"
  }
}

Configureer next-intl door een i18n/routing.ts-bestand aan te maken. Dit is de centrale plek waar alle ondersteunde talen en de standaardtaal staan gedefinieerd.

// i18n/routing.ts
import { defineRouting } from 'next-intl/routing';
import { createNavigation } from 'next-intl/navigation';

export const routing = defineRouting({
  locales: ['nl', 'en', 'de', 'ar'],
  defaultLocale: 'nl',
  localePrefix: 'as-needed', // /over-ons in NL, /en/about in EN
});

export const { Link, redirect, usePathname, useRouter, getPathname } =
  createNavigation(routing);

De localePrefix: 'as-needed'-strategie betekent dat de standaardtaal geen prefix in de URL krijgt (schoner voor SEO), terwijl andere talen wel expliciet in het pad staan. Alternatieven zijn 'always' (verplicht voor alle talen) en 'never' (gebruik cookies alleen).

Locale routing met het [locale]-segment

Verplaats alle bestaande app/-routes naar app/[locale]/. De nieuwe root-layout moet de lang-attribute correct zetten en berichten leveren aan de NextIntlClientProvider.

// app/[locale]/layout.tsx
import { NextIntlClientProvider, hasLocale } from 'next-intl';
import { setRequestLocale } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
import type { ReactNode } from 'react';

export function generateStaticParams() {
  return routing.locales.map((locale) => ({ locale }));
}

type Props = {
  children: ReactNode;
  params: Promise<{ locale: string }>;
};

export default async function LocaleLayout({ children, params }: Props) {
  const { locale } = await params;

  if (!hasLocale(routing.locales, locale)) {
    notFound();
  }

  // Belangrijk: markeer dit request als statisch renderbaar
  setRequestLocale(locale);

  return (
    <html lang={locale} dir={locale === 'ar' ? 'rtl' : 'ltr'}>
      <body>
        <NextIntlClientProvider>{children}</NextIntlClientProvider>
      </body>
    </html>
  );
}

De generateStaticParams-functie zorgt dat elke taal een eigen statisch build-artifact krijgt. In combinatie met setRequestLocale weet next-intl welke berichten te laden zonder runtime overhead. Dit is essentieel voor SSG en ISR. Zonder setRequestLocale valt je pagina terug op dynamische rendering, en dat merk je pas als je build-output ineens veel kleiner is dan verwacht.

Maak vervolgens een i18n/request.ts die per request de juiste berichten inlaadt.

// i18n/request.ts
import { getRequestConfig } from 'next-intl/server';
import { hasLocale } from 'next-intl';
import { routing } from './routing';

export default getRequestConfig(async ({ requestLocale }) => {
  const requested = await requestLocale;
  const locale = hasLocale(routing.locales, requested)
    ? requested
    : routing.defaultLocale;

  return {
    locale,
    messages: (await import(`../messages/${locale}.json`)).default,
  };
});

Registreer dit configbestand in next.config.ts met de next-intl plugin.

// next.config.ts
import type { NextConfig } from 'next';
import createNextIntlPlugin from 'next-intl/plugin';

const withNextIntl = createNextIntlPlugin('./i18n/request.ts');

const nextConfig: NextConfig = {
  experimental: { typedRoutes: true },
};

export default withNextIntl(nextConfig);

Middleware voor taaldetectie

De middleware is het hart van je i18n-setup. Deze detecteert bij elk request de gewenste taal via drie signalen (in deze volgorde): een NEXT_LOCALE-cookie, de Accept-Language-header van de browser, of het pad zelf. Zonder middleware zou / nooit weten of hij de Nederlandse of Engelse variant moet tonen.

// middleware.ts
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';

export default createMiddleware(routing);

export const config = {
  // Match alles behalve api, _next, statische bestanden
  matcher: ['/((?!api|trpc|_next|_vercel|.*\..*).*)']
};

Deze middleware doet vier dingen automatisch: (1) detecteert de voorkeurstaal, (2) redirect / naar /nl of /en waar nodig, (3) rewrites interne paden zodat je in app/[locale]/ normale routes ziet, en (4) zet de NEXT_LOCALE-cookie zodat volgende bezoeken direct de juiste taal krijgen zonder header-parsing.

Wil je middleware combineren met authenticatie of rate limiting? Kijk dan naar de patronen in onze middleware gids voor Next.js App Router. Je kunt createMiddleware wrappen in je eigen functie en de response-chain doorzetten.

Vertalingen laden in Server Components

In Server Components gebruik je getTranslations. Deze functie is async omdat next-intl de berichten lazy laadt op basis van het request. Voor Client Components gebruik je de synchrone useTranslations-hook, die de berichten ophaalt uit de NextIntlClientProvider.

// app/[locale]/page.tsx (Server Component)
import { getTranslations, setRequestLocale } from 'next-intl/server';

type Props = { params: Promise<{ locale: string }> };

export default async function HomePage({ params }: Props) {
  const { locale } = await params;
  setRequestLocale(locale);

  const t = await getTranslations('HomePage');

  return (
    <main>
      <h1>{t('title')}</h1>
      <p>{t('description')}</p>
      <button>{t('cta')}</button>
    </main>
  );
}
// components/Navigation.tsx ('use client')
'use client';
import { useTranslations } from 'next-intl';
import { Link } from '@/i18n/routing';

export function Navigation() {
  const t = useTranslations('Nav');

  return (
    <nav>
      <Link href="/">{t('home')}</Link>
      <Link href="/about">{t('about')}</Link>
      <Link href="/contact">{t('contact')}</Link>
    </nav>
  );
}

Let op dat we Link importeren uit @/i18n/routing, niet uit next/link. De next-intl Link voegt automatisch de juiste locale-prefix toe. Als een gebruiker op de Engelse site is en op <Link href="/about"> klikt, wordt dit vertaald naar /en/about.

hreflang tags en SEO metadata

Google gebruikt hreflang-tags om te begrijpen welke pagina bij welke taal en regio hoort. Zonder deze tags kan Google je Nederlandse en Engelse pagina als duplicate content markeren, wat je ranking direct schaadt. Met de Metadata API in Next.js 15 zet je ze centraal in generateMetadata.

// app/[locale]/layout.tsx
import type { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import { routing } from '@/i18n/routing';

export async function generateMetadata({
  params,
}: {
  params: Promise<{ locale: string }>;
}): Promise<Metadata> {
  const { locale } = await params;
  const t = await getTranslations({ locale, namespace: 'Meta' });

  return {
    title: t('title'),
    description: t('description'),
    alternates: {
      canonical: `https://example.com/${locale}`,
      languages: Object.fromEntries(
        routing.locales.map((l) => [l, `https://example.com/${l}`])
      ),
    },
  };
}

De alternates.languages-map genereert automatisch <link rel="alternate" hreflang="nl" href="https://example.com/nl" />-tags in de <head>. Voeg ook een x-default toe voor de standaardtaal-fallback. Meer over Metadata patterns vind je in onze complete Metadata API gids.

Vergeet ook je sitemap.xml niet. Voor elke URL moet je één <url>-entry hebben met <xhtml:link>-referenties naar alle taalvarianten. Next.js 15 heeft hier native support voor via de alternates-property in MetadataRoute.Sitemap.

RTL support voor Arabisch en Hebreeuws

Right-to-left talen zoals Arabisch (ar), Hebreeuws (he) en Perzisch (fa) vereisen meer dan alleen vertaalde tekst. De hele lay-out moet spiegelen. Zet allereerst dir="rtl" op de <html>-tag, zoals we in de layout deden. De browser draait dan automatisch tekstrichting, scrollbars en flexbox-volgorde om.

// Utility functie voor RTL detectie
const RTL_LOCALES = ['ar', 'he', 'fa', 'ur'];

export function getDirection(locale: string): 'ltr' | 'rtl' {
  return RTL_LOCALES.includes(locale) ? 'rtl' : 'ltr';
}

Voor CSS mag je in 2026 geen hard-coded margin-left en padding-right meer gebruiken. Die breken in RTL. Vervang ze door logische eigenschappen.

/* Fout: breekt in RTL */
.card { margin-left: 1rem; padding-right: 0.5rem; }

/* Goed: werkt in beide richtingen */
.card { margin-inline-start: 1rem; padding-inline-end: 0.5rem; }

Tailwind CSS 4 ondersteunt logische eigenschappen native via de ms-*, me-*, ps-*, pe-*-utility klassen. Voor pixel-perfecte iconen in menu's (bijvoorbeeld een chevron die naar rechts wijst voor "verder") gebruik je [dir=rtl]:rotate-180.

Datums, getallen en pluralisatie formatteren

Vertalen is meer dan strings vervangen. Datums, valuta, aantallen en pluralisatie verschillen fundamenteel per locale. Nederlands schrijft "5.678,90 €", Engels "€5,678.90". next-intl gebruikt de standaard Intl API van de browser voor consistente formattering.

// Server Component met formattering
import { getFormatter, getTranslations } from 'next-intl/server';

export default async function OrderSummary({ order }: { order: Order }) {
  const format = await getFormatter();
  const t = await getTranslations('Order');

  return (
    <div>
      <p>{t('placed', { date: format.dateTime(order.date, 'long') })}</p>
      <p>{t('total', { amount: format.number(order.total, {
        style: 'currency', currency: 'EUR'
      })})}</p>
      <p>{t('items', { count: order.items.length })}</p>
    </div>
  );
}

Pluralisatie via ICU MessageFormat is ingebouwd. Definieer je bericht met plural-syntax en next-intl kiest automatisch de juiste vorm.

// messages/nl.json
{
  "Order": {
    "items": "{count, plural, =0 {Geen artikelen} =1 {# artikel} other {# artikelen}}"
  }
}

Voor complexere formattering (relatieve tijd zoals "3 uur geleden", lijstformatting zoals "appels, peren en bananen") heeft next-intl equivalenten van Intl.RelativeTimeFormat en Intl.ListFormat. Zie de next-intl documentatie voor de volledige API.

next-intl vs next-i18next vs Paraglide

Kiezen tussen bibliotheken is de moeilijkste beslissing bij een nieuwe i18n-setup. Hieronder een pragmatische vergelijking op de dimensies die er in productie toe doen.

Aspectnext-intl 4.xnext-i18next 15.xParaglide.js 2.x
App Router supportNative, first-classNee (alleen Pages Router)Ja
Server ComponentsVolledig ondersteundN.v.t.Ondersteund
Bundle grootte~15 KB gzip~25 KB gzip~2 KB + berichten
Type safetyOptioneel (TS augmentation)BeperktVolledig (compile-time)
ICU MessageFormatJaJaBeperkt
Middleware includedJaNeeJa
LeercurveLaagLaag (bekend)Middel
Beste voorNieuwe App Router projectenLegacy Pages RouterPerformance-kritieke sites

Voor 95% van de nieuwe projecten in 2026 is next-intl de juiste keuze. Kies Paraglide alleen als bundle-grootte kritiek is (bijvoorbeeld marketing-landingspagina's die op 3G moeten laden), en accepteer dan de kleinere community. Migreer uit next-i18next als je nog niet naar de App Router bent overgegaan, want het pad wordt met elke Next.js-release moeilijker.

Veelvoorkomende fouten en oplossingen

Zelfs met een goed opgezette configuratie loop je tegen een aantal terugkerende problemen aan. Hieronder de vier die ik het vaakst zie in code reviews (en meestal zelf één keer heb moeten oplossen voordat ze bekend voelden).

1. "setRequestLocale niet aangeroepen" waarschuwing

Als je een pagina hebt zonder setRequestLocale(locale), valt next-intl terug op dynamische rendering en verlies je alle SSG-voordelen. Voeg het toe aan élk async page/layout dat getTranslations gebruikt. Deze waarschuwing verschijnt alleen in development, dus check je build-log ook.

2. Middleware matcht statische bestanden

Standaard-configuratie ['/((?!api|_next|.*\..*).*)'] vergeet soms edge cases zoals /robots.txt of /sitemap.xml. Voeg deze expliciet uit. Anders krijgt Googlebot een redirect naar /nl/sitemap.xml en indexeert je site niet. (In mijn laatste project kostte dit twee weken zichtbaarheid.)

3. Vertaalsleutels missen bij runtime

Als je meerdere talen ondersteunt maar niet alle sleutels vertaalt, valt next-intl standaard terug op de sleutelnaam zelf (bv. "HomePage.title"). Dit is een silent failure. Zet onError in i18n/request.ts om deze errors naar Sentry te sturen tijdens development.

4. Hydration mismatch met dates/numbers

Als je server in UTC draait en de client in Europe/Amsterdam, kan een format.dateTime() op de server anders resulteren dan op de client. Los dit op door timeZone expliciet in getRequestConfig te zetten, of gebruik suppressHydrationWarning op elementen met tijdgevoelige inhoud.

Veelgestelde vragen

Ondersteunt de Next.js App Router native i18n zonder externe bibliotheek?

Nee. Sinds de App Router de i18n-configuratie uit next.config.js niet meer ondersteunt, moet je zelf routing bouwen via een [locale]-segment en middleware. Praktisch gezien betekent dit: gebruik next-intl of Paraglide. Een DIY-oplossing bouwen kost al snel meer dan een week.

Hoe stel je hreflang tags in Next.js in?

Via de Metadata API. Zet in generateMetadata een alternates.languages-object waarin elke ondersteunde locale-code mapt naar de bijbehorende URL. Next.js rendert deze automatisch als <link rel="alternate" hreflang="..." />-tags in de <head>. Vergeet x-default niet als fallback.

Wat is het verschil tussen next-intl en next-i18next?

next-intl is gebouwd voor React Server Components en de App Router, terwijl next-i18next een wrapper is om react-i18next die alleen met de Pages Router werkt. Voor alle nieuwe projecten in 2026 kies je next-intl. next-i18next wordt door de maintainer expliciet niet aanbevolen voor App Router migraties.

Werkt next-intl met statische export (output: 'export')?

Ja, mits je de as-needed of always localePrefix strategie gebruikt en generateStaticParams in je root layout hebt staan. De middleware werkt niet met statische export. Je moet dan taaldetectie client-side doen, of via je CDN (bv. Cloudflare Workers) implementeren.

Hoe combineer je next-intl middleware met Auth.js?

Exporteer één gecombineerde middleware. Roep binnen dezelfde functie eerst je auth-check aan, en delegeer daarna naar createMiddleware(routing) uit next-intl. Twee losse middleware-bestanden werken niet in Next.js, want het framework accepteert er maar één per project.

Editorial Team
Over de Auteur Editorial Team

Our team of expert writers and editors.