Next.js Internationalisierung und statische Generierung: SSG-Mehrsprachen-Website in der Praxis

Beim ersten Versuch, im Next.js App Router mehrsprachige statische Seiten zu generieren, bin ich wirklich in jede Falle getappt. Vielleicht kennen Sie das: Die Konfiguration sieht in der Doku korrekt aus – und beim build fliegen Fehler. Oder der Build läuft durch, braucht aber fünfzehn Minuten für alle Locales …
Hier ein paar Szenen aus dieser Woche – kommt Ihnen etwas bekannt vor?
Sind Sie auch auf diese Probleme gestoßen?
Szenario 1: Build-Time-Fehler
Ich erinnere mich, dass ich einmal aufgeregt „npm run build“ ausgeführt habe und das Terminal mir einfach eine Fehlermeldung angezeigt hat:
Error: Page "/en/about" is missing `generateStaticParams()`
so it cannot be used with `output: "export"`.
Ich war völlig verwirrt und dachte: „Was zum Teufel? Ich habe eindeutig „i18n“ in „next.config.js“ konfiguriert!“ Später entdeckte ich, dass die Internationalisierung im App Router völlig anders funktioniert als im Pages Router – die alte Konfiguration funktioniert einfach nicht mehr.
Szenario 2: Übermäßige Buildzeit
Ein anderes Mal unterstützte mein Projekt 6 Sprachen mit etwa 50 Seiten pro Sprache. Der Build dauerte:
✓ Generating static pages (152/152) - 15m 32s
15 Minuten! Das haben Sie richtig gelesen. Jedes Mal, wenn ich eine kleine Änderung vornahm, so lange warten zu müssen, hat die Developer Experience ruiniert. Ich dachte ständig: „Wenn das in Produktion geht, wird CI/CD dann nicht ewig warten?“
Szenario 3: Übersetzungsaktualisierungen werden nicht wirksam
Das frustrierendste Problem war folgendes: Ich habe die Übersetzungsdatei „zh-CN.json“ aktualisiert, neu erstellt und erneut bereitgestellt, aber auf der Website wurden immer noch die alten Übersetzungen angezeigt! Ich musste den Browser-Cache leeren, um den neuen Inhalt zu sehen. Dies ist eine Katastrophe in der Produktion – Benutzer sehen veraltete Inhalte.
Was ist die Ursache?
Nachdem ich viel Zeit mit Recherchen verbracht hatte, verstand ich endlich den Kern dieser Probleme:
-
App Router unterstützt die i18n-Konfiguration von Pages Router nicht mehr – Das ist das größte Problem. Das Konfigurieren des Felds „i18n“ in „next.config.js“ funktioniert mit App Router einfach nicht.
-
Konflikt zwischen statischem Export und dynamischem Rendering – Mit
output: 'export'müssen alle Seiten zum Build-Zeitpunkt feststehen. Dynamische APIs wiecookies()oderheaders()führen dann zu Fehlern. -
Mechanismus zum Zwischenspeichern von Übersetzungsdateien – Next.js speichert importierte JSON-Dateien zwischen. Wenn Sie Übersetzungen während der Entwicklung aktualisieren, wird der Cache nicht ungültig, sodass Sie den neuesten Inhalt nicht sehen können.
Wenn Sie auf diese Probleme gestoßen sind, ist dieser Artikel genau das Richtige für Sie. Ich erkläre Ihnen Schritt für Schritt, wie Sie die mehrsprachige statische Generierung im Next.js App Router richtig implementieren und diese Fallstricke vermeiden.
Das neue i18n-Paradigma von App Router verstehen
Bevor wir mit dem Codieren beginnen, ist es meiner Meinung nach wichtig, den Internationalisierungsansatz von App Router zu verstehen. Es ist wirklich ganz anders als Pages Router.
Pages Router vs. App Router: Zwei völlig unterschiedliche Ansätze
Damit Sie die Unterschiede auf einen Blick erkennen können, habe ich eine Vergleichstabelle erstellt:
| Merkmal | Pages Router | App Router |
|---|---|---|
| Konfiguration | i18n in next.config.js | Middleware + dynamische Route [lang] |
| Routenstruktur | Automatisch /en/, /zh/ usw. | Manuell app/[lang]/page.tsx |
| Statische Generierung | getStaticPaths | generateStaticParams |
| Übersetzungen | serverSideTranslations | Server Components importieren JSON direkt |
Sehen Sie? Fast alles hat sich geändert. Als ich zum ersten Mal darauf stieß, hatte ich ehrlich gesagt das Gefühl: „Ich muss gefälschte Next.js gelernt haben.“
Was genau ist „generateStaticParams“?
Dies ist eines der Kernkonzepte von App Router. Einfach ausgedrückt besteht sein Zweck darin, Next.js mitzuteilen: „Ich muss statische Seiten für diese Parameter generieren.“
Hier ist ein Beispiel:
// app/[lang]/layout.tsx
export async function generateStaticParams() {
// Return all language parameters that need pre-rendering
return [
{ lang: 'en' },
{ lang: 'zh' },
{ lang: 'ja' }
]
}
Next.js führt diese Funktion während des Builds aus, ruft die zurückgegebene Parameterliste ab und generiert dann eine statische HTML-Datei für jede Parameterkombination. Die endgültige Ausgabe ist:
out/
├── en/
│ └── index.html
├── zh/
│ └── index.html
└── ja/
└── index.html
Wichtiger Punkt: Diese Funktion muss in „layout.tsx“ oder „page.tsx“ definiert sein und der Funktionsname muss genau übereinstimmen (nicht „getStaticParams“, nicht „generateParams“, muss „generateStaticParams“ sein).
Wie lade ich Übersetzungsdateien?
In der Pages Router-Ära verwendeten wir die Funktion „serverSideTranslations“ aus der Bibliothek „next-i18next“. Aber in App Router können Sie Übersetzungsdateien direkt in Serverkomponenten importieren:
// Server components can do this directly
import enTranslations from '@/i18n/locales/en/common.json'
import zhTranslations from '@/i18n/locales/zh-CN/common.json'
const translations = {
'en': enTranslations,
'zh-CN': zhTranslations,
}
export default function Page({ params }: { params: { lang: string } }) {
const t = translations[params.lang]
return <h1>{t.title}</h1>
}
Allerdings gibt es bei diesem Ansatz ein Problem: Alle Sprachübersetzungen werden gebündelt, wodurch die Datei größer wird. In realen Projekten schreiben wir normalerweise eine Loader-Funktion:
// i18n/utils.ts
export async function loadTranslations(locale: string, namespaces: string[]) {
const translations: Record<string, any> = {}
for (const ns of namespaces) {
try {
const translation = await import(`@/i18n/locales/${locale}/${ns}.json`)
translations[ns] = translation.default
} catch (error) {
console.warn(`Translation file not found: ${locale}/${ns}`)
translations[ns] = {}
}
}
return translations
}
Auf diese Weise können Sie bei Bedarf laden und nur die Übersetzungs-Namespaces laden, die für die aktuelle Seite benötigt werden.
Hands-On: Aufbau eines mehrsprachigen SSG-Projekts von Grund auf
Okay, die Theorie ist fertig – jetzt lasst uns programmieren. Ich begleite Sie von Anfang bis Ende durch den Aufbau einer kompletten mehrsprachigen statischen Website.
Schritt 1: Entwerfen Sie die Projektstruktur
Zunächst müssen wir eine klare Verzeichnisstruktur einrichten. Dies ist die Struktur, die ich in realen Projekten verwende – getestet und bewährt:
app/
├── [lang]/ # Language dynamic route (core)
│ ├── layout.tsx # Root layout, contains generateStaticParams
│ ├── page.tsx # Home page
│ ├── about/
│ │ └── page.tsx # About page
│ └── blog/
│ ├── page.tsx # Blog list
│ └── [slug]/
│ └── page.tsx # Blog details (nested dynamic route)
├── i18n/
│ ├── locales/ # Translation files directory
│ │ ├── en/
│ │ │ ├── common.json # Common translations
│ │ │ ├── home.json # Home page translations
│ │ │ └── blog.json # Blog translations
│ │ ├── zh-CN/
│ │ │ ├── common.json
│ │ │ ├── home.json
│ │ │ └── blog.json
│ │ └── ja/
│ │ ├── common.json
│ │ ├── home.json
│ │ └── blog.json
│ ├── config.ts # i18n configuration file
│ └── utils.ts # Translation utility functions
└── middleware.ts # Language detection and redirection
Warum dieses Design?
- Ordner „[lang]“: Dies ist der Kern des dynamischen Routings – Next.js übergibt den Sprachparameter von der URL an Seitenkomponenten.
- Namespace-basierte Übersetzungsdateien: Vermeiden Sie eine große Übersetzungsdatei, teilen Sie sie nach Seitenfunktionalität auf und laden Sie sie bei Bedarf.
- Zentralisiertes „config.ts“: Alle sprachbezogenen Konfigurationen an einem Ort für einfache Wartung.
Schritt 2: Core i18n-Dateien konfigurieren
Schreiben wir die Konfigurationsdatei – das ist die Grundlage des gesamten Systems:
// i18n/config.ts
export const i18nConfig = {
// Supported languages list
locales: ['en', 'zh-CN', 'ja'],
// Default language
defaultLocale: 'en',
// Path prefix strategy
// 'always': All languages get prefix /en/, /zh-CN/
// 'as-needed': Default language has no prefix, others do
localePrefix: 'always',
// [IMPORTANT] Only pre-render major languages (optimize build time)
localesToPrerender: process.env.NODE_ENV === 'production'
? ['en', 'zh-CN'] // Production: only pre-render English and Chinese
: ['en'], // Development: only render default language
} as const
// Export types for TypeScript type checking
export type Locale = (typeof i18nConfig)['locales'][number]
// Translation namespaces (for code splitting)
export const namespaces = ['common', 'home', 'about', 'blog'] as const
export type Namespace = (typeof namespaces)[number]
Wichtige Punkte erklärt:
- „as const“: Dies ist die TypeScript-Syntax, die sicherstellt, dass es sich bei dem Typ um präzise Literaltypen und nicht um breite „string[]“-Typen handelt.
- „localesToPrerender“: Das ist entscheidend! Wenn Sie 10 Sprachen unterstützen, aber nur zwei Hauptsprachen vorab rendern, kann die Erstellungszeit um 80 % reduziert werden. Andere Sprachen können ISR (Inkrementelle statische Regeneration) oder On-Demand-Generierung verwenden.
- Namespaces: Teilen Sie Übersetzungsdateien in mehrere JSONs auf, um zu vermeiden, dass beim ersten Laden eine große Übersetzungsdatei heruntergeladen wird.
Schritt 3: Implementieren Sie das Dienstprogramm zum Laden von Übersetzungen
Dies ist ein einfacher, aber praktischer Übersetzungslader:
// i18n/utils.ts
import type { Locale, Namespace } from './config'
// Translation file cache (avoid repeated reads)
const translationsCache = new Map<string, any>()
/**
* Load translation files for specified language
*
* @param locale Language code like 'en', 'zh-CN'
* @param namespaces Translation namespace array like ['common', 'home']
* @returns Translation object { common: {...}, home: {...} }
*/
export async function loadTranslations(
locale: Locale,
namespaces: Namespace[]
) {
const translations: Record<string, any> = {}
for (const namespace of namespaces) {
const cacheKey = `${locale}-${namespace}`
// Check cache to avoid repeated loading
if (!translationsCache.has(cacheKey)) {
try {
// Dynamic import of translation file
const translation = await import(
`@/i18n/locales/${locale}/${namespace}.json`
)
translationsCache.set(cacheKey, translation.default)
} catch (error) {
console.warn(`⚠️ Translation file not found: ${locale}/${namespace}.json`)
translationsCache.set(cacheKey, {})
}
}
translations[namespace] = translationsCache.get(cacheKey)
}
return translations
}
/**
* Create type-safe translation function
*
* Usage:
* const t = createTranslator(translations)
* t('common.nav.home')
* t('home.welcome', { name: 'John' }) // Supports variable replacement
*/
export function createTranslator(translations: any) {
return (key: string, params?: Record<string, string>) => {
const keys = key.split('.')
let value = translations
// Access nested properties layer by layer
for (const k of keys) {
value = value?.[k]
}
// Return key itself when translation not found (useful for debugging)
if (!value) {
console.warn(`⚠️ Translation missing: ${key}`)
return key
}
// Support variable replacement: replace {{name}} with actual value
if (params) {
return Object.entries(params).reduce(
(str, [key, val]) => str.replace(`{{${key}}}`, val),
value
)
}
return value
}
}
Highlights dieses Dienstprogramms:
- Caching-Mechanismus: Cache nach dem ersten Laden, um wiederholte Dateilesevorgänge zu vermeiden.
- Fehlerbehandlung: Wenn die Übersetzungsdatei nicht gefunden wird, stürzt das Programm nicht ab – es wird lediglich eine Warnung ausgegeben und ein leeres Objekt zurückgegeben.
- Variablenersetzung: Unterstützt die Verwendung von
{{VariableName}}-Platzhaltern in Übersetzungen. - Typfreundlich: Funktioniert mit TypeScript zur typsicheren Überprüfung von Übersetzungsschlüsseln.
Schritt 4: Root-Layout erstellen (am kritischsten)
Dies ist die Kerndatei des gesamten mehrsprachigen Systems:
// app/[lang]/layout.tsx
import { i18nConfig } from '@/i18n/config'
import { loadTranslations } from '@/i18n/utils'
import type { Locale } from '@/i18n/config'
/**
* [CORE] Generate static parameters for all languages
*
* This function executes at build time, Next.js generates corresponding static pages based on return value
*
* Important notes:
* 1. Function name must be generateStaticParams (can't misspell)
* 2. Must be defined in layout.tsx or page.tsx
* 3. Returned parameter names must match route folder names ([lang] → lang)
*/
export async function generateStaticParams() {
console.log(`🌍 Generating static params for ${i18nConfig.localesToPrerender.length} locales...`)
return i18nConfig.localesToPrerender.map((locale) => ({
lang: locale, // ⚠️ Note: Must be 'lang' not 'locale'
}))
}
/**
* Root layout component
*
* This component wraps all pages for setting global configuration
*/
export default async function RootLayout({
children,
params,
}: {
children: React.ReactNode
params: { lang: string }
}) {
// Load common translations (navigation, footer, etc.)
const translations = await loadTranslations(params.lang as Locale, ['common'])
return (
<html
lang={params.lang}
// Set right-to-left layout for Arabic
dir={params.lang === 'ar' ? 'rtl' : 'ltr'}
>
<head>
{/* Add global meta tags here */}
</head>
<body>
{/* Place global components like navbar, footer here */}
{children}
</body>
</html>
)
}
/**
* Generate metadata (SEO)
*
* This function generates page <title>, <meta> tags, etc.
*/
export async function generateMetadata({ params }: { params: { lang: string } }) {
return {
// Set language-related meta tags
alternates: {
canonical: `https://example.com/${params.lang}`,
languages: {
'en': 'https://example.com/en',
'zh-CN': 'https://example.com/zh-CN',
'ja': 'https://example.com/ja',
},
},
// Open Graph tags (for social media sharing)
openGraph: {
locale: params.lang,
alternateLocale: i18nConfig.locales.filter(l => l !== params.lang),
},
}
}
Einige einfache Fallstricke:
⚠️ Falle 1: Parameternamen müssen übereinstimmen
// ❌ Wrong: parameter name is locale, but route folder is [lang]
export async function generateStaticParams() {
return [{ locale: 'en' }] // This will error
}
// ✅ Correct: parameter name matches folder name
export async function generateStaticParams() {
return [{ lang: 'en' }] // Must be lang
}
```⚠️ **Falle 2: Dynamische APIs können nicht verwendet werden**
```typescript
// ❌ Wrong: using cookies in statically generated page
export default async function Layout({ children }) {
const locale = cookies().get('NEXT_LOCALE') // This causes build failure
return <html lang={locale}>{children}</html>
}
// ✅ Correct: use route parameters
export default async function Layout({ children, params }) {
return <html lang={params.lang}>{children}</html>
}
Schritt 5: Übersetzungsdateien erstellen
Auch die Struktur der Übersetzungsdateien ist wichtig. Hier ist mein empfohlenes Format:
// i18n/locales/en/common.json
{
"nav": {
"home": "Home",
"about": "About",
"blog": "Blog",
"contact": "Contact"
},
"footer": {
"copyright": "© {{year}} All rights reserved",
"privacy": "Privacy Policy",
"terms": "Terms of Service"
},
"actions": {
"readMore": "Read More",
"backToTop": "Back to Top",
"share": "Share",
"edit": "Edit"
},
"messages": {
"loading": "Loading...",
"error": "Something went wrong",
"success": "Success",
"noResults": "No results found"
}
}
``````json
// i18n/locales/en/blog.json
{
"title": "Blog Posts",
"publishedAt": "Published on",
"author": "Author",
"tags": "Tags",
"relatedPosts": "Related Posts",
"readingTime": "Reading time: {{minutes}} minutes",
"shareOn": "Share on {{platform}}"
}
Best Practices für Übersetzungsdateien:
- Hierarchische Struktur: Verwenden Sie verschachtelte Objekte, um Übersetzungen zu organisieren, platzieren Sie nicht alle Schlüssel auf der obersten Ebene.
- Variablenplatzhalter: Verwenden Sie das Format
{{variableName}}für eine konsistente Handhabung. - Schlüssel konsistent halten: Alle Sprachübersetzungsdateien sollten die gleiche Schlüsselstruktur haben.
- Kommentare hinzufügen: Fügen Sie neben komplexen Übersetzungen Kommentare hinzu, die Nutzungsszenarien erläutern.
Schritt 6: Verschachtelte dynamische Routen verarbeiten
Wenn Ihr Projekt über einen Blog oder Produktdetailseiten verfügt, müssen Sie verschachtelte dynamische Routen verarbeiten. Das war eine meiner größten Fallstricke.
// app/[lang]/blog/[slug]/page.tsx
import { i18nConfig } from '@/i18n/config'
import { loadTranslations, createTranslator } from '@/i18n/utils'
import type { Locale } from '@/i18n/config'
// Assume you have these helper functions (need to implement yourself in real projects)
async function getBlogSlugs(): Promise<string[]> {
// Get all blog post slugs from filesystem or CMS
return ['getting-started', 'advanced-tips', 'performance-guide']
}
async function getBlogPost(slug: string, locale: Locale) {
// Get blog post content for specific language
// ...
}
/**
* [KEY] generateStaticParams for nested routes
*
* Need to generate all combinations of language × posts
* For example: en/getting-started, zh-CN/getting-started, en/advanced-tips...
*/
export async function generateStaticParams() {
const startTime = Date.now()
console.log('📝 Generating blog post params...')
// Get all post slugs (only need one request)
const slugs = await getBlogSlugs()
// Use flatMap to generate all combinations of languages and posts
const params = i18nConfig.localesToPrerender.flatMap((locale) =>
slugs.map((slug) => ({
lang: locale,
slug: slug,
}))
)
const duration = Date.now() - startTime
console.log(`✅ Generated ${params.length} blog post params in ${duration}ms`)
return params
}
/**
* Blog post page component
*/
export default async function BlogPost({
params,
}: {
params: { lang: string; slug: string }
}) {
// Load translations and post content
const [translations, post] = await Promise.all([
loadTranslations(params.lang as Locale, ['common', 'blog']),
getBlogPost(params.slug, params.lang as Locale),
])
const t = createTranslator(translations)
return (
<article className="prose">
<h1>{post.title}</h1>
<p className="text-gray-600">
{t('blog.publishedAt')}: {new Date(post.date).toLocaleDateString(params.lang)}
</p>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
Kernpunkt der Leistungsoptimierung:
Hier liegt ein häufiger Fehler vor. Sie könnten versucht sein, Daten für jede Sprache separat abzurufen:
// ❌ Wrong approach: multiple requests, slow build
export async function generateStaticParams() {
const results = []
for (const locale of i18nConfig.localesToPrerender) {
// Query database or CMS once per language - too slow!
const slugs = await getBlogSlugs(locale)
results.push(...slugs.map(slug => ({ lang: locale, slug })))
}
return results
}
Der richtige Ansatz besteht darin, die Daten einmal anzufordern und dann mit „flatMap“ Kombinationen zu generieren:
// ✅ Correct approach: one request, fast generation
export async function generateStaticParams() {
// Only one data request
const slugs = await getBlogSlugs()
// Use flatMap to generate all language × post combinations
return i18nConfig.localesToPrerender.flatMap((locale) =>
slugs.map((slug) => ({ lang: locale, slug }))
)
}
In meinem Projekt reduzierte diese Optimierung die Bauzeit von 18 Minuten auf 6 Minuten – ein sehr spürbarer Effekt!
Schritt 7: Implementieren Sie Middleware zur Spracherkennung
Der Zweck der Middleware besteht darin, die Sprachpräferenz des Benutzers automatisch zu erkennen und auf die entsprechende Sprachversion umzuleiten.
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { i18nConfig } from './i18n/config'
/**
* Middleware
*
* This function executes before each request for:
* 1. Detecting user's language preference
* 2. Redirecting to corresponding language path
*/
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// Check if path already includes language prefix
const pathnameHasLocale = i18nConfig.locales.some(
(locale) =>
pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
// If language prefix already exists, pass through
if (pathnameHasLocale) return
// Get user's preferred language
const locale = getLocale(request) ?? i18nConfig.defaultLocale
// Redirect to path with language prefix
request.nextUrl.pathname = `/${locale}${pathname}`
return NextResponse.redirect(request.nextUrl)
}
/**
* Language detection function
*
* Priority:
* 1. Language preference saved in Cookie
* 2. Accept-Language request header
* 3. Return null, use default language
*/
function getLocale(request: NextRequest): string | null {
// Priority 1: Check Cookie
const localeCookie = request.cookies.get('NEXT_LOCALE')?.value
if (localeCookie && i18nConfig.locales.includes(localeCookie as any)) {
return localeCookie
}
// Priority 2: Check Accept-Language request header
const acceptLanguage = request.headers.get('accept-language')
if (acceptLanguage) {
// Accept-Language format: zh-CN,zh;q=0.9,en;q=0.8
const preferred = acceptLanguage.split(',')[0].split('-')[0]
const match = i18nConfig.locales.find(locale =>
locale.toLowerCase().startsWith(preferred.toLowerCase())
)
if (match) return match
}
// No matching language found, return null
return null
}
/**
* Middleware configuration
*
* matcher defines which paths need to execute middleware
*/
export const config = {
// Match all paths except:
// - API routes starting with /api
// - Static files at /_next/static
// - Images at /_next/image
// - Static resources like /favicon.ico
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}
Middleware-Anwendungsfälle:
Angenommen, ein Benutzer besucht direkt „https://example.com/blog“, dann wird die Middleware Folgendes tun:
- Überprüfen Sie, ob im Cookie eine gespeicherte Spracheinstellung vorhanden ist (z. B. hat der Benutzer zuvor Chinesisch ausgewählt).
- Wenn nicht, überprüfen Sie den Header „Accept-Language“ des Browsers (der Browser sendet automatisch die Systemsprache des Benutzers).
- Leiten Sie basierend auf den Erkennungsergebnissen zu „https://example.com/zh-CN/blog“ oder „https://example.com/en/blog“ weiter
Dadurch wird eine automatische Spracherkennung für ein besseres Benutzererlebnis erreicht.
Schritt 8: Sprachumschalter-Komponente erstellen
Schließlich benötigen wir einen Sprachumschalter, damit Benutzer die Sprache manuell ändern können:
// components/LanguageSwitcher.tsx
'use client'
import { usePathname, useRouter } from 'next/navigation'
import { i18nConfig } from '@/i18n/config'
import type { Locale } from '@/i18n/config'
// Language display name mapping
const localeNames: Record<Locale, string> = {
'en': 'English',
'zh-CN': 'Chinesisch (vereinfacht)',
'ja': 'Japanisch',
}
export function LanguageSwitcher({ currentLocale }: { currentLocale: Locale }) {
const pathname = usePathname()
const router = useRouter()
const handleLocaleChange = (newLocale: Locale) => {
// Save language preference to Cookie
document.cookie = `NEXT_LOCALE=${newLocale};path=/;max-age=31536000`
// Replace language prefix in path
// Example: /zh-CN/blog → /en/blog
const newPathname = pathname.replace(`/${currentLocale}`, `/${newLocale}`)
// Navigate to new language version
router.push(newPathname)
}
return (
<div className="relative">
<select
value={currentLocale}
onChange={(e) => handleLocaleChange(e.target.value as Locale)}
className="px-4 py-2 border rounded-lg"
>
{i18nConfig.locales.map((locale) => (
<option key={locale} value={locale}>
{localeNames[locale]}
</option>
))}
</select>
</div>
)
}
Verwendung in der Navigation:
// components/Navigation.tsx
import { LanguageSwitcher } from './LanguageSwitcher'
export function Navigation({ lang }: { lang: string }) {
return (
<nav className="flex items-center justify-between p-4">
<div className="flex gap-4">
<a href={`/${lang}/`}>Home</a>
<a href={`/${lang}/about`}>About</a>
<a href={`/${lang}/blog`}>Blog</a>
</div>
<LanguageSwitcher currentLocale={lang} />
</nav>
)
}
Leistungsoptimierung: Builds zum Fliegen bringen
Jetzt ist die Grundfunktionalität implementiert. Wenn Ihre Website jedoch mehrere Sprachen unterstützt, kann die Erstellungszeit recht lang sein. Lassen Sie mich einige praktische Optimierungstechniken vorstellen.
Optimierung 1: Selektives Pre-Rendering
Dies ist die effektivste Optimierung. Wenn Sie 10 Sprachen unterstützen, sich der tatsächliche Datenverkehr jedoch auf zwei bis drei Hauptsprachen konzentriert, rendern Sie einfach die Hauptsprachen vorab:
// i18n/config.ts
export const i18nConfig = {
// All supported languages
locales: ['en', 'zh-CN', 'ja', 'ko', 'de', 'fr', 'es', 'pt'],
defaultLocale: 'en',
// [KEY] Only pre-render major languages
localesToPrerender: process.env.NODE_ENV === 'production'
? ['en', 'zh-CN'] // Production: only pre-render English and Chinese
: ['en'], // Development: only render default language (faster development)
}
Leistungsvergleich:
| Konfiguration | Bauzeit | Beschreibung |
|---|---|---|
| 8 Sprachen vorab rendern | ~24 Minuten | Alle Sprachen generieren statische Seiten |
| 2 Sprachen vorab rendern | ~6 Minuten | Andere beim ersten Besuch generierte Sprachen |
| Nur 1 Sprache rendern | ~3 Minuten | Empfohlen für die Entwicklung |
75 % der Bauzeit eingespart!
Optimierung 2: Inkrementelle statische Regeneration (ISR) verwenden
Für weniger wichtige Sprachen oder selten aufgerufene Seiten können Sie ISR für die On-Demand-Generierung verwenden:
// app/[lang]/blog/[slug]/page.tsx
// Enable ISR, revalidate after 1 hour
export const revalidate = 3600
export async function generateStaticParams() {
const slugs = await getBlogSlugs()
// Only pre-render popular posts in major languages
const topSlugs = slugs.slice(0, 10) // Only pre-render top 10
return i18nConfig.localesToPrerender.flatMap((locale) =>
topSlugs.map((slug) => ({ lang: locale, slug }))
)
}
// [IMPORTANT] Allow dynamic generation of non-pre-rendered pages
export const dynamicParams = true
Mit dieser Konfiguration:
- Build generiert nur 2 Sprachen × 10 Beiträge = 20 Seiten
- Wenn Benutzer nicht vorgerenderte Seiten besuchen, generiert Next.js diese in Echtzeit und speichert sie zwischen
- Automatische Cache-Updates nach 1 Stunde
Optimierung 3: Paralleler Datenabruf
Wenn Sie in „generateStaticParams“ mehrere Datentypen abrufen müssen, verarbeiten Sie diese auf jeden Fall parallel:
// ❌ Wrong: serial fetching (slow)
export async function generateStaticParams() {
const posts = await getBlogPosts() // Wait 2 seconds
const categories = await getCategories() // Wait 1 second
// Total: 3 seconds
}
// ✅ Correct: parallel fetching (fast)
export async function generateStaticParams() {
const [posts, categories] = await Promise.all([
getBlogPosts(), // Execute simultaneously
getCategories(), // Execute simultaneously
])
// Total: 2 seconds (whichever is longest)
}
In meinem Projekt reduzierte diese Optimierung die Datenabrufzeit um 40 %.
Optimierung 4: Probleme mit dem Übersetzungs-Cache lösen
Das Ärgerlichste während der Entwicklung ist, dass die Übersetzungsdateien aktualisiert werden, die Seite jedoch nicht aktualisiert wird. Dies liegt daran, dass Next.js importierte JSON-Dateien zwischenspeichert.
Lösung: Cache in der Entwicklungsumgebung deaktivieren
// i18n/utils.ts
import fs from 'fs/promises'
import path from 'path'
const isDev = process.env.NODE_ENV === 'development'
export async function loadTranslations(
locale: Locale,
namespaces: Namespace[]
) {
// Development environment: re-read file each time
if (isDev) {
const translations: Record<string, any> = {}
for (const ns of namespaces) {
const filePath = path.join(
process.cwd(),
'i18n',
'locales',
locale,
`${ns}.json`
)
try {
const content = await fs.readFile(filePath, 'utf-8')
translations[ns] = JSON.parse(content)
} catch (error) {
console.warn(`Translation file not found: ${filePath}`)
translations[ns] = {}
}
}
return translations
}
// Production environment: use cache
return loadTranslationsWithCache(locale, namespaces)
}
Jetzt wird während der Entwicklung beim Aktualisieren der Seite nach dem Aktualisieren der Übersetzungsdateien der neueste Inhalt angezeigt.
Leitfaden zur Fehlerbehebung bei häufigen Problemen
Bei der tatsächlichen Entwicklung können andere Probleme auftreten. Hier habe ich die häufigsten und meine Lösungen zusammengestellt.
Problem 1: Build-Fehler „generateStaticParams nicht gefunden“
Fehlermeldung:
Error: Page "/en/about" is missing `generateStaticParams()`
so it cannot be used with `output: "export"`.
Schritte zur Fehlerbehebung:
- ✅ Überprüfen Sie, ob „generateStaticParams“ in „layout.tsx“ oder „page.tsx“ definiert ist
- ✅ Bestätigen Sie, dass die Schreibweise des Funktionsnamens korrekt ist (nicht „getStaticParams“, nicht „generateParams“).
- ✅ Bestätigen Sie, dass die Funktion ordnungsgemäß exportiert wurde (muss „Asynchrone Funktion exportieren“ lauten)
- ✅ Überprüfen Sie, ob die Parameternamen mit den Routenordnernamen übereinstimmen
// ❌ Wrong example
export async function getStaticParams() { // Wrong function name
return [{ locale: 'en' }] // Wrong parameter name too
}
// ✅ Correct example
export async function generateStaticParams() {
return [{ lang: 'en' }] // Parameter name must match [lang]
}
Problem 2: Dynamisches Rendering erkannt
Fehlermeldung:
Error: Route /[lang]/about couldn't be rendered statically
because it used `headers` or `cookies`.
Ursache: Verwendete dynamische APIs („headers()“, „cookies()“, „searchParams“) in statisch generierten Seiten.
Lösung:
// ❌ Wrong: using cookies in server component
export default async function Page() {
const locale = cookies().get('NEXT_LOCALE') // Triggers dynamic rendering
return <div>...</div>
}
// ✅ Solution 1: Handle in middleware
// middleware.ts
export function middleware(request: NextRequest) {
const locale = request.cookies.get('NEXT_LOCALE')
// Processing logic...
}
// ✅ Solution 2: Use client component
'use client'
export function LanguageSwitcher() {
const [locale, setLocale] = useState(() => {
// Read Cookie on client side
return getCookie('NEXT_LOCALE')
})
// ...
}
Problem 3: Übersetzungsdatei nicht gefunden
Fehlermeldung:
Error: Cannot find module './locales/en/common.json'
Checkliste:
- ✅ Überprüfen Sie, ob der Dateipfad korrekt ist (Groß-/Kleinschreibung beachten, Linux unterscheidet zwischen Groß- und Kleinschreibung)
- ✅ Bestätigen Sie, dass die JSON-Dateisyntax korrekt ist (kann mit Online-Tools validiert werden)
- ✅ Überprüfen Sie die Pfad-Alias-Konfiguration von „tsconfig.json“:
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
}
}
- ✅ Bestätigen Sie, dass die Übersetzungsdateien zum Zeitpunkt der Erstellung ordnungsgemäß eingebunden sind:
// next.config.js
module.exports = {
// Ensure JSON files are included
webpack: (config) => {
config.module.rules.push({
test: /\.json$/,
type: 'json',
})
return config
},
}
Problem 4: Routenparameter gehen nach Sprachumstellung verloren
Symptom: Beim Wechsel von „/zh-CN/blog/my-post“ zu Englisch wird zu „/en/“ statt zu „/en/blog/my-post“ weitergeleitet.
Ursache: Der Sprachumschalter behält Routenparameter nicht ordnungsgemäß bei.
Lösung:
// ❌ Wrong: hardcoded path
<Link href="/about">About</Link>
// ✅ Solution 1: manually concatenate language parameter
<Link href={`/${params.lang}/about`}>About</Link>
// ✅ Solution 2: wrap a smart Link component
// components/LocalizedLink.tsx
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
export function LocalizedLink({
href,
children,
...props
}: {
href: string
children: React.ReactNode
[key: string]: any
}) {
const pathname = usePathname()
// Extract language from current path
const locale = pathname.split('/')[1]
// Automatically add language prefix
const localizedHref = `/${locale}${href}`
return (
<Link href={localizedHref} {...props}>
{children}
</Link>
)
}
Problem 5: Fehlende oder falsche SEO-Tags
Problem: SEO-Tags für mehrsprachige Seiten (hreflang, canonical) sind falsch konfiguriert, was sich auf die Suchmaschinenindizierung auswirkt.
Lösung: Konfigurieren Sie die „generateMetadata“ jeder Seite ordnungsgemäß:
// app/[lang]/blog/[slug]/page.tsx
export async function generateMetadata({
params,
}: {
params: { lang: string; slug: string }
}) {
const baseUrl = 'https://example.com'
return {
// Page title and description
title: 'My Blog Post',
description: 'This is a blog post',
// Canonical URL
alternates: {
canonical: `${baseUrl}/${params.lang}/blog/${params.slug}`,
// hreflang tags (tell search engines about other language versions)
languages: {
'en': `${baseUrl}/en/blog/${params.slug}`,
'zh-CN': `${baseUrl}/zh-CN/blog/${params.slug}`,
'ja': `${baseUrl}/ja/blog/${params.slug}`,
'x-default': `${baseUrl}/en/blog/${params.slug}`, // Default language
},
},
// Open Graph tags (for social media sharing)
openGraph: {
title: 'My Blog Post',
description: 'This is a blog post',
url: `${baseUrl}/${params.lang}/blog/${params.slug}`,
locale: params.lang,
alternateLocale: i18nConfig.locales.filter(l => l !== params.lang),
},
}
}
Best Practices-Zusammenfassung
Nach all dieser Übung habe ich eine Checkliste mit Best Practices zusammengestellt, die Sie direkt umsetzen können.
Checkliste für die Projektinitialisierung
Stellen Sie vor Beginn der Entwicklung sicher, dass diese Konfigurationen vollständig sind:
- Bestimmen Sie die Liste der unterstützten Sprachen und die Standardsprache
- Erstellen Sie die Verzeichnisstruktur „app/[lang]“.
- [] Konfigurieren Sie „i18n/config.ts“ und Übersetzungsdateiverzeichnisse
- [] Implementieren Sie die Spracherkennung „middleware.ts“.
- „generateStaticParams“ zum Root-Layout hinzufügen
- [] Konfigurieren Sie „next.config.js“ (wenn ein statischer Export erforderlich ist, legen Sie „output: ‘export‘“ fest)
Empfehlungen für die Entwicklungsphase
- Die Entwicklungsumgebung rendert nur die Standardsprache vor („localesToPrerender: [‘en’]`)
- Verwenden Sie TypeScript, um die Typsicherheit von Übersetzungsschlüsseln zu gewährleisten
- Übersetzungs-Namespaces nach Funktionsmodul aufteilen (Common, Home, Blog …)
- Übersetzungscache in der Entwicklungsumgebung deaktivieren (verwenden Sie „fs.readFile“ zum Lesen in Echtzeit)
- Warnprotokolle für fehlende Übersetzungen hinzufügen (erleichtert das Auffinden von Problemen)
Checkliste für die Produktionsbereitstellung
- Selektives Pre-Rendering der wichtigsten Sprachen (Optimierung der Build-Zeit)
- ISR-Strategie konfigurieren (Nebensprachen werden bei Bedarf generiert, „erneut validieren“ festlegen)
- [] Parallelen Datenabruf verwenden („Promise.all“)
- Konfigurieren Sie korrekte Hreflang- und Canonical-Tags
- CDN-Cache-Strategie festlegen (mehrsprachige Pfade berücksichtigen)
- Überwachen Sie den Datenverkehr und die Erstellungszeit für jede Sprachversion
Vervollständigen Sie das Konfigurationsbeispiel für next.config.js
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// Static export (if needed)
output: 'export',
// Image optimization config
images: {
unoptimized: true, // Required for static export
},
// Environment variables
env: {
BUILD_TIME: new Date().toISOString(),
},
// Custom build ID (for cache invalidation)
generateBuildId: async () => {
return `build-${Date.now()}`
},
// Webpack configuration
webpack: (config, { isServer }) => {
// Ensure JSON files are handled correctly
config.module.rules.push({
test: /\.json$/,
type: 'json',
})
return config
},
}
module.exports = nextConfig
Empfohlene Tools und Bibliotheken
Wenn Sie nicht von Grund auf implementieren möchten, ziehen Sie diese vorgefertigten Bibliotheken in Betracht:
| Werkzeug/Bibliothek | Zweck | Bewertung | Notizen |
|---|---|---|---|
| next-intl | Komplette i18n-Lösung | ⭐⭐⭐⭐⭐ | Offiziell empfohlen, am umfassendsten, unterstützt App Router |
| next-international | Leichte i18n-Bibliothek | ⭐⭐⭐⭐ | Leicht und sauber, typsicher |
| @formatjs/intl | Internationalisierungsformatierung | ⭐⭐⭐⭐ | Behandelt Datums-, Zahlen- und Währungsformate |
| typesafe-i18n | Typsichere Übersetzungen | ⭐⭐⭐⭐ | Generiert automatisch Typdefinitionen |
| i18next | Veteran i18n-Bibliothek | ⭐⭐⭐ | Leistungsstark, muss aber für App Router angepasst werden |
Ich persönlich empfehle next-intl – es wurde speziell für Next.js App Router entwickelt, funktioniert sofort und erfordert nicht viel Konfiguration. Wenn Sie jedoch die Prinzipien der i18n-Implementierung tiefgreifend verstehen möchten oder eine umfassende Anpassung benötigen, ist die manuelle Implementierung (wie in diesem Artikel) ebenfalls eine gute Wahl.
Fazit
Um es noch einmal zusammenzufassen: Wir haben eine vollständige Next.js App Router-Lösung zur mehrsprachigen statischen Generierung implementiert, einschließlich:
-
Kernfunktionalität
- Mehrsprachige Struktur basierend auf dynamischem Routing „[lang]“.
- Statische Seitengenerierung mit „generateStaticParams“.
- Übersetzungsdateisystem nach Namespace aufgeteilt
- Automatische Spracherkennung und -umleitung durch Middleware
-
Leistungsoptimierung
- Selektives Pre-Rendering der wichtigsten Sprachen (reduzierte Build-Zeit um 75 %)
- Verwendung von ISR für die On-Demand-Generierung von Nebensprachen
- Paralleler Datenabruf
– Deaktivieren des Caches in der Entwicklungsumgebung
-
Problemlösung
- „generateStaticParams“-Konfigurationsfehler
– Dynamische APIs verursachen Build-Fehler - Probleme beim Zwischenspeichern von Übersetzungsdateien
- Routenparameterverlust beim Sprachwechsel
- SEO-Tag-Konfiguration
- „generateStaticParams“-Konfigurationsfehler
Wichtige Erkenntnisse:
– i18n im App Router erfordert eine manuelle Implementierung, kann die Pages Router-Konfiguration nicht verwenden
- „generateStaticParams“ muss im Layout oder auf der Seite definiert sein, Parameternamen müssen übereinstimmen
– Statisch generierte Seiten können keine dynamischen APIs wie „cookies()“ oder „headers()“ verwenden
– Setzen Sie selektives Pre-Rendering und ISR mit Bedacht ein, um übermäßige Build-Zeiten zu vermeiden
Wenn Sie auch mehrsprachige Websites mit Next.js App Router erstellen, hoffe ich, dass Ihnen dieser Artikel dabei hilft, einige Fallstricke zu vermeiden. Die Internationalisierung selbst ist nicht komplex – der Schlüssel liegt darin, den Build-Mechanismus von Next.js zu verstehen und entsprechend seinen Regeln zu konfigurieren.
Wenn Ihnen die manuelle Implementierung zu umständlich ist, probieren Sie schließlich die Bibliothek „next-intl“ aus – sie kann Ihnen viel Arbeit ersparen.
{{< geo:tldr >}}
- App Router unterstützt die Pages-Router-i18n-Konfiguration nicht mehr: Das i18n-Feld in next.config.js greift nicht – generateStaticParams muss alle Sprachversionen manuell erzeugen
- Build-Fehler beheben: generateStaticParams im layout.tsx für alle Locales; Parameternamen müssen zu [locale] passen; statische Seiten dürfen cookies(), headers() usw. nicht nutzen
- Build-Zeit optimieren: 6 Sprachen × 50 Seiten = 300 Seiten, ~15 Minuten Build – selektives Pre-Rendering wichtiger Seiten und ISR (Incremental Static Regeneration)
- Übersetzungen wirken nicht: Next.js cached importierte JSON-Dateien – Cache leeren oder dynamischen Import nutzen
- Best Practice: generateStaticParams für alle Sprachen, keine dynamischen APIs auf statischen Seiten, selektives Pre-Rendering und ISR für kürzere Builds
{{< /geo:tldr >}}
Next.js mehrsprachige statische Generierung – vollständiger Ablauf
Vom Build-Fehler über Build-Zeit-Optimierung bis zu aktualisierten Übersetzungen
⏱️ Estimated time: 3 hr
- 1
Step 1: Build-Fehler beheben: generateStaticParams konfigurieren
Problem: Page "/en/about" is missing generateStaticParams()
Lösung: generateStaticParams in layout.tsx
```tsx
// app/[locale]/layout.tsx
export async function generateStaticParams() {
return [
{ locale: 'zh' },
{ locale: 'en' },
]
}
export default async function LocaleLayout({
children,
params: { locale }
}) {
// ...
}
```
Wichtig:
• generateStaticParams in layout oder page definieren
• Parameternamen müssen zu [locale] passen
• alle Sprachversionen zurückgeben
Hinweis: Statische Seiten dürfen cookies(), headers() usw. nicht nutzen - 2
Step 2: Build-Zeit optimieren
Problem: 6 Sprachen × 50 Seiten = 300 Seiten, Build ~15 Minuten.
Optimierungen:
1. Selektives Pre-Rendering:
```tsx
export async function generateStaticParams() {
return [
{ locale: 'zh', slug: 'home' },
{ locale: 'zh', slug: 'about' },
{ locale: 'en', slug: 'home' },
{ locale: 'en', slug: 'about' },
]
}
```
2. ISR:
```tsx
export const revalidate = 3600
```
3. Paralleler Aufbau:
```tsx
export async function generateStaticParams() {
const locales = ['zh', 'en', 'ja', 'ko', 'fr', 'de']
const pages = ['home', 'about', 'contact']
return locales.flatMap(locale =>
pages.map(slug => ({ locale, slug }))
)
}
```
Ergebnis: oft unter 5 Minuten - 3
Step 3: Übersetzungs-Updates wirksam machen
Problem: Übersetzungsdatei aktualisiert, Website zeigt noch alte Texte.
Ursache: Next.js cached importierte JSON-Dateien.
Lösungen:
1. Dynamischer Import:
```tsx
const messages = await import(`../messages/${locale}.json`)
```
2. Cache leeren:
```bash
rm -rf .next
npm run build
```
3. Zeitstempel:
```tsx
const messages = await import(
`../messages/${locale}.json?v=${Date.now()}`
)
```
Tipp: In der Entwicklung dynamischen Import nutzen - 4
Step 4: Konflikte mit dynamischen APIs vermeiden
Problem: Statische Seiten dürfen cookies(), headers() usw. nicht nutzen.
Lösung:
```tsx
// ❌ Statische Seite mit dynamischer API
export default async function Page() {
const cookies = await cookies()
return <div>...</div>
}
// ✅ Dynamische Seite
export const dynamic = 'force-dynamic'
export default async function Page() {
const cookies = await cookies()
return <div>...</div>
}
```
Statische und dynamische Seiten trennen
FAQ
Warum schlägt der Build einer mehrsprachigen App-Router-Website fehl?
Pages Router:
• i18n in next.config.js
• automatischer Sprachwechsel
• alle Sprachversionen automatisch
App Router:
• next.config.js-i18n wird ignoriert
• generateStaticParams manuell
• Parameternamen müssen zu [locale] passen
Fehler:
```
Error: Page "/en/about" is missing generateStaticParams()
so it cannot be used with output: "export".
```
Lösung:
```tsx
export async function generateStaticParams() {
return [{ locale: 'zh' }, { locale: 'en' }]
}
```
generateStaticParams in layout oder page, Namen passend zu [locale].
Wie verkürze ich die Build-Zeit einer mehrsprachigen Website?
1. Selektives Pre-Rendering wichtiger Seiten
2. ISR mit revalidate
3. Parallele Kombinationen mit flatMap
Effekt: oft unter 5 Minuten. Selektives Pre-Rendering und ISR sinnvoll kombinieren.
Warum wirken Übersetzungs-Updates nicht?
Lösungen: dynamischer Import, .next löschen und neu bauen, Zeitstempel im Import-Pfad. In der Entwicklung dynamischen Import bevorzugen.
Dürfen statisch generierte Seiten dynamische APIs nutzen?
Alternativen: dynamic = 'force-dynamic' oder Logik in Middleware/Client Components. force-dynamic-Seiten werden nicht statisch vorgerendert.
Wie konfiguriere ich generateStaticParams?
Was sind Best Practices für mehrsprachige Websites?
21 Min. Lesezeit · Veröffentlicht am: 25. Dez. 2025 · Aktualisiert am: 14. Juli 2026
Next.js Komplettleitfaden
Wenn du über die Suche hier gelandet bist, kommst du am schnellsten weiter, indem du zum vorherigen oder nächsten Beitrag dieser Serie springst.
Vorheriger
Next.js Internationalisierung – Vollständiger Leitfaden: next-intl Best Practices
Ausführliche Anleitung zur Internationalisierung mit Next.js App Router – next-intl-Konfiguration, mehrsprachiges Routing und Best Practices für Übersetzungsdateien, inklusive praktischer Codebeispiele
Teil 15 von 51
Nächster
Next.js Mehrsprachige SEO: Vollständiger Leitfaden – Suchmaschinen indexieren jede Sprache korrekt
Über 60 % der mehrsprachigen Websites haben SEO-Konfigurationsfehler. Dieser Artikel erklärt hreflang-Konfiguration, mehrsprachige Sitemaps, URL-Strategien und typische Fallen – damit jede Sprachversion korrekt rankt.
Teil 17 von 51
Ähnliche Beiträge
Next.js App Router Einstieg: Kernkonzepte und Grundlagen im Überblick

Next.js App Router Einstieg: Kernkonzepte und Grundlagen im Überblick
Next.js 15 in der Praxis: Wie ich an einem Wochenende ein produktionsreifes Blog-System aufgebaut habe


Kommentare
Melde dich mit GitHub an, um einen Kommentar zu hinterlassen