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>
61 lines
1.7 KiB
TypeScript
61 lines
1.7 KiB
TypeScript
"use client";
|
|
|
|
import { useRouter } from "next/navigation";
|
|
import { useState } from "react";
|
|
import { useLanguages, useLocale } from "@/components/i18n/LocaleProvider";
|
|
|
|
export default function LanguageSwitcher({
|
|
className = "",
|
|
variant = "dark",
|
|
}: {
|
|
className?: string;
|
|
variant?: "dark" | "light";
|
|
}) {
|
|
const locale = useLocale();
|
|
const languages = useLanguages();
|
|
const router = useRouter();
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
async function change(next: string) {
|
|
if (next === locale || busy) return;
|
|
setBusy(true);
|
|
await fetch("/api/locale", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ locale: next }),
|
|
});
|
|
router.refresh();
|
|
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 =
|
|
variant === "dark"
|
|
? "text-white/60 hover:text-white hover:bg-white/10"
|
|
: "text-gray-400 hover:text-gray-700 hover:bg-gray-100";
|
|
|
|
return (
|
|
<div className={`flex gap-1 text-xs ${className}`} aria-label="Sprache">
|
|
{languages.map((l) => (
|
|
<button
|
|
key={l.code}
|
|
type="button"
|
|
onClick={() => change(l.code)}
|
|
disabled={busy}
|
|
aria-current={l.code === locale}
|
|
title={l.label}
|
|
className={`rounded px-1.5 py-0.5 uppercase font-semibold transition-colors ${
|
|
l.code === locale ? activeCls : inactiveCls
|
|
}`}
|
|
>
|
|
{l.code}
|
|
</button>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|