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>
53 lines
1.7 KiB
TypeScript
53 lines
1.7 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { getLanguage, importTranslations } from "@/lib/db/languages";
|
|
import { currentTemplateHash, validateImport } from "@/lib/i18n/template";
|
|
import { ForbiddenError, requireRole, UnauthorizedError } from "@/lib/auth/session";
|
|
import { isSameOrigin } from "@/lib/origin";
|
|
|
|
export async function POST(
|
|
request: Request,
|
|
{ params }: { params: Promise<{ code: string }> }
|
|
) {
|
|
try {
|
|
if (!isSameOrigin(request)) throw new ForbiddenError();
|
|
await requireRole(["admin"]);
|
|
} catch (err) {
|
|
if (err instanceof UnauthorizedError || err instanceof ForbiddenError) {
|
|
return NextResponse.json({ error: "Nicht berechtigt." }, { status: 403 });
|
|
}
|
|
throw err;
|
|
}
|
|
|
|
const { code } = await params;
|
|
const language = await getLanguage(code);
|
|
if (!language) {
|
|
return NextResponse.json({ error: "not_found" }, { status: 404 });
|
|
}
|
|
if (language.is_base) {
|
|
return NextResponse.json({ error: "cannot_import_base" }, { status: 400 });
|
|
}
|
|
|
|
const form = await request.formData();
|
|
const file = form.get("file");
|
|
if (!(file instanceof File)) {
|
|
return NextResponse.json({ error: "invalid_input" }, { status: 400 });
|
|
}
|
|
|
|
let parsed: unknown;
|
|
try {
|
|
parsed = JSON.parse(await file.text());
|
|
} catch {
|
|
return NextResponse.json({ error: "invalid_json" }, { status: 400 });
|
|
}
|
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
return NextResponse.json({ error: "invalid_json" }, { status: 400 });
|
|
}
|
|
|
|
const { entries, skipped } = validateImport(parsed as Record<string, unknown>);
|
|
if (entries.length > 0) {
|
|
await importTranslations(code, entries, currentTemplateHash());
|
|
}
|
|
|
|
return NextResponse.json({ imported: entries.length, skipped });
|
|
}
|