Internationalisierung in Next.js: 11-Locale-Implementierung
Um ein globales Publikum anzusprechen, ist mehr erforderlich, als Ihre Texte durch Google Translate laufen zu lassen. Eine Implementierung der Produktionsinternationalisierung umfasst Routing-Strategie, RTL-Layoutunterstützung (von rechts nach links), serverseitige Gebietsschemaauflösung, Übersetzungsschlüsselorganisation, SEO-Hreflang-Signale und einen nachhaltigen Übersetzungsworkflow. Wenn Sie einen dieser Punkte falsch verstehen, haben Sie entweder fehlerhafte Layouts für Arabisch-/Urdu-Benutzer, fehlende Hreflang-Tags, die Ihre Suchrankings aufteilen, oder eine Übersetzungspipeline, die unter der Last des Content-Wachstums zusammenbricht.
Dieses Handbuch dokumentiert die vollständige 11-Gebietsschema-Einrichtung, die auf ECOSIRE.COM verwendet wird – Englisch plus Chinesisch, Spanisch, Arabisch, Portugiesisch, Französisch, Deutsch, Japanisch, Türkisch, Hindi und Urdu – basierend auf next-intl v4.8. Jedes Muster hier wird anhand von 5.577 MDX-Inhaltsdateien und 12.543 Übersetzungsschlüsseln produktionsgetestet.
Wichtige Erkenntnisse
– Verwenden Sie
localePrefix: 'as-needed'– Englisch hat kein Präfix, andere Gebietsschemas erhalten/zh/,/ar/usw. – next-intl v4 verwendet das Musterrouting.ts+navigation.ts– importiert niemals ausnext/navigationdirekt in länderspezifische Komponenten
- Arabisch (
ar) und Urdu (ur) erforderndir="rtl"auf<html>und eine RTL-kompatible Schriftart (Noto Sans Arabic)en.json-Schlüssel MÜSSEN verschachtelte Objekte sein – flache, durch Punkte getrennte Schlüssel unterbrechen die Namespace-AuflösunggenerateMetadata()(niemals statischexport const metadata) mitalternates.languagesfür hreflang auf jeder Seite – Serverkomponenten verwendengetTranslations('namespace'), Clientkomponenten verwendenuseTranslations('namespace')– Admin-Seiten verwenden zwei Übersetzungs-Hooks:tfür modulspezifische Schlüssel,tcfür gemeinsameadmin.common-Schlüssel- Automatisieren Sie die Übersetzung neuer Schlüssel mit der Google Translate API; Blockieren Sie niemals die Entwicklung manueller Übersetzungen
Projekt-Setup
pnpm add next-intl@4
Verzeichnisstruktur für jede App:
src/
i18n/
routing.ts # defineRouting() — locale list, prefix strategy
navigation.ts # createNavigation() — locale-aware Link, useRouter
request.ts # getRequestConfig() — server-side message loading
messages/
en.json # Source of truth (~12,543 keys)
zh.json
es.json
ar.json
pt.json
fr.json
de.json
ja.json
tr.json
hi.json
ur.json
Routing-Konfiguration
// src/i18n/routing.ts
import { defineRouting } from 'next-intl/routing';
export const locales = [
'en', 'zh', 'es', 'ar', 'pt', 'fr', 'de', 'ja', 'tr', 'hi', 'ur',
] as const;
export type Locale = (typeof locales)[number];
export const rtlLocales: Locale[] = ['ar', 'ur'];
export const localeNames: Record<Locale, string> = {
en: 'English',
zh: '中文',
es: 'Español',
ar: 'العربية',
pt: 'Português',
fr: 'Français',
de: 'Deutsch',
ja: '日本語',
tr: 'Türkçe',
hi: 'हिन्दी',
ur: 'اردو',
};
export const routing = defineRouting({
locales,
defaultLocale: 'en',
localePrefix: 'as-needed', // /en/blog → /blog, /zh/blog → /zh/blog
});
// src/i18n/navigation.ts
import { createNavigation } from 'next-intl/navigation';
import { routing } from './routing';
export const { Link, redirect, useRouter, usePathname, getPathname } =
createNavigation(routing);
Middleware/Proxy
next-intl v4 verwendet eine Middleware-Funktion zur Erkennung und Weiterleitung des Gebietsschemas:
// src/proxy.ts (not middleware.ts — naming matters for Next.js 16)
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';
import { NextRequest, NextResponse } from 'next/server';
const intlMiddleware = createMiddleware(routing);
export function middleware(request: NextRequest) {
// Auth protection for dashboard routes
const isDashboard = request.nextUrl.pathname.includes('/dashboard');
const isPortal = request.nextUrl.pathname.includes('/portal');
const authCookie = request.cookies.get('ecosire_auth');
if ((isDashboard || isPortal) && !authCookie) {
const loginUrl = new URL('/auth/login', request.url);
// Prevent open redirect: only allow relative redirect paths
const from = request.nextUrl.pathname;
if (from.startsWith('/') && !from.startsWith('//')) {
loginUrl.searchParams.set('from', from);
}
return NextResponse.redirect(loginUrl);
}
return intlMiddleware(request);
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|.*\\..*|_vercel|favicon.ico).*)',
],
};
Serverseitiges Laden von Nachrichten
// src/i18n/request.ts
import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
// Validate locale is supported; fall back to default
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
};
});
Gebietsschema-Layout mit RTL- und Schriftartenunterstützung
// src/app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { getMessages, getTranslations } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing, rtlLocales, type Locale } from '@/i18n/routing';
import { Noto_Sans, Noto_Sans_Arabic } from 'next/font/google';
const notoSans = Noto_Sans({ subsets: ['latin'], variable: '--font-sans' });
const notoArabic = Noto_Sans_Arabic({
subsets: ['arabic'],
variable: '--font-arabic',
});
export async function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'metadata' });
return {
title: { template: `%s | ${t('siteName')}`, default: t('defaultTitle') },
other: { 'content-language': locale }, // For Bing/AI engine language detection
};
}
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
if (!routing.locales.includes(locale as Locale)) {
notFound();
}
const messages = await getMessages();
const isRtl = rtlLocales.includes(locale as Locale);
return (
<html
lang={locale}
dir={isRtl ? 'rtl' : 'ltr'}
className={`${notoSans.variable} ${notoArabic.variable}`}
>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
Übersetzungsschlüsselorganisation
Die en.json-Struktur muss verschachtelte Objekte verwenden, keine flachen, durch Punkte getrennten Schlüssel:
// WRONG — flat keys break next-intl namespace resolution
{
"nav.home": "Home",
"nav.blog": "Blog",
"admin.common.save": "Save"
}
// CORRECT — nested objects
{
"nav": {
"home": "Home",
"blog": "Blog"
},
"admin": {
"common": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"status": "Status"
},
"contacts": {
"title": "Contacts",
"addContact": "Add Contact",
"searchPlaceholder": "Search contacts..."
}
}
}
Serverkomponenten
// Server component: use getTranslations (async)
import { getTranslations } from 'next-intl/server';
export default async function ServicesPage({ params }: Props) {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'services.odoo' });
return (
<main>
<h1>{t('hero.title')}</h1>
<p>{t('hero.description')}</p>
</main>
);
}
// generateMetadata must also fetch translations for locale-aware metadata
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'services.odoo' });
return {
title: t('meta.title'),
description: t('meta.description'),
alternates: {
canonical: `https://ecosire.com${locale === 'en' ? '' : `/${locale}`}/services/odoo`,
languages: {
'x-default': 'https://ecosire.com/services/odoo',
en: 'https://ecosire.com/services/odoo',
zh: 'https://ecosire.com/zh/services/odoo',
ar: 'https://ecosire.com/ar/services/odoo',
// ... all 11 locales
},
},
};
}
Client-Komponenten
// Client component: use useTranslations (synchronous)
'use client';
import { useTranslations } from 'next-intl';
// Admin pages use two hooks: module-specific + shared admin.common
export default function ContactsPage() {
const t = useTranslations('admin.contacts'); // Module-specific
const tc = useTranslations('admin.common'); // Shared terms
// IMPORTANT: stateConfig must be inside component (after hooks init)
const stateConfig = {
active: { label: tc('statusActive'), className: 'bg-green-100 text-green-800' },
inactive: { label: tc('statusInactive'), className: 'bg-gray-100 text-gray-700' },
};
return (
<div>
<h1>{t('title')}</h1>
<button>{tc('addNew')}</button>
{/* ... */}
</div>
);
}
Sprachumschalter-Komponente
// components/language-switcher.tsx
'use client';
import { useLocale } from 'next-intl';
import { usePathname, useRouter } from '@/i18n/navigation'; // NOT next/navigation
import { locales, localeNames } from '@/i18n/routing';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Globe } from 'lucide-react';
export function LanguageSwitcher() {
const locale = useLocale();
const pathname = usePathname();
const router = useRouter();
const handleLocaleChange = (newLocale: string) => {
router.replace(pathname, { locale: newLocale });
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button aria-label="Change language">
<Globe className="h-4 w-4" />
<span>{localeNames[locale as keyof typeof localeNames]}</span>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{locales.map((loc) => (
<DropdownMenuItem
key={loc}
onClick={() => handleLocaleChange(loc)}
className={loc === locale ? 'font-bold' : ''}
>
{localeNames[loc]}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}
Übersetzungsworkflow
Neue Schlüssel hinzufügen
- Zuerst zu
en.jsonhinzufügen (Quelle der Wahrheit) - Führen Sie das Übersetzungsskript aus:
node apps/web/scripts/translate-missing.mjs
Das Skript findet Schlüssel, die in en.json vorhanden sind, aber in anderen Gebietsschemadateien fehlen, und ruft die Google Translate-API auf:
// scripts/translate-missing.mjs (simplified)
import { Translate } from '@google-cloud/translate/build/src/v2/index.js';
import { readFileSync, writeFileSync } from 'fs';
const translate = new Translate({ key: process.env.GOOGLE_TRANSLATE_API_KEY });
async function translateMissingKeys(sourceLocale, targetLocale, sourceMessages, targetMessages) {
const missing = findMissingKeys(sourceMessages, targetMessages);
for (const [keyPath, value] of missing) {
const [translated] = await translate.translate(value, targetLocale);
setNestedKey(targetMessages, keyPath, translated);
}
return targetMessages;
}
Validierung der Übersetzungsabdeckung
# Check for missing keys across all locales
node apps/web/scripts/validate-blog-i18n.mjs
# Fix flat keys accidentally added
node apps/web/scripts/fix-en-flat-keys.mjs
Übersetzung von Blog-Inhalten
Bei MDX-Inhaltsdateien bleiben die Frontmatter-Titel auf Englisch (Titel sind in i18n JSON), aber der Hauptinhalt wird je nach Gebietsschema übersetzt:
src/content/blog/
en/ # English originals (or root directory)
odoo-erp-guide.mdx
zh/
odoo-erp-guide.mdx # Translated body
ar/
odoo-erp-guide.mdx
# ... 10 locale directories
// src/lib/blog.ts — locale-aware loader
export async function getPost(slug: string, locale: string) {
// Try locale-specific file first
const localePath = path.join(contentDir, locale, `${slug}.mdx`);
const englishPath = path.join(contentDir, `${slug}.mdx`);
const filePath = fs.existsSync(localePath) ? localePath : englishPath;
const raw = fs.readFileSync(filePath, 'utf8');
const { data: frontmatter, content } = matter(raw);
return { frontmatter, content, locale };
}
Häufig gestellte Fragen
Warum localePrefix: 'as-needed' anstelle von always verwenden?
Mit as-needed haben englische URLs kein Präfix (/blog/post), während andere Gebietsschemas ein Präfix (/zh/blog/post) erhalten. Dies ist die Standardkonvention – Englisch ist die Standardeinstellung und verfügt über saubere URLs. Mit always erhält Englisch auch /en/blog/post, wodurch unnötige Weiterleitungen entstehen und Ihr vorhandenes englisches Linkkapital aufgeteilt wird, wenn Sie migrieren.
Warum müssen en.json-Schlüssel verschachtelte Objekte und keine flachen, durch Punkte getrennten Zeichenfolgen sein?
Die Namespace-Auflösung von next-intl teilt die Namespace-Zeichenfolge nach Punkten auf, um den JSON-Baum zu durchlaufen. Wenn es sich bei Ihren Schlüsseln um flache, durch Punkte getrennte Zeichenfolgen wie "admin.contacts.title" handelt, sucht next-intl nach messages.admin.contacts.title als verschachteltem Pfad, stellt jedoch fest, dass der Schlüssel buchstäblich die Zeichenfolge "admin.contacts.title" auf der Stammebene ist, was zu einem Suchfehler führt. Verwenden Sie immer tatsächlich verschachtelte Objekte.
Wie gehe ich mit sprachübergreifender Pluralisierung um?
next-intl verwendet das ICU-Nachrichtenformat zur Pluralisierung. Englisch hat zwei Pluralformen (one/other), Arabisch hat sechs (null, eins, zwei, wenige, viele, andere). Verwenden Sie die ICU-Syntax {count, plural, one {# item} other {# items}} in Ihren Übersetzungswerten und next-intl wendet automatisch die richtigen Pluralregeln für jedes Gebietsschema an.
Kann ich next-intl mit dem Pages Router verwenden?
next-intl v4 ist hauptsächlich für den App Router konzipiert. Verwenden Sie für Pages Router next-intl v2 oder v3. Die Muster unterscheiden sich erheblich: Pages Router verwendet getStaticProps + NextIntlProvider, während App Router getRequestConfig + NextIntlClientProvider im Layout verwendet. Wenn Sie den Pages Router verwenden, sollten Sie eine Migration zum App Router in Betracht ziehen, bevor Sie i18n in großem Maßstab implementieren.
Wie richte ich die RTL-Unterstützung für Arabisch und Urdu ein?
Legen Sie dir="rtl" für das Element <html> fest, wenn das Gebietsschema ar oder ur ist. Verwenden Sie das Variantenpräfix rtl: von Tailwind für richtungsspezifische Stile (rtl:mr-4 wird in RTL zu ml-4). Laden Sie eine kompatible Schriftart – Noto Sans Arabic deckt sowohl arabische als auch Urdu-Schriften ab. Vermeiden Sie hartcodierte CSS-Werte left/right; Verwenden Sie die logischen Eigenschaften start/end (ms-4, me-4 in Tailwind v4).
Was ist der beste Ansatz, um große Mengen an Inhalten schnell zu übersetzen?
Verwenden Sie die Google Translate API für die erste maschinelle Übersetzung aller Schlüssel und Inhalte und lassen Sie anschließend gut sichtbare Inhalte (Homepage, Produktbeschreibungen, CTA-Kopie) von Muttersprachlern überprüfen. Für Blog-Inhalte ist die maschinelle Übersetzung als Ausgangspunkt akzeptabel, sollte jedoch auf technische Richtigkeit überprüft werden. Budget für Muttersprachlerbewertung von mindestens den drei bis fünf beliebtesten Orten nach Verkehr.
Nächste Schritte
Richtig durchgeführte Internationalisierung ist ein Wettbewerbsvorteil – Ihre Inhalte erreichen 11-mal mehr potenzielle Kunden, Ihre SEO-Hreflang-Signale werden konsolidiert, anstatt das Link-Equity aufzuteilen, und Benutzer in ihrer Muttersprache konvertieren deutlich schneller.
ECOSIRE hat eine vollständige Implementierung mit 11 Gebietsschemata erstellt und verwaltet diese, die 5.577 Inhaltsdateien, 12.543 Übersetzungsschlüssel und RTL-Layoutunterstützung umfasst. Wenn Sie eine Internationalisierung für Ihre Next.js-Anwendung benötigen, [entdecken Sie unsere Frontend-Engineering-Dienste] (/services), um zu erfahren, wie wir sie für Ihre Codebasis implementieren können.
Geschrieben von
ECOSIRE Research and Development Team
Entwicklung von Enterprise-Digitalprodukten bei ECOSIRE. Einblicke in Odoo-Integrationen, E-Commerce-Automatisierung und KI-gestützte Geschäftslösungen.
Verwandte Artikel
Next.js 16 App Router: Production Patterns and Pitfalls
Production-ready Next.js 16 App Router patterns: server components, caching strategies, metadata API, error boundaries, and performance pitfalls to avoid.
Ausweitung des E-Commerce auf internationale Märkte: Ein vollständiger Strategieleitfaden
Vollständiger Leitfaden zur internationalen E-Commerce-Expansion, der Marktforschung, Lokalisierung, Logistik, Zahlungen, Einhaltung gesetzlicher Vorschriften und Marketingstrategie umfasst.
ERP für die internationale Expansion: Verwaltung länderübergreifender Aktivitäten im Jahr 2026
So nutzen Sie ERP-Systeme zur Verwaltung der internationalen Expansion, einschließlich Buchhaltung in mehreren Währungen, Steuerkonformität, Lokalisierung, Lieferkettenmanagement und Odoo-Konfiguration für mehrere Unternehmen.