From d8746827649f63436093f1f9069347b0c7d77659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=B6rdle=20Bot?= Date: Fri, 28 Nov 2025 15:48:27 +0100 Subject: [PATCH] Dokumentiere i18n-Implementierung --- I18N.md | 349 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 29 +++-- 2 files changed, 370 insertions(+), 8 deletions(-) create mode 100644 I18N.md diff --git a/I18N.md b/I18N.md new file mode 100644 index 0000000..fdeb21b --- /dev/null +++ b/I18N.md @@ -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

{t('welcome')}

; +} +``` + +### 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 ; +} +``` + +### Navigation + +Verwende die lokalisierte Navigation aus `lib/navigation.ts`: + +```typescript +import { Link } from '@/lib/navigation'; + +// Automatisch lokalisiert +Admin +Rock +``` + +## 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'; + + +``` + +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 +{genre.name} // Rendert { de: "...", en: "..." } +``` + +✅ **Richtig:** +```typescript +{getLocalizedValue(genre.name, locale)} +``` + +### 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 +{genre.name} + +// ✅ Richtig +{getLocalizedValue(genre.name, locale)} +``` + +### Ü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 + + +// ✅ Richtig + +``` + +## 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) + diff --git a/README.md b/README.md index 6193f73..31f3c76 100644 --- a/README.md +++ b/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 - + - +