Dokumentiere i18n-Implementierung

This commit is contained in:
Hördle Bot
2025-11-28 15:48:27 +01:00
parent 771d0d06f3
commit d874682764
2 changed files with 370 additions and 8 deletions

349
I18N.md Normal file
View File

@@ -0,0 +1,349 @@
# 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)

View File

@@ -4,6 +4,7 @@ Eine Web-App inspiriert von Heardle, bei der Nutzer täglich einen Song anhand k
## Features ## Features
- **🌍 Mehrsprachigkeit (i18n):** Vollständige Unterstützung für Deutsch und Englisch mit automatischer Sprachumleitung und lokalisierten Inhalten.
- **Tägliches Rätsel:** Jeden Tag ein neuer Song für alle Nutzer. - **Tägliches Rätsel:** Jeden Tag ein neuer Song für alle Nutzer.
- **Inkrementelle Hinweise:** Startet mit 2 Sekunden, dann 4s, 7s, 11s, 16s, 30s, bis 60s (7 Versuche). - **Inkrementelle Hinweise:** Startet mit 2 Sekunden, dann 4s, 7s, 11s, 16s, 30s, bis 60s (7 Versuche).
- **Admin Dashboard:** - **Admin Dashboard:**
@@ -51,6 +52,17 @@ Eine Web-App inspiriert von Heardle, bei der Nutzer täglich einen Song anhand k
- Special-Verknüpfung: Direkte Links zu Specials in News-Beiträgen. - Special-Verknüpfung: Direkte Links zu Specials in News-Beiträgen.
- Verwaltung über das Admin-Dashboard. - Verwaltung über das Admin-Dashboard.
## Internationalisierung (i18n)
Hördle unterstützt vollständige Mehrsprachigkeit für Deutsch und Englisch.
👉 **[Vollständige i18n-Dokumentation](I18N.md)**
**Schnellstart:**
- Deutsche Version: `http://localhost:3000/de`
- Englische Version: `http://localhost:3000/en`
- Root (`/`) leitet automatisch zur Standardsprache (Deutsch) um
## White Labeling ## White Labeling
Hördle ist "White Label Ready". Das bedeutet, du kannst das Branding (Name, Farben, Logos) komplett anpassen, ohne den Code zu ändern. Hördle ist "White Label Ready". Das bedeutet, du kannst das Branding (Name, Farben, Logos) komplett anpassen, ohne den Code zu ändern.
@@ -103,7 +115,7 @@ Das Ziel ist es, den Song mit so wenigen Hinweisen wie möglich zu erraten und d
```bash ```bash
npm run dev npm run dev
``` ```
Die App läuft unter `http://localhost:3000`. Die App läuft unter `http://localhost:3000` (leitet automatisch zu `/de` um).
## Deployment mit Docker ## Deployment mit Docker
@@ -139,7 +151,7 @@ Das Projekt ist für den Betrieb mit Docker optimiert.
- Beim Start des Containers wird automatisch ein Migrations-Skript ausgeführt, das fehlende Cover-Bilder aus den MP3s extrahiert. - Beim Start des Containers wird automatisch ein Migrations-Skript ausgeführt, das fehlende Cover-Bilder aus den MP3s extrahiert.
4. **Admin-Zugang:** 4. **Admin-Zugang:**
- URL: `/admin` - URL: `/de/admin` oder `/en/admin`
- Standard-Passwort: `admin123` (Bitte in `docker-compose.yml` ändern! Muss als Hash hinterlegt werden.) - Standard-Passwort: `admin123` (Bitte in `docker-compose.yml` ändern! Muss als Hash hinterlegt werden.)
5. **Special Curation & Scheduling verwenden:** 5. **Special Curation & Scheduling verwenden:**
@@ -210,12 +222,12 @@ Hördle kann problemlos als iFrame in andere Webseiten eingebettet werden. Die A
### Genre-spezifische Einbindung ### Genre-spezifische Einbindung
Einzelne Genres können direkt eingebunden werden: Einzelne Genres können direkt eingebunden werden (mit Locale-Präfix):
```html ```html
<!-- Rock Genre --> <!-- Rock Genre (Deutsch) -->
<iframe <iframe
src="https://hoerdle.elpatron.me/Rock" src="https://hoerdle.elpatron.me/de/Rock"
width="100%" width="100%"
height="800" height="800"
frameborder="0" frameborder="0"
@@ -223,9 +235,9 @@ Einzelne Genres können direkt eingebunden werden:
title="Hördle Rock Quiz"> title="Hördle Rock Quiz">
</iframe> </iframe>
<!-- Pop Genre --> <!-- Pop Genre (Englisch) -->
<iframe <iframe
src="https://hoerdle.elpatron.me/Pop" src="https://hoerdle.elpatron.me/en/Pop"
width="100%" width="100%"
height="800" height="800"
frameborder="0" frameborder="0"
@@ -239,8 +251,9 @@ Einzelne Genres können direkt eingebunden werden:
Auch thematische Specials können direkt eingebettet werden: Auch thematische Specials können direkt eingebettet werden:
```html ```html
<!-- Weihnachtslieder (Deutsch) -->
<iframe <iframe
src="https://hoerdle.elpatron.me/special/Weihnachtslieder" src="https://hoerdle.elpatron.me/de/special/Weihnachtslieder"
width="100%" width="100%"
height="800" height="800"
frameborder="0" frameborder="0"