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:
2026-07-20 10:34:51 +02:00
co-authored by Claude Sonnet 5
parent 660490f1ac
commit 236f8c9490
24 changed files with 1548 additions and 633 deletions
+11 -3
View File
@@ -4,6 +4,7 @@ import "./globals.css";
import { getSettings } from "@/lib/db/settings";
import { COLOR_VARS, DEFAULT_COLORS, faviconUrl, SITE_TITLE } from "@/lib/types";
import { getDictionary, getLocale } from "@/lib/i18n";
import { listLanguages } from "@/lib/db/languages";
import { LocaleProvider } from "@/components/i18n/LocaleProvider";
import packageJson from "@/package.json";
@@ -41,8 +42,15 @@ export default async function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
const [settings, locale] = await Promise.all([getSettings(), getLocale()]);
const dict = getDictionary(locale);
const [settings, locale, allLanguages] = await Promise.all([
getSettings(),
getLocale(),
listLanguages(),
]);
const dict = await getDictionary(locale);
const languages = allLanguages
.filter((l) => l.active)
.map((l) => ({ code: l.code, label: l.label }));
const colors: Record<string, string> = {
...DEFAULT_COLORS,
@@ -55,7 +63,7 @@ export default async function RootLayout({
return (
<html lang={locale} className={`${geistSans.variable} h-full antialiased`}>
<body style={style} className="min-h-full flex flex-col">
<LocaleProvider locale={locale} dict={dict}>
<LocaleProvider locale={locale} dict={dict} languages={languages}>
{children}
</LocaleProvider>
<footer className="mt-auto px-4 py-2 text-right text-xs text-gray-400 select-none">