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
- **🌍 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.
- **Inkrementelle Hinweise:** Startet mit 2 Sekunden, dann 4s, 7s, 11s, 16s, 30s, bis 60s (7 Versuche).
- **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.
- 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
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
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
@@ -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.
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.)
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
Einzelne Genres können direkt eingebunden werden:
Einzelne Genres können direkt eingebunden werden (mit Locale-Präfix):
```html
<!-- Rock Genre -->
<!-- Rock Genre (Deutsch) -->
<iframe
src="https://hoerdle.elpatron.me/Rock"
src="https://hoerdle.elpatron.me/de/Rock"
width="100%"
height="800"
frameborder="0"
@@ -223,9 +235,9 @@ Einzelne Genres können direkt eingebunden werden:
title="Hördle Rock Quiz">
</iframe>
<!-- Pop Genre -->
<!-- Pop Genre (Englisch) -->
<iframe
src="https://hoerdle.elpatron.me/Pop"
src="https://hoerdle.elpatron.me/en/Pop"
width="100%"
height="800"
frameborder="0"
@@ -239,8 +251,9 @@ Einzelne Genres können direkt eingebunden werden:
Auch thematische Specials können direkt eingebettet werden:
```html
<!-- Weihnachtslieder (Deutsch) -->
<iframe
src="https://hoerdle.elpatron.me/special/Weihnachtslieder"
src="https://hoerdle.elpatron.me/de/special/Weihnachtslieder"
width="100%"
height="800"
frameborder="0"