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>
47 lines
1.5 KiB
TypeScript
47 lines
1.5 KiB
TypeScript
// Reine, abhängigkeitsfreie Umwandlung zwischen dem verschachtelten
|
||
// Dict-Objekt (lib/i18n/dictionaries.ts) und flachen Schlüssel-Wert-
|
||
// Paaren ("login.errorGeneric") – Grundlage für Vorlage-Export/-Import
|
||
// von Sprachpaketen. Arrays (z. B. weekdays.long) werden über den
|
||
// numerischen Index als eigene Schlüssel geflattet ("weekdays.long.0")
|
||
// und beim Zurückbauen anhand rein numerischer Kind-Schlüssel wieder
|
||
// als Array erkannt.
|
||
|
||
export function flattenDict(value: unknown, prefix = ""): Record<string, string> {
|
||
const out: Record<string, string> = {};
|
||
|
||
function walk(node: unknown, path: string) {
|
||
if (Array.isArray(node)) {
|
||
node.forEach((item, i) => walk(item, `${path}.${i}`));
|
||
} else if (node !== null && typeof node === "object") {
|
||
for (const [key, child] of Object.entries(node)) {
|
||
walk(child, path ? `${path}.${key}` : key);
|
||
}
|
||
} else {
|
||
out[path] = String(node);
|
||
}
|
||
}
|
||
|
||
walk(value, prefix);
|
||
return out;
|
||
}
|
||
|
||
export function unflattenDict(flat: Record<string, string>): unknown {
|
||
const root: Record<string, unknown> = {};
|
||
|
||
for (const [path, value] of Object.entries(flat)) {
|
||
const parts = path.split(".");
|
||
let node: Record<string, unknown> = root;
|
||
for (let i = 0; i < parts.length - 1; i++) {
|
||
const key = parts[i];
|
||
if (node[key] === undefined) {
|
||
const nextIsIndex = /^\d+$/.test(parts[i + 1]);
|
||
node[key] = nextIsIndex ? [] : {};
|
||
}
|
||
node = node[key] as Record<string, unknown>;
|
||
}
|
||
node[parts[parts.length - 1]] = value;
|
||
}
|
||
|
||
return root;
|
||
}
|