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.
116 lines
4.3 KiB
TypeScript
116 lines
4.3 KiB
TypeScript
import { NextResponse } from "next/server";
|
||
import { listActiveLanguageCodes } from "@/lib/db/languages";
|
||
import { replaceTranslations, type TranslationTable } from "@/lib/db/translations";
|
||
import { requireRole, UnauthorizedError, ForbiddenError } from "@/lib/auth/session";
|
||
import { isSameOrigin } from "@/lib/origin";
|
||
|
||
// Übersetzt einen deutschen Text via DeepL in alle aktiven, von DeepL
|
||
// unterstützten Sprachen und schreibt das Ergebnis in die passende
|
||
// *_translations-Tabelle. Ohne DEEPL_API_KEY läuft die Route kontrolliert
|
||
// ins Leere (configured: false), damit das UI die manuelle Eingabe anbietet.
|
||
|
||
const ALLOWED_TABLES: Record<string, { idColumn: string; translationTable: TranslationTable }> = {
|
||
dishes: { idColumn: "id", translationTable: "dish_translations" },
|
||
sections: { idColumn: "key", translationTable: "section_translations" },
|
||
allergens: { idColumn: "code", translationTable: "allergen_translations" },
|
||
additives: { idColumn: "code", translationTable: "additive_translations" },
|
||
};
|
||
|
||
// Unsere Sprachcodes -> DeepL target_lang. Sprachen ohne Eintrag hier sind
|
||
// von DeepL nicht unterstützt und bleiben bei manueller Eingabe.
|
||
const DEEPL_TARGET_LANG: Record<string, string> = {
|
||
en: "EN", pl: "PL", fr: "FR", es: "ES", it: "IT", nl: "NL", pt: "PT-PT",
|
||
ru: "RU", ja: "JA", zh: "ZH", cs: "CS", da: "DA", el: "EL", fi: "FI",
|
||
hu: "HU", id: "ID", ko: "KO", nb: "NB", ro: "RO", sk: "SK", sl: "SL",
|
||
sv: "SV", tr: "TR", uk: "UK", bg: "BG", et: "ET", lv: "LV", lt: "LT",
|
||
};
|
||
|
||
async function translateWithDeepL(text: string, targetLang: string, apiKey: string) {
|
||
const host = apiKey.endsWith(":fx")
|
||
? "https://api-free.deepl.com"
|
||
: "https://api.deepl.com";
|
||
const res = await fetch(`${host}/v2/translate`, {
|
||
method: "POST",
|
||
headers: {
|
||
Authorization: `DeepL-Auth-Key ${apiKey}`,
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify({
|
||
text: [text],
|
||
target_lang: targetLang,
|
||
source_lang: "DE",
|
||
}),
|
||
});
|
||
if (!res.ok) {
|
||
throw new Error(`DeepL antwortete mit ${res.status}: ${await res.text()}`);
|
||
}
|
||
const data = await res.json();
|
||
return data.translations?.[0]?.text as string | undefined;
|
||
}
|
||
|
||
export async function POST(request: Request) {
|
||
if (!isSameOrigin(request)) {
|
||
return NextResponse.json({ error: "Nicht erlaubt." }, { status: 403 });
|
||
}
|
||
|
||
try {
|
||
await requireRole(["staff", "admin"]);
|
||
} catch (err) {
|
||
if (err instanceof UnauthorizedError) {
|
||
return NextResponse.json({ error: "Nicht angemeldet." }, { status: 401 });
|
||
}
|
||
if (err instanceof ForbiddenError) {
|
||
return NextResponse.json({ error: "Nicht berechtigt." }, { status: 403 });
|
||
}
|
||
throw err;
|
||
}
|
||
|
||
const body = await request.json().catch(() => null);
|
||
const { table, id, text } = body ?? {};
|
||
const config = ALLOWED_TABLES[table];
|
||
if (!config || typeof text !== "string" || !text.trim()) {
|
||
return NextResponse.json({ error: "Ungültige Eingaben." }, { status: 400 });
|
||
}
|
||
// Bei einem noch nicht gespeicherten (neuen) Eintrag gibt es keine
|
||
// Zeile zum Aktualisieren – nur übersetzen, das Speichern übernimmt
|
||
// anschließend das reguläre Formular.
|
||
const persist = typeof id === "string" && id.length > 0;
|
||
|
||
const apiKey = process.env.DEEPL_API_KEY;
|
||
if (!apiKey) {
|
||
return NextResponse.json({ configured: false });
|
||
}
|
||
|
||
const activeCodes = (await listActiveLanguageCodes()).filter((c) => c !== "de");
|
||
const targets = activeCodes
|
||
.map((code) => ({ code, target: DEEPL_TARGET_LANG[code.toLowerCase()] }))
|
||
.filter((t): t is { code: string; target: string } => Boolean(t.target));
|
||
|
||
try {
|
||
const results = await Promise.all(
|
||
targets.map(async (t) => ({
|
||
code: t.code,
|
||
text: await translateWithDeepL(text, t.target, apiKey),
|
||
}))
|
||
);
|
||
const translations: Record<string, string> = {};
|
||
for (const r of results) {
|
||
if (r.text) translations[r.code] = r.text;
|
||
}
|
||
if (Object.keys(translations).length === 0) {
|
||
throw new Error("Keine Übersetzung erhalten.");
|
||
}
|
||
|
||
if (persist) {
|
||
await replaceTranslations(config.translationTable, id, translations);
|
||
}
|
||
|
||
return NextResponse.json({ configured: true, translations });
|
||
} catch (err) {
|
||
return NextResponse.json(
|
||
{ error: err instanceof Error ? err.message : "Übersetzung fehlgeschlagen." },
|
||
{ status: 502 }
|
||
);
|
||
}
|
||
}
|