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:
@@ -2,9 +2,7 @@
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { LOCALES, LOCALE_LABELS } from "@/lib/i18n/shared";
|
||||
import type { Locale } from "@/lib/i18n/shared";
|
||||
import { useLocale } from "@/components/i18n/LocaleProvider";
|
||||
import { useLanguages, useLocale } from "@/components/i18n/LocaleProvider";
|
||||
|
||||
export default function LanguageSwitcher({
|
||||
className = "",
|
||||
@@ -14,10 +12,11 @@ export default function LanguageSwitcher({
|
||||
variant?: "dark" | "light";
|
||||
}) {
|
||||
const locale = useLocale();
|
||||
const languages = useLanguages();
|
||||
const router = useRouter();
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
async function change(next: Locale) {
|
||||
async function change(next: string) {
|
||||
if (next === locale || busy) return;
|
||||
setBusy(true);
|
||||
await fetch("/api/locale", {
|
||||
@@ -29,6 +28,9 @@ export default function LanguageSwitcher({
|
||||
setBusy(false);
|
||||
}
|
||||
|
||||
// Bei nur einer aktiven Sprache gibt es nichts umzuschalten.
|
||||
if (languages.length <= 1) return null;
|
||||
|
||||
const activeCls =
|
||||
variant === "dark" ? "bg-white/25 text-white" : "bg-gray-200 text-gray-900";
|
||||
const inactiveCls =
|
||||
@@ -38,19 +40,19 @@ export default function LanguageSwitcher({
|
||||
|
||||
return (
|
||||
<div className={`flex gap-1 text-xs ${className}`} aria-label="Sprache">
|
||||
{LOCALES.map((l) => (
|
||||
{languages.map((l) => (
|
||||
<button
|
||||
key={l}
|
||||
key={l.code}
|
||||
type="button"
|
||||
onClick={() => change(l)}
|
||||
onClick={() => change(l.code)}
|
||||
disabled={busy}
|
||||
aria-current={l === locale}
|
||||
title={LOCALE_LABELS[l]}
|
||||
aria-current={l.code === locale}
|
||||
title={l.label}
|
||||
className={`rounded px-1.5 py-0.5 uppercase font-semibold transition-colors ${
|
||||
l === locale ? activeCls : inactiveCls
|
||||
l.code === locale ? activeCls : inactiveCls
|
||||
}`}
|
||||
>
|
||||
{l}
|
||||
{l.code}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user