DeepL-Anbindung für automatische Übersetzung

app/api/translate ruft DeepL für EN+PL auf und schreibt das Ergebnis
in die jeweilige _en/_pl-Spalte (Staff/Admin-only, Service-Role-Key).
Ohne DEEPL_API_KEY (neue Docker-Variable) antwortet die Route mit
configured:false, das Frontend zeigt dann den Hinweis zur manuellen
Eingabe statt eines Fehlers.
This commit is contained in:
2026-07-18 02:08:02 +02:00
parent 1cc19de221
commit 30a02ba7b7
3 changed files with 111 additions and 0 deletions
+6
View File
@@ -19,3 +19,9 @@ APP_URL=http://localhost:3000
# Titel im Browser-Tab und Überschrift, solange kein Logo hochgeladen ist.
SITE_TITLE=Canteen Menu
# Optional: DeepL-API-Key für automatische Übersetzung von Gerichten nach
# Englisch/Polnisch (kostenloser Free-Tier unter deepl.com/de/pro-api,
# Free-Keys enden auf ":fx"). Ohne Key bleibt "Automatisch übersetzen"
# inaktiv; Englisch/Polnisch lassen sich weiterhin manuell eintragen.
DEEPL_API_KEY=
+100
View File
@@ -0,0 +1,100 @@
import { NextResponse } from "next/server";
import { createClient } from "@/lib/supabase/server";
import { createAdminClient } from "@/lib/supabase/admin";
import { isSameOrigin } from "@/lib/origin";
// Übersetzt einen deutschen Text nach Englisch/Polnisch via DeepL und
// schreibt das Ergebnis in die _en/_pl-Spalte der angegebenen Zeile.
// Ohne DEEPL_API_KEY läuft die Route kontrolliert ins Leere (configured:
// false), damit das UI die manuelle Eingabe anbieten kann.
const ALLOWED_TABLES: Record<string, { idColumn: string; textColumn: string }> = {
dishes: { idColumn: "id", textColumn: "name" },
sections: { idColumn: "key", textColumn: "label" },
allergens: { idColumn: "code", textColumn: "name" },
additives: { idColumn: "code", textColumn: "name" },
};
async function translateWithDeepL(text: string, targetLang: "EN" | "PL", 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 });
}
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) return NextResponse.json({ error: "Nicht angemeldet." }, { status: 401 });
const admin = createAdminClient();
const { data: profile } = await admin
.from("profiles")
.select("role")
.eq("id", user.id)
.single();
if (!profile || !["staff", "admin"].includes(profile.role)) {
return NextResponse.json({ error: "Nicht berechtigt." }, { status: 403 });
}
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 });
}
try {
const [en, pl] = await Promise.all([
translateWithDeepL(text, "EN", apiKey),
translateWithDeepL(text, "PL", apiKey),
]);
if (!en || !pl) throw new Error("Keine Übersetzung erhalten.");
if (persist) {
const { error } = await admin
.from(table)
.update({ [`${config.textColumn}_en`]: en, [`${config.textColumn}_pl`]: pl })
.eq(config.idColumn, id);
if (error) throw new Error(error.message);
}
return NextResponse.json({ configured: true, en, pl });
} catch (err) {
return NextResponse.json(
{ error: err instanceof Error ? err.message : "Übersetzung fehlgeschlagen." },
{ status: 502 }
);
}
}
+5
View File
@@ -36,6 +36,10 @@ services:
APP_URL: ${APP_URL:-http://localhost:3000}
# Titel im Browser-Tab und Überschrift, solange kein Logo hochgeladen ist.
SITE_TITLE: ${SITE_TITLE:-Canteen Menu}
# Optional: DeepL-API-Key für automatische Übersetzung von Gerichten
# (Kürzel-Erklärungen, Bereichen) nach Englisch/Polnisch. Ohne Key
# bleibt "Automatisch übersetzen" inaktiv, manuelle Eingabe geht immer.
DEEPL_API_KEY: ${DEEPL_API_KEY:-}
ports:
- "0.0.0.0:3000:3000"
restart: unless-stopped
@@ -71,6 +75,7 @@ services:
SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY}
APP_URL: ${APP_URL:-http://localhost:3000}
SITE_TITLE: ${SITE_TITLE:-Canteen Menu}
DEEPL_API_KEY: ${DEEPL_API_KEY:-}
ports:
- "0.0.0.0:3000:3000"
volumes: