feat: Speiseplan-Inhalte variabel mehrsprachig (Teil 2)

Gerichtnamen, Essenszeiten und Allergen-/Zusatzstoff-Bezeichnungen sind
jetzt genauso dynamisch mehrsprachig wie die Bedienoberfläche aus
v1.10.0, statt fest auf Englisch/Polnisch begrenzt zu sein. Neue
Übersetzungstabellen ersetzen die alten name_en/name_pl-Spalten,
DeepL-Auto-Übersetzung und CSV-Import/-Export unterstützen jetzt
beliebig viele aktive Sprachen.
This commit is contained in:
2026-07-20 11:10:06 +02:00
parent 0d69d77d4e
commit cd8b0fab9f
24 changed files with 723 additions and 290 deletions
+8 -3
View File
@@ -1,14 +1,19 @@
import { listCodes } from "@/lib/db/code-lists";
import { listActiveLanguageCodes } from "@/lib/db/languages";
import { authErrorResponse, requireRole } from "@/lib/auth/session";
import { toCsv } from "@/lib/csv";
export async function GET() {
try {
await requireRole(["staff", "admin"]);
const items = await listCodes("allergens");
const [items, activeCodes] = await Promise.all([
listCodes("allergens"),
listActiveLanguageCodes(),
]);
const langCodes = activeCodes.filter((c) => c !== "de");
const rows = [
["code", "name", "name_en", "name_pl"],
...items.map((i) => [i.code, i.name, i.name_en ?? "", i.name_pl ?? ""]),
["code", "name", ...langCodes.map((c) => `name_${c}`)],
...items.map((i) => [i.code, i.name, ...langCodes.map((c) => i.translations[c] ?? "")]),
];
return new Response(toCsv(rows), {
headers: {
+11 -8
View File
@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { importCodes, type CodeImportRow } from "@/lib/db/code-lists";
import { listActiveLanguageCodes } from "@/lib/db/languages";
import { authErrorResponse, requireRole } from "@/lib/auth/session";
import { isSameOrigin } from "@/lib/origin";
import { parseCsvWithHeader } from "@/lib/csv";
@@ -22,8 +23,10 @@ export async function POST(request: Request) {
if (codeIdx === -1 || nameIdx === -1) {
return NextResponse.json({ error: "missing_columns" }, { status: 400 });
}
const nameEnIdx = header.indexOf("name_en");
const namePlIdx = header.indexOf("name_pl");
const activeCodes = (await listActiveLanguageCodes()).filter((c) => c !== "de");
const langCols = activeCodes
.map((code) => ({ code, idx: header.indexOf(`name_${code}`) }))
.filter((c) => c.idx !== -1);
const rows: CodeImportRow[] = [];
const skipped: { row: number; reason: string }[] = [];
@@ -35,12 +38,12 @@ export async function POST(request: Request) {
skipped.push({ row: rowNum, reason: "Kürzel oder Bezeichnung fehlt" });
return;
}
rows.push({
code,
name,
name_en: nameEnIdx >= 0 ? c[nameEnIdx]?.trim() || null : null,
name_pl: namePlIdx >= 0 ? c[namePlIdx]?.trim() || null : null,
});
const translations: Record<string, string> = {};
for (const { code: lang, idx } of langCols) {
const value = c[idx]?.trim();
if (value) translations[lang] = value;
}
rows.push({ code, name, translations });
});
const result = rows.length > 0 ? await importCodes("allergens", rows) : { created: 0, updated: 0 };