Dokumentiere i18n-Implementierung
This commit is contained in:
349
I18N.md
Normal file
349
I18N.md
Normal 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)
|
||||
|
||||
29
README.md
29
README.md
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user