diff --git a/.env.example b/.env.example index 4a2fa4f..00f35de 100644 --- a/.env.example +++ b/.env.example @@ -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= diff --git a/app/api/translate/route.ts b/app/api/translate/route.ts new file mode 100644 index 0000000..f0c916c --- /dev/null +++ b/app/api/translate/route.ts @@ -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 = { + 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 } + ); + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 04a512e..61811cd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: