350 lines
8.8 KiB
Markdown
350 lines
8.8 KiB
Markdown
# Internationalisierung (i18n) Dokumentation
|
|
|
|
Hördle unterstützt vollständige Mehrsprachigkeit (Internationalisierung) für Deutsch und Englisch.
|
|
|
|
## Übersicht
|
|
|
|
Die i18n-Implementierung basiert auf [next-intl](https://next-intl-docs.vercel.app/) 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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
import { createLocalizedObject } from '@/lib/i18n';
|
|
|
|
const name = createLocalizedObject('Rock', 'Rock');
|
|
// { de: "Rock", en: "Rock" }
|
|
```
|
|
|
|
## Verwendung in Komponenten
|
|
|
|
### Server Components
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
'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`:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
<span>{genre.name}</span> // Rendert { de: "...", en: "..." }
|
|
```
|
|
|
|
✅ **Richtig:**
|
|
```typescript
|
|
<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:
|
|
|
|
```typescript
|
|
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()`:
|
|
|
|
```typescript
|
|
// ❌ 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:
|
|
|
|
```typescript
|
|
// ❌ 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:**
|
|
```bash
|
|
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:**
|
|
```typescript
|
|
// 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
|
|
|
|
- [next-intl Dokumentation](https://next-intl-docs.vercel.app/)
|
|
- [Next.js App Router i18n](https://nextjs.org/docs/app/building-your-application/routing/internationalization)
|
|
|