Design wechseln

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

Easton editorial illustration: component assembly loom

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:

  1. 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.

  2. Konflikt zwischen statischem Export und dynamischem Rendering – Mit output: 'export' müssen alle Seiten zum Build-Zeitpunkt feststehen. Dynamische APIs wie cookies() oder headers() führen dann zu Fehlern.

  3. 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:

MerkmalPages RouterApp Router
Konfigurationi18n in next.config.jsMiddleware + dynamische Route [lang]
RoutenstrukturAutomatisch /en/, /zh/ usw.Manuell app/[lang]/page.tsx
Statische GenerierunggetStaticPathsgenerateStaticParams
ÜbersetzungenserverSideTranslationsServer 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?

  1. Ordner „[lang]“: Dies ist der Kern des dynamischen Routings – Next.js übergibt den Sprachparameter von der URL an Seitenkomponenten.
  2. Namespace-basierte Übersetzungsdateien: Vermeiden Sie eine große Übersetzungsdatei, teilen Sie sie nach Seitenfunktionalität auf und laden Sie sie bei Bedarf.
  3. 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:

  1. „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.
  2. „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.
  3. 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:

  1. Caching-Mechanismus: Cache nach dem ersten Laden, um wiederholte Dateilesevorgänge zu vermeiden.
  2. 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.
  3. Variablenersetzung: Unterstützt die Verwendung von {{VariableName}}-Platzhaltern in Übersetzungen.
  4. 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:

  1. Hierarchische Struktur: Verwenden Sie verschachtelte Objekte, um Übersetzungen zu organisieren, platzieren Sie nicht alle Schlüssel auf der obersten Ebene.
  2. Variablenplatzhalter: Verwenden Sie das Format {{variableName}} für eine konsistente Handhabung.
  3. Schlüssel konsistent halten: Alle Sprachübersetzungsdateien sollten die gleiche Schlüsselstruktur haben.
  4. 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:

  1. Überprüfen Sie, ob im Cookie eine gespeicherte Spracheinstellung vorhanden ist (z. B. hat der Benutzer zuvor Chinesisch ausgewählt).
  2. Wenn nicht, überprüfen Sie den Header „Accept-Language“ des Browsers (der Browser sendet automatisch die Systemsprache des Benutzers).
  3. 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:

KonfigurationBauzeitBeschreibung
8 Sprachen vorab rendern~24 MinutenAlle Sprachen generieren statische Seiten
2 Sprachen vorab rendern~6 MinutenAndere beim ersten Besuch generierte Sprachen
Nur 1 Sprache rendern~3 MinutenEmpfohlen 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:

  1. Build generiert nur 2 Sprachen × 10 Beiträge = 20 Seiten
  2. Wenn Benutzer nicht vorgerenderte Seiten besuchen, generiert Next.js diese in Echtzeit und speichert sie zwischen
  3. 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:

  1. ✅ Überprüfen Sie, ob „generateStaticParams“ in „layout.tsx“ oder „page.tsx“ definiert ist
  2. ✅ Bestätigen Sie, dass die Schreibweise des Funktionsnamens korrekt ist (nicht „getStaticParams“, nicht „generateParams“).
  3. ✅ Bestätigen Sie, dass die Funktion ordnungsgemäß exportiert wurde (muss „Asynchrone Funktion exportieren“ lauten)
  4. ✅ Ü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:

  1. ✅ Überprüfen Sie, ob der Dateipfad korrekt ist (Groß-/Kleinschreibung beachten, Linux unterscheidet zwischen Groß- und Kleinschreibung)
  2. ✅ Bestätigen Sie, dass die JSON-Dateisyntax korrekt ist (kann mit Online-Tools validiert werden)
  3. ✅ Überprüfen Sie die Pfad-Alias-Konfiguration von „tsconfig.json“:
// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./*"]
    }
  }
}
  1. ✅ 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/BibliothekZweckBewertungNotizen
next-intlKomplette i18n-Lösung⭐⭐⭐⭐⭐Offiziell empfohlen, am umfassendsten, unterstützt App Router
next-internationalLeichte i18n-Bibliothek⭐⭐⭐⭐Leicht und sauber, typsicher
@formatjs/intlInternationalisierungsformatierung⭐⭐⭐⭐Behandelt Datums-, Zahlen- und Währungsformate
typesafe-i18nTypsichere Übersetzungen⭐⭐⭐⭐Generiert automatisch Typdefinitionen
i18nextVeteran 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:

  1. Kernfunktionalität

    • Mehrsprachige Struktur basierend auf dynamischem Routing „[lang]“.
    • Statische Seitengenerierung mit „generateStaticParams“.
    • Übersetzungsdateisystem nach Namespace aufgeteilt
    • Automatische Spracherkennung und -umleitung durch Middleware
  2. 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
  3. Problemlösung

    • „generateStaticParams“-Konfigurationsfehler
      – Dynamische APIs verursachen Build-Fehler
    • Probleme beim Zwischenspeichern von Übersetzungsdateien
    • Routenparameterverlust beim Sprachwechsel
    • SEO-Tag-Konfiguration

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. 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. 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. 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. 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?
Ursache: App Router unterstützt die Pages-Router-i18n-Konfiguration nicht.

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?
Problem: 6 Sprachen × 50 Seiten ≈ 15 Minuten Build.

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?
Next.js cached importierte JSON-Dateien.

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?
Nein – cookies(), headers() usw. brechen statische Generierung.

Alternativen: dynamic = 'force-dynamic' oder Logik in Middleware/Client Components. force-dynamic-Seiten werden nicht statisch vorgerendert.
Wie konfiguriere ich generateStaticParams?
In layout.tsx oder page.tsx definieren, alle Locale-Kombinationen zurückgeben, Parameternamen wie [locale] und [slug]. Bei vielen Kombinationen selektives Pre-Rendering oder ISR nutzen.
Was sind Best Practices für mehrsprachige Websites?
generateStaticParams für alle Sprachen, dynamische APIs auf statischen Seiten vermeiden, selektives Pre-Rendering und ISR, JSON-Übersetzungen mit Typen, Performance im Blick. Bei Bedarf next-intl statt Hand-Rollout.

21 Min. Lesezeit · Veröffentlicht am: 25. Dez. 2025 · Aktualisiert am: 14. Juli 2026

Ähnliche Beiträge

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog