Next.js‑i18n mit App Router
Server Components, ISR und edge‑optimierte Übersetzungen für Next.js‑Anwendungen.
Set up in 4 steps
Install
Add @better-i18n/use-intl, use-intl, and the Next.js adapter to your project.
npm install @better-i18n/use-intl use-intl
Add middleware for locale detection
The middleware reads the Accept-Language header and URL prefix to detect the user's locale and redirect accordingly.
import { betterI18nMiddleware } from '@better-i18n/next';
export const middleware = betterI18nMiddleware;
export const config = { matcher: ['/((?!api|_next).*)'] };Load messages in a Server Component
Use getMessages() in your root layout to fetch translations server-side and pass them to BetterI18nProvider.
// app/[locale]/layout.tsx
import { BetterI18nProvider } from '@better-i18n/use-intl';
import { getMessages } from '@better-i18n/use-intl/server';
export default async function RootLayout({ children, params }) {
const messages = await getMessages({
project: 'your-org/your-project',
locale: params.locale,
});
return (
<html lang={params.locale}>
<body>
<BetterI18nProvider messages={messages} locale={params.locale} project="your-org/your-project">
{children}
</BetterI18nProvider>
</body>
</html>
);
}Use translations in Client Components
Call useTranslations() in any Client Component. Messages are already hydrated from the server — no extra fetch.
'use client';
import { useTranslations } from '@better-i18n/use-intl';
export function HeroSection() {
const t = useTranslations('home');
return <h1>{t('title')}</h1>;
}Features
Middleware Setup
Add locale detection and routing to your Next.js app with a single middleware file.
// middleware.ts — locale detection
import { betterI18nMiddleware } from '@better-i18n/next'
export const middleware = betterI18nMiddleware
export const config = { matcher: ['/((?!api|_next).*)'] }Schnellstart
Fügen Sie i18n zu Ihrer Next.js‑App mit nur wenigen Zeilen Code hinzu.
// app/[locale]/page.tsx
import { getTranslations } from '@better-i18n/next';
export default async function Page({ params }: { params: { locale: string } }) {
const t = await getTranslations(params.locale, 'home');
return (
<main>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
</main>
);
}ISR und Internationalisierung
Kombinieren Sie inkrementelle statische Regenerierung mit i18n für schnelle, stets aktuelle mehrsprachige Seiten.
// app/[locale]/layout.tsx — ISR with i18n
import { getMessages } from '@better-i18n/use-intl/server';
import { BetterI18nProvider } from '@better-i18n/use-intl';
export const revalidate = 3600; // Revalidate every hour
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: { locale: string };
}) {
const messages = await getMessages({
project: 'your-org/your-project',
locale: params.locale,
});
return (
<BetterI18nProvider messages={messages} locale={params.locale}>
{children}
</BetterI18nProvider>
);
}ISR with generateStaticParams
Pre-render pages for every locale at build time, then refresh with ISR on a schedule.
// app/[locale]/[slug]/page.tsx — Generate static pages per locale
import { getMessages } from '@better-i18n/use-intl/server';
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: { locale: string; slug: string };
}) {
const t = await getMessages({
project: 'your-org/your-project',
locale: params.locale,
namespace: 'blog',
});
return <article><h1>{t[params.slug + '.title']}</h1></article>;
}On-Demand Revalidation
Trigger ISR revalidation when translations are updated — hook into the Better i18n publish webhook.
// app/api/revalidate/route.ts — On-demand ISR for translation updates
import { revalidatePath } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const { locale, path, secret } = await request.json();
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ error: 'Invalid secret' }, { status: 401 });
}
// Revalidate the specific locale path
revalidatePath(`/${locale}${path}`);
return NextResponse.json({ revalidated: true, locale, path });
}Edge-Laufzeitumgebung und Erkennung der Ländereinstellung
Führen Sie die Erkennung der Ländereinstellungen und das Laden von Nachrichten am Rand durch, um weltweit Reaktionszeiten von unter 50 ms zu erzielen.
// 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-Compatible Message Loading
Cache translations at the edge with a lightweight in-memory TTL cache for instant responses.
// 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 with i18n
Return translated API responses from edge functions with minimal 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 });
}Fehlerbehebung bei häufigen i18n-Problemen
Beheben Sie Probleme mit der Hydration, fehlende Locale-Fallbacks und Unterschiede bei der Datums-/Zahlenformatierung.
// Fix: Hydration mismatch with date/number formatting
// Problem: Server renders "1,000" but client renders "1.000"
// Solution: Ensure the same locale is used on both server and client
// app/[locale]/layout.tsx
import { getFormatter } from '@better-i18n/use-intl/server';
export default async function Layout({ children, params }: {
children: React.ReactNode;
params: { locale: string };
}) {
// Pre-format on server with the explicit locale
const formatter = await getFormatter(params.locale);
return (
<html lang={params.locale} suppressHydrationWarning>
<body>{children}</body>
</html>
);
}
// components/Price.tsx — Client component
'use client';
import { useFormatter } from '@better-i18n/use-intl';
export function Price({ amount }: { amount: number }) {
const format = useFormatter();
// useFormatter automatically uses the locale from the provider
// ensuring server and client render the same output
return <span>{format.number(amount, { style: 'currency', currency: 'USD' })}</span>;
}Locale Fallback Chain
Define fallback chains so regional variants like pt-BR fall back to pt, then 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
);
}Consistent Date Formatting
Avoid server/client date mismatches by explicitly setting timeZone to UTC.
// components/LocalizedDate.tsx — Consistent date formatting
'use client';
import { useFormatter, useLocale } from '@better-i18n/use-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>
);
}Fortgeschrittene Muster
Verschachtelte Layouts, parallele Routen und Serveraktionen mit typsicheren Übersetzungen.
// app/[locale]/dashboard/layout.tsx — Nested layout with namespace
import { getMessages } from '@better-i18n/use-intl/server';
import { BetterI18nProvider } from '@better-i18n/use-intl';
import { DashboardNav } from '@/components/DashboardNav';
export default async function DashboardLayout({
children,
params,
}: {
children: React.ReactNode;
params: { locale: string };
}) {
// Load dashboard-specific namespace alongside common messages
const [commonMessages, dashMessages] = await Promise.all([
getMessages({ project: 'your-org/your-project', locale: params.locale, namespace: 'common' }),
getMessages({ project: 'your-org/your-project', locale: params.locale, namespace: 'dashboard' }),
]);
const messages = { ...commonMessages, ...dashMessages };
return (
<BetterI18nProvider messages={messages} locale={params.locale}>
<DashboardNav />
<main>{children}</main>
</BetterI18nProvider>
);
}Parallel Routes with i18n
Load translations independently in parallel route slots for modular, locale-aware layouts.
// app/[locale]/@analytics/page.tsx — Parallel route with i18n
import { getTranslations } from '@better-i18n/next';
export default async function AnalyticsSlot({
params,
}: {
params: { locale: string };
}) {
const t = await getTranslations(params.locale, '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 with Translation
Return translated validation errors and success messages from server actions.
// app/[locale]/contact/actions.ts — Server action with i18n
'use server';
import { getTranslations } from '@better-i18n/next';
import { headers } from 'next/headers';
export async function submitContactForm(formData: FormData) {
const headersList = headers();
const locale = headersList.get('x-locale') ?? 'en';
const t = await getTranslations(locale, '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 '@better-i18n/use-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>
);
}Funktioniert mit beliebten Next.js i18n-Bibliotheken
Better i18n ersetzt nicht Ihre bevorzugte i18n-Bibliothek — es ist die Übersetzungsmanagement-Schicht, die sie noch leistungsfähiger macht.
Better i18n + next-intl
Die beliebteste Next.js i18n-Bibliothek mit App Router-Unterstützung, typsicheren Nachrichten und ICU-Syntax.
Better i18n synchronisiert Übersetzungen direkt im next-intl JSON-Format. Verwalten Sie im Dashboard, deployen Sie sofort über CDN.
Better i18n + next-i18next
Bewährte i18n-Bibliothek für Next.js basierend auf i18next. Ideal für Pages Router und Migration zum App Router.
Export im i18next-kompatiblen Namespace-JSON. Better i18n übernimmt den Übersetzungsworkflow, next-i18next die Laufzeit.
Better i18n + Lingui
Leichtgewichtige, makro-basierte i18n-Bibliothek mit exzellenter DX und automatischer Nachrichtenextraktion.
Extrahieren Sie Nachrichten mit Lingui CLI, verwalten Sie Übersetzungen in Better i18n, synchronisieren Sie automatisch über GitHub-Integration.
Related Articles
Django i18n mit KI-Übersetzung: Vollständige Einrichtungsanleitung
Django i18n mit KI-Übersetzung: Vollständige Einrichtungsanleitung Django wird mit einem ausgereiften Internationalisierungs-Framework (i18n) geliefert,...
Read more →Developer Experience im Detail: Wie Better i18n's Platform UX Lokalisierung schnell und intuitiv macht
Lokalisierungsplattformen haben den Ruf, unhandliche Oberflächen, langsame Editoren und Workflows zu haben, die wirken, als wären sie von einem Komitee...
Read more →Medienverwaltung mit Unsplash-Integration: Ein vollständiger CMS-Leitfaden
Die Verwaltung von Medien neben Übersetzungen war in den meisten i18n-Tools immer ein Nachgedanke. Sie übersetzen den Text und kümmern sich dann manuell...
Read more →Entdecken Sie weitere Leitfäden zu Frameworks
Mit Next.js‑i18n loslegen
Kostenloser Tarif verfügbar. Keine Kreditkarte erforderlich.