Files
hoerdle/I18N.md
2025-11-28 15:48:27 +01:00

8.8 KiB

Internationalisierung (i18n) Dokumentation

Hördle unterstützt vollständige Mehrsprachigkeit (Internationalisierung) für Deutsch und Englisch.

Übersicht

Die i18n-Implementierung basiert auf next-intl und nutzt den Next.js App Router mit dynamischen [locale]-Segmenten.

Unterstützte Sprachen

  • Deutsch (de) - Standardsprache
  • Englisch (en)

URL-Struktur

Alle Routen sind lokalisiert:

  • http://localhost:3000/ → Redirect zu /de (Standard)
  • http://localhost:3000/de → Deutsche Version
  • http://localhost:3000/en → Englische Version
  • http://localhost:3000/de/admin → Admin-Dashboard (Deutsch)
  • http://localhost:3000/de/Rock → Rock Genre (Deutsch)
  • http://localhost:3000/de/special/Weihnachtslieder → Special (Deutsch)

Architektur

Verzeichnisstruktur

app/
  [locale]/              # Lokalisierte Routen
    layout.tsx           # Root Layout mit i18n Provider
    page.tsx             # Homepage
    admin/
      page.tsx           # Admin Dashboard
    [genre]/
      page.tsx           # Genre-spezifische Seite
    special/
      [name]/
        page.tsx         # Special-Seite

i18n/
  request.ts             # next-intl Konfiguration

messages/
  de.json                # Deutsche Übersetzungen
  en.json                # Englische Übersetzungen

lib/
  i18n.ts                # Helper-Funktionen für lokalisierte DB-Werte
  navigation.ts          # Lokalisierte Navigation (Link, useRouter, etc.)

Übersetzungsdateien

Die Übersetzungen sind in JSON-Dateien unter messages/ organisiert:

{
  "Common": {
    "loading": "Laden...",
    "error": "Ein Fehler ist aufgetreten"
  },
  "Game": {
    "play": "Abspielen",
    "pause": "Pause",
    "won": "Gewonnen!"
  },
  "Home": {
    "welcome": "Willkommen bei Hördle"
  }
}

Datenbank-Schema

Die folgenden Modelle unterstützen mehrsprachige Felder:

Genre

  • name: JSON { "de": "Rock", "en": "Rock" }
  • subtitle: JSON { "de": "Klassischer Rock", "en": "Classic Rock" }

Special

  • name: JSON { "de": "Weihnachtslieder", "en": "Christmas Songs" }
  • subtitle: JSON { "de": "Festliche Musik", "en": "Festive Music" }

News

  • title: JSON { "de": "Neues Feature", "en": "New Feature" }
  • content: JSON { "de": "Markdown Inhalt...", "en": "Markdown content..." }

Helper-Funktionen

getLocalizedValue(value, locale, fallback?)

Extrahiert den lokalisierten Wert aus einem JSON-Objekt:

import { getLocalizedValue } from '@/lib/i18n';

const genreName = getLocalizedValue(genre.name, 'de'); // "Rock"
const genreNameEn = getLocalizedValue(genre.name, 'en'); // "Rock"

Fallback-Verhalten:

  1. Versucht die angeforderte Locale (de oder en)
  2. Fallback zu de falls nicht vorhanden
  3. Fallback zu en falls nicht vorhanden
  4. Fallback zum ersten verfügbaren Schlüssel
  5. Fallback zum übergebenen fallback-Parameter

createLocalizedObject(de, en?)

Erstellt ein lokalisiertes Objekt:

import { createLocalizedObject } from '@/lib/i18n';

const name = createLocalizedObject('Rock', 'Rock');
// { de: "Rock", en: "Rock" }

Verwendung in Komponenten

Server Components

import { getTranslations } from 'next-intl/server';
import { getLocalizedValue } from '@/lib/i18n';

export default async function Page({ params }: { params: { locale: string } }) {
  const { locale } = await params;
  const t = await getTranslations('Home');
  
  const genreName = getLocalizedValue(genre.name, locale);
  
  return <h1>{t('welcome')}</h1>;
}

Client Components

'use client';
import { useTranslations } from 'next-intl';
import { useLocale } from 'next-intl';

export default function Game() {
  const t = useTranslations('Game');
  const locale = useLocale();
  
  return <button>{t('play')}</button>;
}

Navigation

Verwende die lokalisierte Navigation aus lib/navigation.ts:

import { Link } from '@/lib/navigation';

// Automatisch lokalisiert
<Link href="/admin">Admin</Link>
<Link href="/Rock">Rock</Link>

Admin-Interface

Das Admin-Dashboard unterstützt mehrsprachige Eingaben:

  1. Sprach-Tabs: Wechsle zwischen DE und EN Tabs
  2. Genre/Special/News: Alle Felder können in beiden Sprachen bearbeitet werden
  3. Vorschau: Sieh dir die lokalisierte Version direkt an

Beispiel: Genre erstellen

  1. Öffne /de/admin
  2. Wähle den DE Tab
  3. Gib Name und Subtitle ein
  4. Wechsle zum EN Tab
  5. Gib die englischen Übersetzungen ein
  6. Speichere

Migration bestehender Daten

Bestehende Daten werden automatisch migriert:

  1. Migration 20251128131405_add_i18n_columns: Fügt neue JSON-Spalten hinzu
  2. Migration 20251128132806_switch_to_json_columns: Konvertiert String-Spalten zu JSON

Wichtig: Alte String-Werte werden automatisch in beide Sprachen kopiert:

  • "Rock"{ "de": "Rock", "en": "Rock" }

Middleware

Die Middleware (middleware.ts) leitet Anfragen automatisch um:

  • //de (Standard)
  • Ungültige Locales → 404
  • Validiert Locale-Parameter

Sprachumschalter

Die LanguageSwitcher-Komponente ermöglicht Nutzern, zwischen Sprachen zu wechseln:

import LanguageSwitcher from '@/components/LanguageSwitcher';

<LanguageSwitcher />

Die aktuelle Route bleibt erhalten, nur die Locale ändert sich:

  • /de/admin/en/admin
  • /de/Rock/en/Rock

API-Endpunkte

API-Routen unterstützen einen optionalen locale-Parameter:

GET /api/genres?locale=de
GET /api/specials?locale=en
GET /api/news?locale=de

Falls kein locale angegeben wird, wird de als Standard verwendet.

Best Practices

1. Immer getLocalizedValue verwenden

Falsch:

<span>{genre.name}</span> // Rendert { de: "...", en: "..." }

Richtig:

<span>{getLocalizedValue(genre.name, locale)}</span>

2. Übersetzungsschlüssel konsistent benennen

Verwende Namespaces für bessere Organisation:

  • Common.* - Allgemeine UI-Elemente
  • Game.* - Spiel-spezifische Texte
  • Home.* - Homepage-Texte
  • Navigation.* - Navigations-Elemente

3. Fallbacks definieren

Immer einen Fallback-Wert angeben:

const name = getLocalizedValue(genre.name, locale, 'Unbekannt');

4. Neue Übersetzungen hinzufügen

  1. Füge den Schlüssel zu messages/de.json hinzu
  2. Füge den Schlüssel zu messages/en.json hinzu
  3. Verwende useTranslations('Namespace') oder getTranslations('Namespace')

Troubleshooting

404-Fehler auf /de oder /en

Problem: Route wird nicht gefunden.

Lösung:

  1. Überprüfe, ob middleware.ts korrekt konfiguriert ist
  2. Stelle sicher, dass app/[locale]/layout.tsx existiert
  3. Prüfe die i18n/request.ts Konfiguration

"Objects are not valid as a React child"

Problem: Ein JSON-Objekt wird direkt gerendert statt des lokalisierten Werts.

Lösung: Verwende getLocalizedValue():

// ❌ Falsch
<span>{genre.name}</span>

// ✅ Richtig
<span>{getLocalizedValue(genre.name, locale)}</span>

Übersetzungen werden nicht angezeigt

Problem: Texte erscheinen als Schlüssel (z.B. "Game.play").

Lösung:

  1. Überprüfe, ob der Übersetzungsschlüssel in messages/de.json und messages/en.json existiert
  2. Stelle sicher, dass der Namespace korrekt ist: useTranslations('Game') für Game.play
  3. Prüfe die JSON-Syntax auf Fehler

Admin-Interface zeigt Objekte statt Text

Problem: In Dropdowns oder Listen werden { de: "...", en: "..." } angezeigt.

Lösung: Verwende getLocalizedValue() in allen Render-Funktionen:

// ❌ Falsch
<option value={s.id}>{s.name}</option>

// ✅ Richtig
<option value={s.id}>{getLocalizedValue(s.name, activeTab)}</option>

Erweiterung um weitere Sprachen

Um eine neue Sprache hinzuzufügen (z.B. Französisch):

  1. Übersetzungsdatei erstellen:

    cp messages/de.json messages/fr.json
    
  2. Übersetzungen hinzufügen: Bearbeite messages/fr.json mit französischen Übersetzungen

  3. Locale zur Konfiguration hinzufügen:

    • i18n/request.ts: const locales = ['en', 'de', 'fr'];
    • middleware.ts: locales: ['en', 'de', 'fr']
    • lib/navigation.ts: export const locales = ['de', 'en', 'fr'] as const;
  4. Layout aktualisieren:

    // app/[locale]/layout.tsx
    if (!['en', 'de', 'fr'].includes(locale)) {
      notFound();
    }
    
  5. LanguageSwitcher erweitern: Füge einen Button für fr hinzu

  6. Datenbank-Migration: Bestehende Daten behalten ihre Struktur, neue Einträge können optional fr enthalten

Weitere Ressourcen