Next.js i18n met App Router
Server Components, ISR en edge-geoptimaliseerde vertalingen voor Next.js-applicaties.
middlewaremiddleware.tsgetRequestConfigi18n/request.tsgetMessages()app/[locale]/page.tsxuseTranslations()components/Hero.tsxSetup
Set up in 4 steps
Installatie
Voeg @better-i18n/next en next-intl toe aan je project.
npm install @better-i18n/next next-intlVoeg middleware toe voor locale-detectie
De middleware leest de Accept-Language-header en het URL-voorvoegsel om de locale van de gebruiker te detecteren en dienovereenkomstig door te sturen.
import { createBetterI18nMiddleware } from '@better-i18n/next';
export default createBetterI18nMiddleware({
project: 'your-org/your-project',
defaultLocale: 'en',
localePrefix: 'always',
});
export const config = { matcher: ['/((?!api|_next).*)'] };Laad berichten in een Server Component
Gebruik getMessages() in je root layout om vertalingen server-side op te halen en door te geven aan BetterI18nProvider.
// app/[locale]/layout.tsx
import { BetterI18nProvider } from '@better-i18n/next/client';
import { getMessages } from '@better-i18n/next/server';
const config = { project: 'your-org/your-project', defaultLocale: 'en' };
export default async function RootLayout({ children, params }) {
const { locale } = await params;
const messages = await getMessages(config, locale);
return (
<html lang={locale}>
<body>
<BetterI18nProvider locale={locale} messages={messages} config={config}>
{children}
</BetterI18nProvider>
</body>
</html>
);
}Gebruik vertalingen in Client Components
Roep useTranslations() aan in elke Client Component. Berichten zijn al gehydrateerd vanaf de server — geen extra fetch nodig.
'use client';
import { useTranslations } from 'next-intl';
export function HeroSection() {
const t = useTranslations('home');
return <h1>{t('title')}</h1>;
}Routing
Edge Runtime en detectie van lokale instellingen
Voer locale-detectie en het laden van berichten uit aan de rand voor responstijden van minder dan 50 ms wereldwijd.
Middleware-configuratie
Voeg locale-detectie en routering toe aan je Next.js-app met één middleware-bestand.
// middleware.ts — locale detection
import { createBetterI18nMiddleware } from '@better-i18n/next'
export default createBetterI18nMiddleware({
project: 'your-org/your-project',
defaultLocale: 'en',
localePrefix: 'always',
})
export const config = { matcher: ['/((?!api|_next).*)'] }// middleware.ts — Edge-based locale detection
import { NextRequest, NextResponse } from 'next/server';
const SUPPORTED_LOCALES = ['en', 'de', 'fr', 'ja', 'es'] as const;
const DEFAULT_LOCALE = 'en';
function getPreferredLocale(request: NextRequest): string {
// 1. Check URL prefix
const pathname = request.nextUrl.pathname;
const urlLocale = SUPPORTED_LOCALES.find(
(l) => pathname.startsWith(`/${l}/`) || pathname === `/${l}`
);
if (urlLocale) return urlLocale;
// 2. Check cookie
const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value;
if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale as any)) {
return cookieLocale;
}
// 3. Parse Accept-Language header
const acceptLang = request.headers.get('accept-language') ?? '';
const preferred = acceptLang
.split(',')
.map((part) => part.split(';')[0].trim().substring(0, 2))
.find((code) => SUPPORTED_LOCALES.includes(code as any));
return preferred ?? DEFAULT_LOCALE;
}
export function middleware(request: NextRequest) {
const locale = getPreferredLocale(request);
const { pathname } = request.nextUrl;
const hasLocale = SUPPORTED_LOCALES.some(
(l) => pathname.startsWith(`/${l}/`) || pathname === `/${l}`
);
if (!hasLocale) {
return NextResponse.redirect(
new URL(`/${locale}${pathname}`, request.url)
);
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};Edge-compatibel berichten laden
Cache vertalingen op de edge met een lichtgewicht in-memory TTL-cache voor directe reacties.
// lib/edge-messages.ts — Edge-compatible message loading
const messageCache = new Map<string, { data: Record<string, string>; ts: number }>();
const TTL = 60_000; // 1 minute cache at edge
export async function getEdgeMessages(
locale: string,
namespace: string
): Promise<Record<string, string>> {
const cacheKey = `${locale}:${namespace}`;
const cached = messageCache.get(cacheKey);
if (cached && Date.now() - cached.ts < TTL) {
return cached.data;
}
const response = await fetch(
`https://cdn.better-i18n.com/your-org/your-project/${locale}/${namespace}.json`,
{ next: { revalidate: 60 } }
);
const data = await response.json();
messageCache.set(cacheKey, { data, ts: Date.now() });
return data;
}Edge API-route met i18n
Retourneer vertaalde API-reacties vanuit edge-functies met een minimale cold start.
// app/api/translate/route.ts — Edge API route with i18n
import { getEdgeMessages } from '@/lib/edge-messages';
export const runtime = 'edge';
export async function GET(request: Request) {
const url = new URL(request.url);
const locale = url.searchParams.get('locale') ?? 'en';
const key = url.searchParams.get('key') ?? '';
const messages = await getEdgeMessages(locale, 'api-responses');
const translated = messages[key] ?? key;
return Response.json({ text: translated, locale });
}Rendering
ISR & Internationalisering
Combineer incrementele statische regeneratie met i18n voor snelle, altijd actuele meertalige pagina's.
revalidate = 3600app/[locale]/layout.tsx~60 minrevalidate = 1800app/[locale]/[slug]/page.tsx~30 minrevalidatePath()app/api/revalidate/route.ts< 1 minSnelle start
Voeg i18n toe aan je Next.js-app met slechts een paar regels code.
// app/[locale]/page.tsx
import { getTranslations } from 'next-intl/server';
export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'home' });
return (
<main>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
</main>
);
}// app/[locale]/layout.tsx — ISR with i18n
import { getMessages } from '@better-i18n/next/server';
import { BetterI18nProvider } from '@better-i18n/next/client';
export const revalidate = 3600; // Revalidate every hour
const config = { project: 'your-org/your-project', defaultLocale: 'en' };
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const messages = await getMessages(config, locale);
return (
<BetterI18nProvider locale={locale} messages={messages} config={config}>
{children}
</BetterI18nProvider>
);
}ISR met generateStaticParams
Pre-render pagina's voor elke locale tijdens de build, en vernieuw ze vervolgens periodiek met ISR.
// app/[locale]/[slug]/page.tsx — Generate static pages per locale
import { getMessages } from '@better-i18n/next/server';
const config = { project: 'your-org/your-project', defaultLocale: 'en' };
export async function generateStaticParams() {
const locales = ['en', 'de', 'fr', 'ja'];
const slugs = await fetchAllSlugs();
return locales.flatMap((locale) =>
slugs.map((slug) => ({ locale, slug }))
);
}
export const revalidate = 1800; // ISR: refresh every 30 min
export default async function Page({
params,
}: {
params: Promise<{ locale: string; slug: string }>;
}) {
const { locale, slug } = await params;
const messages = await getMessages(config, locale, { namespaces: ['blog'] });
return <article><h1>{messages.blog[slug + '.title']}</h1></article>;
}On-demand revalidatie
Activeer ISR-revalidatie wanneer vertalingen worden bijgewerkt — haak in op de publish-webhook van Better I18N.
// app/api/i18n/revalidate/route.ts — On-demand ISR for translation updates
import { createRevalidateHandler } from '@better-i18n/next/revalidate';
// Called by the Better i18n publish webhook — verifies the HMAC signature,
// then revalidates the paths/tags below.
export const POST = createRevalidateHandler({
secret: process.env.BETTER_I18N_WEBHOOK_SECRET!,
revalidatePaths: ['/'],
revalidateTags: ['i18n-messages'],
});Advanced
Geavanceerde patronen
Geneste lay-outs, parallelle routes en serveracties met typeveilige vertalingen.
// app/[locale]/dashboard/layout.tsx — Nested layout with namespace
import { getMessages } from '@better-i18n/next/server';
import { BetterI18nProvider } from '@better-i18n/next/client';
import { DashboardNav } from '@/components/DashboardNav';
const config = { project: 'your-org/your-project', defaultLocale: 'en' };
export default async function DashboardLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
// Load the dashboard-specific namespace alongside common messages
const messages = await getMessages(config, locale, {
namespaces: ['common', 'dashboard'],
});
return (
<BetterI18nProvider locale={locale} messages={messages} config={config}>
<DashboardNav />
<main>{children}</main>
</BetterI18nProvider>
);
}Parallelle routes met i18n
Laad vertalingen onafhankelijk in parallelle route-slots voor modulaire, locale-bewuste layouts.
// app/[locale]/@analytics/page.tsx — Parallel route with i18n
import { getTranslations } from 'next-intl/server';
export default async function AnalyticsSlot({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'analytics' });
return (
<section aria-label={t('title')}>
<h2>{t('title')}</h2>
<p>{t('description')}</p>
</section>
);
}
// app/[locale]/layout.tsx — Consuming parallel routes
export default function Layout({
children,
analytics,
notifications,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
notifications: React.ReactNode;
}) {
return (
<div>
<main>{children}</main>
<aside>{analytics}</aside>
<aside>{notifications}</aside>
</div>
);
}Server Actions met vertaling
Retourneer vertaalde validatiefouten en succesberichten vanuit server actions.
// app/[locale]/contact/actions.ts — Server action with i18n
'use server';
import { getTranslations } from 'next-intl/server';
import { headers } from 'next/headers';
export async function submitContactForm(formData: FormData) {
const headersList = await headers();
// Set by createBetterI18nMiddleware — see the routing section above
const locale = headersList.get('x-locale') ?? 'en';
const t = await getTranslations({ locale, namespace: 'contact' });
const email = formData.get('email') as string;
const message = formData.get('message') as string;
if (!email || !message) {
return { error: t('validation.required') };
}
try {
await sendEmail({ email, message, locale });
return { success: t('form.success') };
} catch {
return { error: t('form.error') };
}
}
// app/[locale]/contact/page.tsx — Using the server action
'use client';
import { useTranslations } from 'next-intl';
import { submitContactForm } from './actions';
export default function ContactPage() {
const t = useTranslations('contact');
return (
<form action={submitContactForm}>
<label>{t('form.email')}</label>
<input name="email" type="email" required />
<label>{t('form.message')}</label>
<textarea name="message" required />
<button type="submit">{t('form.submit')}</button>
</form>
);
}Problemen met i18n oplossen
Los problemen op met betrekking tot hydratatie, ontbrekende locale fallbacks en verschillen in datum-/getalnotatie.
// Fix: Hydration mismatch with date/number formatting
// Problem: Server renders "1,000" but client renders "1.000"
// Solution: BetterI18nProvider already passes an explicit timeZone down to
// NextIntlClientProvider, so server and client share the same formatting locale.
// app/[locale]/layout.tsx
import { getFormatter } from 'next-intl/server';
export default async function Layout({ children, params }: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
// Pre-format on server with the explicit locale
const format = await getFormatter({ locale });
return (
<html lang={locale} suppressHydrationWarning>
<body>{children}</body>
</html>
);
}
// components/Price.tsx — Client component
'use client';
import { useFormatter } from 'next-intl';
export function Price({ amount }: { amount: number }) {
const format = useFormatter();
// useFormatter automatically uses the locale/timeZone from BetterI18nProvider
// ensuring server and client render the same output
return <span>{format.number(amount, { style: 'currency', currency: 'USD' })}</span>;
}Locale-fallbackketen
Definieer fallbackketens zodat regionale varianten zoals pt-BR terugvallen op pt en vervolgens op en.
// lib/i18n-config.ts — Locale fallback chain
const FALLBACK_CHAIN: Record<string, string[]> = {
'pt-BR': ['pt', 'en'],
'zh-TW': ['zh-CN', 'en'],
'en-GB': ['en'],
'de-AT': ['de', 'en'],
};
export function resolveMessages(
locale: string,
allMessages: Record<string, Record<string, string>>
): Record<string, string> {
const chain = FALLBACK_CHAIN[locale] ?? ['en'];
const primary = allMessages[locale] ?? {};
// Merge fallback messages (primary overrides fallbacks)
return chain.reduceRight(
(merged, fallbackLocale) => ({
...merged,
...(allMessages[fallbackLocale] ?? {}),
}),
primary
);
}Consistente datumopmaak
Voorkom server/client-datumverschillen door timeZone expliciet in te stellen op UTC.
// components/LocalizedDate.tsx — Consistent date formatting
'use client';
import { useFormatter, useLocale } from 'next-intl';
export function LocalizedDate({ date }: { date: Date | string }) {
const format = useFormatter();
const locale = useLocale();
const dateObj = typeof date === 'string' ? new Date(date) : date;
return (
<time dateTime={dateObj.toISOString()}>
{format.dateTime(dateObj, {
year: 'numeric',
month: 'long',
day: 'numeric',
// Explicitly set timeZone to avoid server/client mismatch
timeZone: 'UTC',
})}
</time>
);
}Capabilities
Functies
Werkt met populaire Next.js i18n-bibliotheken
Better I18N vervangt je favoriete i18n-bibliotheek niet — het is de vertaalmanagementlaag die ze nog krachtiger maakt.
Better I18N + next-intl
De populairste Next.js i18n-bibliotheek met App Router-ondersteuning, type-veilige berichten en ICU-syntax.Better I18N synchroniseert vertalingen direct naar next-intl JSON-formaat. Beheer in ons dashboard, deploy direct via CDN.
Better I18N + next-i18next
Beproefde i18n-bibliotheek voor Next.js gebaseerd op i18next. Ideaal voor Pages Router en migratie naar App Router.Exporteer naar i18next-compatibel JSON met namespaces. Better I18N beheert de vertaalworkflow, next-i18next de runtime.
Better I18N + Lingui
Lichtgewicht, macro-gebaseerde i18n-bibliotheek met uitstekende DX en automatische berichtextractie.Extraheer berichten met Lingui CLI, beheer vertalingen in Better I18N, synchroniseer automatisch via GitHub-integratie.
Related Articles
en vs en-US: How to Name Locale Files Without Breaking Your i18n Pipeline
You are setting up i18n. You create the folder. Then you stop, because you have to name the first file and there are two obvious answers: locales/en.json...
Read More →Introducing the Better i18n CLI: Manage Translations from Your Terminal
Until now, managing translation keys on Better i18n meant using the dashboard UI or connecting an MCP server to your AI assistant. Both work great — but...
Read More →How to Add i18n to Shopify Hydrogen (Complete Guide)
Shopify Hydrogen is the modern way to build custom storefronts — but adding internationalization (i18n) can be challenging. You need locale-aware routing,...
Read More →Gerelateerd
Bekijk andere framework-handleidingen
Begin met bouwen met Next.js i18n
Gratis pakket beschikbaar. Geen creditcard vereist.