Next.jsReactTypeScripti18n

Mehrsprachige Architektur mit Next.js App Router + Static Export

Sloth255
Sloth255
·2 min read·326 words

Hintergrund und Anforderungen

Wir möchten mehrsprachige Unterstützung unter folgenden Einschränkungen implementieren:

  • Next.js 15 App Router mit TypeScript
  • Static Export (output: 'export') – Deployment über S3 + CloudFront
  • Kein Server-Side Rendering – Dynamische APIs und Middleware können nicht verwendet werden
  • SEO erforderlich – Suchmaschinen müssen geeignete Sprachinhalte bereitgestellt werden

Architekturübersicht

1. Routing-Design

/                    # Root-Seite (Standard: en)
/en/                 # Englische Seite
/ja/                 # Japanische Seite

Verwendet Dynamic Segment [locale], um Seiten für jede Sprache während der statischen Generierung vor zu rendern.

2. Übersetzungsdaten-Management

Übersetzungsdaten werden im JSON-Format verwaltet:

src/locales/en.json
{
  "title": "title"
}
src/locales/ja.json
{
  "title": "タイトル"
}

3. Zustandsverwaltung mit React Context API

src/i18n/context.tsx
interface I18nContextType {
  messages: Record<string, any>;
  locale: string;
}

const I18nContext = createContext<I18nContextType | undefined>(undefined);

export function I18nProvider({ 
  children, 
  messages, 
  locale 
}: I18nProviderProps) {
  return (
    <I18nContext.Provider value={{ messages, locale }}>
      {children}
    </I18nContext.Provider>
  );
}

export function useTranslations() {
  const context = useContext(I18nContext);
  if (!context) {
    throw new Error('useTranslations must be used within I18nProvider');
  }
  
  const t = (key: string): string => {
    // Verschachtelte Schlüssel unterstützen
    const keys = key.split('.');
    let value: any = context.messages;
    for (const k of keys) {
      value = value?.[k];
    }
    return typeof value === 'string' ? value : key;
  };
  
  return { 
    t, 
    locale: context.locale,
    messages: context.messages,
    isLoaded: true 
  };
}

4. Context-Bereitstellung in der Layout-Hierarchie

src/app/layout.tsx
export default async function RootLayout({ children }: Props) {
  const locale = defaultLocale;
  const messages = await getMessages(locale);
  
  return (
    <html lang={locale}>
      <body>
        <I18nProvider messages={messages} locale={locale}>
          <LanguageSwitcherClient />
          {children}
        </I18nProvider>
      </body>
    </html>
  );
}
src/app/[locale]/layout.tsx
export default async function LocaleLayout({ children, params }: Props) {
  const { locale } = await params;
  const messages = await getMessages(locale);
  
  return (
    <div lang={locale}>
      <I18nProvider messages={messages} locale={locale}>
        {children}
      </I18nProvider>
    </div>
  );
}

Standardsprache und clientseitige Weiterleitung

Serverseitig (Während der statischen Generierung)

Die Root-Seite (/) wird immer in Englisch (en) generiert:

src/app/page.tsx
export default async function RootPage() {
  const locale = defaultLocale; // 'en'
  const messages = await getMessages(locale);
  
  return (
    <I18nProvider messages={messages} locale={locale}>
      <ClientLocaleRedirect />
      <PageContent />
    </I18nProvider>
  );
}

Begründung:

  • SEO-Strategie: Stabile Inhalte für Suchmaschinen-Crawler bereitstellen
  • Static-Export-Einschränkungen: Benutzersprache kann serverseitig nicht bestimmt werden

Clientseitig (Browser)

Beim ersten Zugriff wird nach Erkennung der Browser-Spracheinstellungen automatisch weitergeleitet:

src/components/ClientLocaleRedirect.tsx
'use client';

export default function ClientLocaleRedirect() {
  useEffect(() => {
    // Prüfen, ob Weiterleitung bereits durchgeführt wurde
    if (typeof window === 'undefined' || 
        sessionStorage.getItem('localeRedirectDone')) {
      return;
    }

    const supportedLocales = ['en', 'ja'];
    const pathname = window.location.pathname;
    
    // Nur für Root-Pfad weiterleiten
    if (pathname === '/') {
      // Browser-Spracheinstellungen abrufen
      const browserLocales = navigator.languages || [navigator.language];
      
      // Unterstützte Sprache finden
      const preferredLocale = browserLocales
        .map(lang => lang.split('-')[0]) // 'ja-JP' -> 'ja'
        .find(lang => supportedLocales.includes(lang));
      
      // Weiterleiten, wenn sich von Standard unterscheidet
      if (preferredLocale && preferredLocale !== defaultLocale) {
        sessionStorage.setItem('localeRedirectDone', 'true');
        window.location.replace(`/${preferredLocale}`);
      }
    }
  }, []);

  return null;
}

Verhaltensszenarien

Situation Während SSG Client-Verhalten
Suchmaschinen-Crawler / auf Englisch generieren Keine Weiterleitung
Japanischer Browser-Nutzer / auf Englisch generieren Weiterleitung zu /ja
Englischer Browser-Nutzer / auf Englisch generieren Keine Weiterleitung (wie angezeigt)
Nutzer mit nicht unterstützter Sprache / auf Englisch generieren Keine Weiterleitung (auf Englisch anzeigen)

Verwendung in Komponenten

'use client';

function NetworkInfoClient() {
  const { t } = useTranslations();
  
  return (
    <div>
      <h2>{t('title')}</h2>
    </div>
  );
}

Technische Vorteile

  1. Typsicherheit: TypeScript + Context API ermöglicht Prüfungen auf das Vorhandensein von Übersetzungsschlüsseln
  2. Klare Abhängigkeiten: Komponenten, die useTranslations verwenden, müssen innerhalb des Providers platziert werden, was das Design ausdrücklich macht
  3. Performance: Schnelle Seitenauslieferung durch statische Generierung
  4. SEO: Jede Sprachseite hat eine unabhängige URL, optimiert für Suchmaschinen
  5. UX: Automatische Weiterleitung basierend auf Browser-Spracheinstellungen reduziert den Aufwand für Benutzer

Zusammenfassung

Wir haben mehrsprachige Unterstützung mit der React Context API in einer Next.js App Router Static-Export-Umgebung implementiert. Wir ergänzen serverseitige Einschränkungen durch clientseitige Logik und erreichen so bessere UX.

Das Ändern der Standardsprache erfordert nur das Bearbeiten von defaultLocale in config.ts und gewährleistet Erweiterbarkeit.