feat: Dynamisches Sprachpaket-System für die Bedienoberfläche
Teil 1 des "mehrsprachig variabel"-Umbaus (Speiseplan-Inhalte folgen
in einer späteren Version). Sprachen sind jetzt zur Laufzeit über eine
neue Admin-Seite "Sprachen" verwaltbar statt fest im Code zu stehen:
- Neue Tabellen languages/ui_translations (Migration 0004), bestehende
Englisch-/Polnisch-Texte per generiertem Seed automatisch übernommen
(Migration 0005) - keine manuelle Nacharbeit, nichts geht verloren.
- lib/i18n/flatten.ts: verschachtelte Dict-Objekte (inkl. Arrays wie
weekdays.long) <-> flache Schlüssel-Wert-Paare, Basis für Vorlage-
Export/-Import.
- lib/i18n/template.ts: Vollständigkeits-/Veraltet-Berechnung (Hash
über die DE-Schlüsselliste) und Import-Validierung (Platzhalter wie
{message} müssen erhalten bleiben, unbekannte Schlüssel werden
ignoriert statt Fehler zu werfen).
- getDictionary()/getLocale() (lib/i18n/index.ts) jetzt asynchron und
DB-gestützt für Nicht-Basis-Sprachen, mit Fallback auf Deutsch für
fehlende Schlüssel. Locale-Typ bewusst von festem TypeScript-Tupel
auf string generalisiert (Kompromiss: Compiler prüft Vollständigkeit
nicht mehr automatisch, das übernimmt jetzt die Veraltet-Erkennung
zur Laufzeit).
- LanguageSwitcher/LocaleProvider lesen aktive Sprachen jetzt aus dem
Context statt aus einer festen Konstante; blendet sich bei nur einer
aktiven Sprache automatisch aus.
- Neue Admin-Routen app/api/admin/languages/* (Liste inkl.
Vollständigkeit, Anlegen, Aktivieren/Deaktivieren mit
"mindestens eine muss aktiv bleiben"-Schutz, Löschen außer
Basis-Sprache, Vorlage-Download, Datei-Import) + LanguageManager.tsx.
Vollständig gegen echten DB-Zustand getestet: Vorlage-Export/-Import
inkl. Platzhalter-Validierung, neue Sprache anlegen, auf eine Sprache
reduzieren (Umschalter verschwindet, "letzte Sprache"-Schutz greift),
Veraltet-Erkennung nach simuliertem neuen Schlüssel, Cascade-Delete.
Version 1.10.0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+26
-5
@@ -1,16 +1,37 @@
|
||||
// Nur für Server-Komponenten/Routen importieren (next/headers)!
|
||||
// Nur für Server-Komponenten/Routen importieren (next/headers, DB)!
|
||||
// Client-Komponenten müssen stattdessen "@/lib/i18n/shared" verwenden,
|
||||
// sonst landet next/headers im Client-Bundle.
|
||||
// sonst landet next/headers/die DB-Anbindung im Client-Bundle.
|
||||
|
||||
import "server-only";
|
||||
import { cookies } from "next/headers";
|
||||
import { listActiveLanguageCodes, listTranslations } from "@/lib/db/languages";
|
||||
import { dictionaries, type Dict } from "./dictionaries";
|
||||
import { flattenDict, unflattenDict } from "./flatten";
|
||||
import { DEFAULT_LOCALE, isLocale, type Locale } from "./locales";
|
||||
import { LOCALE_COOKIE } from "./shared";
|
||||
|
||||
export * from "./shared";
|
||||
|
||||
/** Aktuelle Sprache aus dem Cookie lesen (Server-Komponenten). */
|
||||
/** Aktuelle Sprache aus dem Cookie lesen, validiert gegen die aktiven
|
||||
* Sprachen (Server-Komponenten). Ungültig oder nicht mehr aktiv →
|
||||
* Basis-Sprache. */
|
||||
export async function getLocale(): Promise<Locale> {
|
||||
const store = await cookies();
|
||||
const value = store.get("locale")?.value;
|
||||
return isLocale(value) ? value : DEFAULT_LOCALE;
|
||||
const value = store.get(LOCALE_COOKIE)?.value;
|
||||
const active = await listActiveLanguageCodes();
|
||||
return isLocale(value, active) ? value : DEFAULT_LOCALE;
|
||||
}
|
||||
|
||||
/** Wörterbuch für eine Sprache. Nicht-deutsche Sprachen werden aus der
|
||||
* Datenbank geladen und mit der deutschen Vorlage aufgefüllt – fehlt
|
||||
* eine Übersetzung (z. B. nach einem Update mit neuen Texten), zeigt
|
||||
* die App automatisch den deutschen Text statt leer zu bleiben. */
|
||||
export async function getDictionary(locale: Locale): Promise<Dict> {
|
||||
if (locale === DEFAULT_LOCALE) return dictionaries.de;
|
||||
const rows = await listTranslations(locale);
|
||||
const flat = flattenDict(dictionaries.de);
|
||||
for (const row of rows) {
|
||||
if (row.key in flat) flat[row.key] = row.value;
|
||||
}
|
||||
return unflattenDict(flat) as Dict;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user