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:
@@ -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("additives");
|
||||
const [items, activeCodes] = await Promise.all([
|
||||
listCodes("additives"),
|
||||
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: {
|
||||
|
||||
@@ -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("additives", rows) : { created: 0, updated: 0 };
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { query, queryOne } from "@/lib/db";
|
||||
import { loadTranslationsFor, replaceTranslations } from "@/lib/db/translations";
|
||||
import { authErrorResponse, requireRole } from "@/lib/auth/session";
|
||||
import { isSameOrigin } from "@/lib/origin";
|
||||
import type { Dish } from "@/lib/types";
|
||||
|
||||
interface DishRow {
|
||||
id: string;
|
||||
name: string;
|
||||
price: string;
|
||||
allergens: string[];
|
||||
additives: string[];
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
@@ -16,19 +25,26 @@ export async function PATCH(
|
||||
const { id } = await params;
|
||||
const body = (await request.json()) as {
|
||||
name: string;
|
||||
name_en: string | null;
|
||||
name_pl: string | null;
|
||||
price: number;
|
||||
allergens: string[];
|
||||
additives: string[];
|
||||
translations?: Record<string, string | null>;
|
||||
};
|
||||
const dish = await queryOne<Dish>(
|
||||
`update dishes set name = $1, name_en = $2, name_pl = $3, price = $4,
|
||||
allergens = $5, additives = $6
|
||||
where id = $7 returning *`,
|
||||
[body.name, body.name_en, body.name_pl, body.price, body.allergens, body.additives, id]
|
||||
const row = await queryOne<DishRow>(
|
||||
`update dishes set name = $1, price = $2, allergens = $3, additives = $4
|
||||
where id = $5 returning id, name, price, allergens, additives`,
|
||||
[body.name, body.price, body.allergens, body.additives, id]
|
||||
);
|
||||
if (!dish) return NextResponse.json({ error: "not_found" }, { status: 404 });
|
||||
if (!row) return NextResponse.json({ error: "not_found" }, { status: 404 });
|
||||
if (body.translations) {
|
||||
await replaceTranslations("dish_translations", row.id, body.translations);
|
||||
}
|
||||
const translations = await loadTranslationsFor("dish_translations", [row.id]);
|
||||
const dish: Dish = {
|
||||
...row,
|
||||
price: Number(row.price),
|
||||
translations: translations.get(row.id) ?? {},
|
||||
};
|
||||
return NextResponse.json(dish);
|
||||
} catch (err) {
|
||||
const res = authErrorResponse(err);
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
import { query } from "@/lib/db";
|
||||
import { loadTranslationsFor } from "@/lib/db/translations";
|
||||
import { listActiveLanguageCodes } from "@/lib/db/languages";
|
||||
import { authErrorResponse, requireRole } from "@/lib/auth/session";
|
||||
import { toCsv } from "@/lib/csv";
|
||||
import type { Dish } from "@/lib/types";
|
||||
|
||||
interface DishRow {
|
||||
id: string;
|
||||
name: string;
|
||||
price: string;
|
||||
allergens: string[];
|
||||
additives: string[];
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
await requireRole(["staff", "admin"]);
|
||||
const dishes = await query<Dish>("select * from dishes order by name");
|
||||
const [dishes, activeCodes] = await Promise.all([
|
||||
query<DishRow>("select id, name, price, allergens, additives from dishes order by name"),
|
||||
listActiveLanguageCodes(),
|
||||
]);
|
||||
const langCodes = activeCodes.filter((c) => c !== "de");
|
||||
const translations = await loadTranslationsFor(
|
||||
"dish_translations",
|
||||
dishes.map((d) => d.id)
|
||||
);
|
||||
const rows = [
|
||||
["name", "name_en", "name_pl", "price", "allergens", "additives"],
|
||||
["name", ...langCodes.map((c) => `name_${c}`), "price", "allergens", "additives"],
|
||||
...dishes.map((d) => [
|
||||
d.name,
|
||||
d.name_en ?? "",
|
||||
d.name_pl ?? "",
|
||||
String(d.price),
|
||||
...langCodes.map((c) => translations.get(d.id)?.[c] ?? ""),
|
||||
d.price,
|
||||
(d.allergens ?? []).join(";"),
|
||||
(d.additives ?? []).join(";"),
|
||||
]),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { withTransaction } from "@/lib/db";
|
||||
import { replaceTranslations } from "@/lib/db/translations";
|
||||
import { listActiveLanguageCodes } from "@/lib/db/languages";
|
||||
import { authErrorResponse, requireRole } from "@/lib/auth/session";
|
||||
import { isSameOrigin } from "@/lib/origin";
|
||||
import { parseCsvWithHeader, splitCodes } from "@/lib/csv";
|
||||
@@ -7,8 +9,7 @@ import { parseCsvWithHeader, splitCodes } from "@/lib/csv";
|
||||
interface ImportRow {
|
||||
rowNum: number;
|
||||
name: string;
|
||||
name_en: string | null;
|
||||
name_pl: string | null;
|
||||
translations: Record<string, string>;
|
||||
price: number;
|
||||
allergens: string[];
|
||||
additives: string[];
|
||||
@@ -32,8 +33,10 @@ export async function POST(request: Request) {
|
||||
if (nameIdx === -1 || priceIdx === -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 allergensIdx = header.indexOf("allergens");
|
||||
const additivesIdx = header.indexOf("additives");
|
||||
|
||||
@@ -52,11 +55,15 @@ export async function POST(request: Request) {
|
||||
skipped.push({ row: rowNum, reason: `Ungültiger Preis „${c[priceIdx] ?? ""}“` });
|
||||
return;
|
||||
}
|
||||
const translations: Record<string, string> = {};
|
||||
for (const { code, idx } of langCols) {
|
||||
const value = c[idx]?.trim();
|
||||
if (value) translations[code] = value;
|
||||
}
|
||||
rows.push({
|
||||
rowNum,
|
||||
name,
|
||||
name_en: nameEnIdx >= 0 ? c[nameEnIdx]?.trim() || null : null,
|
||||
name_pl: namePlIdx >= 0 ? c[namePlIdx]?.trim() || null : null,
|
||||
translations,
|
||||
price,
|
||||
allergens: allergensIdx >= 0 ? splitCodes(c[allergensIdx]) : [],
|
||||
additives: additivesIdx >= 0 ? splitCodes(c[additivesIdx]) : [],
|
||||
@@ -65,6 +72,7 @@ export async function POST(request: Request) {
|
||||
|
||||
let created = 0;
|
||||
let updated = 0;
|
||||
const savedTranslations: { id: string; translations: Record<string, string> }[] = [];
|
||||
if (rows.length > 0) {
|
||||
await withTransaction(async (client) => {
|
||||
for (const row of rows) {
|
||||
@@ -84,21 +92,27 @@ export async function POST(request: Request) {
|
||||
}
|
||||
if (existing.rows.length === 1) {
|
||||
await client.query(
|
||||
`update dishes set name_en = $1, name_pl = $2, price = $3, allergens = $4, additives = $5
|
||||
where id = $6`,
|
||||
[row.name_en, row.name_pl, row.price, row.allergens, row.additives, existing.rows[0].id]
|
||||
`update dishes set price = $1, allergens = $2, additives = $3 where id = $4`,
|
||||
[row.price, row.allergens, row.additives, existing.rows[0].id]
|
||||
);
|
||||
savedTranslations.push({ id: existing.rows[0].id, translations: row.translations });
|
||||
updated++;
|
||||
} else {
|
||||
await client.query(
|
||||
`insert into dishes (name, name_en, name_pl, price, allergens, additives)
|
||||
values ($1, $2, $3, $4, $5, $6)`,
|
||||
[row.name, row.name_en, row.name_pl, row.price, row.allergens, row.additives]
|
||||
const inserted = await client.query<{ id: string }>(
|
||||
`insert into dishes (name, price, allergens, additives)
|
||||
values ($1, $2, $3, $4) returning id`,
|
||||
[row.name, row.price, row.allergens, row.additives]
|
||||
);
|
||||
savedTranslations.push({ id: inserted.rows[0].id, translations: row.translations });
|
||||
created++;
|
||||
}
|
||||
}
|
||||
});
|
||||
for (const { id, translations } of savedTranslations) {
|
||||
if (Object.keys(translations).length > 0) {
|
||||
await replaceTranslations("dish_translations", id, translations);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ created, updated, skipped });
|
||||
|
||||
+35
-9
@@ -1,12 +1,35 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { query, queryOne } from "@/lib/db";
|
||||
import { loadTranslationsFor, replaceTranslations } from "@/lib/db/translations";
|
||||
import { authErrorResponse, requireRole } from "@/lib/auth/session";
|
||||
import { isSameOrigin } from "@/lib/origin";
|
||||
import type { Dish } from "@/lib/types";
|
||||
|
||||
interface DishRow {
|
||||
id: string;
|
||||
name: string;
|
||||
price: string;
|
||||
allergens: string[];
|
||||
additives: string[];
|
||||
}
|
||||
|
||||
async function withTranslations(rows: DishRow[]): Promise<Dish[]> {
|
||||
const translations = await loadTranslationsFor(
|
||||
"dish_translations",
|
||||
rows.map((r) => r.id)
|
||||
);
|
||||
return rows.map((r) => ({
|
||||
...r,
|
||||
price: Number(r.price),
|
||||
translations: translations.get(r.id) ?? {},
|
||||
}));
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const dishes = await query<Dish>("select * from dishes order by name");
|
||||
return NextResponse.json(dishes);
|
||||
const rows = await query<DishRow>(
|
||||
"select id, name, price, allergens, additives from dishes order by name"
|
||||
);
|
||||
return NextResponse.json(await withTranslations(rows));
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
@@ -17,17 +40,20 @@ export async function POST(request: Request) {
|
||||
await requireRole(["staff", "admin"]);
|
||||
const body = (await request.json()) as {
|
||||
name: string;
|
||||
name_en: string | null;
|
||||
name_pl: string | null;
|
||||
price: number;
|
||||
allergens: string[];
|
||||
additives: string[];
|
||||
translations?: Record<string, string | null>;
|
||||
};
|
||||
const dish = await queryOne<Dish>(
|
||||
`insert into dishes (name, name_en, name_pl, price, allergens, additives)
|
||||
values ($1, $2, $3, $4, $5, $6) returning *`,
|
||||
[body.name, body.name_en, body.name_pl, body.price, body.allergens, body.additives]
|
||||
);
|
||||
const row = (await queryOne<DishRow>(
|
||||
`insert into dishes (name, price, allergens, additives)
|
||||
values ($1, $2, $3, $4) returning id, name, price, allergens, additives`,
|
||||
[body.name, body.price, body.allergens, body.additives]
|
||||
))!;
|
||||
if (body.translations) {
|
||||
await replaceTranslations("dish_translations", row.id, body.translations);
|
||||
}
|
||||
const [dish] = await withTranslations([row]);
|
||||
return NextResponse.json(dish, { status: 201 });
|
||||
} catch (err) {
|
||||
const res = authErrorResponse(err);
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { query, queryOne } from "@/lib/db";
|
||||
import { loadTranslationsFor, replaceTranslations } from "@/lib/db/translations";
|
||||
import { authErrorResponse, requireRole } from "@/lib/auth/session";
|
||||
import { isSameOrigin } from "@/lib/origin";
|
||||
import type { Section } from "@/lib/types";
|
||||
|
||||
interface SectionRow {
|
||||
key: string;
|
||||
label: string;
|
||||
time_range: string;
|
||||
sort: number;
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ key: string }> }
|
||||
@@ -15,25 +23,37 @@ export async function PATCH(
|
||||
await requireRole(["staff", "admin"]);
|
||||
const { key } = await params;
|
||||
const body = (await request.json()) as Partial<
|
||||
Pick<Section, "label" | "label_en" | "label_pl" | "time_range" | "sort">
|
||||
>;
|
||||
Pick<Section, "label" | "time_range" | "sort">
|
||||
> & { translations?: Record<string, string | null> };
|
||||
|
||||
const fields: string[] = [];
|
||||
const values: unknown[] = [];
|
||||
for (const col of ["label", "label_en", "label_pl", "time_range", "sort"] as const) {
|
||||
for (const col of ["label", "time_range", "sort"] as const) {
|
||||
if (body[col] !== undefined) {
|
||||
values.push(body[col]);
|
||||
fields.push(`${col} = $${values.length}`);
|
||||
}
|
||||
}
|
||||
if (fields.length === 0) return NextResponse.json({ error: "empty_update" }, { status: 400 });
|
||||
values.push(key);
|
||||
|
||||
const section = await queryOne<Section>(
|
||||
`update sections set ${fields.join(", ")} where key = $${values.length} returning *`,
|
||||
values
|
||||
);
|
||||
if (!section) return NextResponse.json({ error: "not_found" }, { status: 404 });
|
||||
let row: SectionRow | null;
|
||||
if (fields.length > 0) {
|
||||
values.push(key);
|
||||
row = await queryOne<SectionRow>(
|
||||
`update sections set ${fields.join(", ")} where key = $${values.length}
|
||||
returning key, label, time_range, sort`,
|
||||
values
|
||||
);
|
||||
} else {
|
||||
row = await queryOne<SectionRow>(
|
||||
"select key, label, time_range, sort from sections where key = $1",
|
||||
[key]
|
||||
);
|
||||
}
|
||||
if (!row) return NextResponse.json({ error: "not_found" }, { status: 404 });
|
||||
if (body.translations) {
|
||||
await replaceTranslations("section_translations", row.key, body.translations);
|
||||
}
|
||||
const translations = await loadTranslationsFor("section_translations", [row.key]);
|
||||
const section: Section = { ...row, translations: translations.get(row.key) ?? {} };
|
||||
return NextResponse.json(section);
|
||||
} catch (err) {
|
||||
const res = authErrorResponse(err);
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { query, queryOne } from "@/lib/db";
|
||||
import { loadTranslationsFor, replaceTranslations } from "@/lib/db/translations";
|
||||
import { authErrorResponse, requireRole } from "@/lib/auth/session";
|
||||
import { isSameOrigin } from "@/lib/origin";
|
||||
import type { Section } from "@/lib/types";
|
||||
|
||||
interface SectionRow {
|
||||
key: string;
|
||||
label: string;
|
||||
time_range: string;
|
||||
sort: number;
|
||||
}
|
||||
|
||||
async function withTranslations(rows: SectionRow[]): Promise<Section[]> {
|
||||
const translations = await loadTranslationsFor(
|
||||
"section_translations",
|
||||
rows.map((r) => r.key)
|
||||
);
|
||||
return rows.map((r) => ({ ...r, translations: translations.get(r.key) ?? {} }));
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const sections = await query<Section>("select * from sections order by sort");
|
||||
return NextResponse.json(sections);
|
||||
const rows = await query<SectionRow>(
|
||||
"select key, label, time_range, sort from sections order by sort"
|
||||
);
|
||||
return NextResponse.json(await withTranslations(rows));
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
@@ -17,19 +35,22 @@ export async function POST(request: Request) {
|
||||
await requireRole(["staff", "admin"]);
|
||||
const body = (await request.json()) as {
|
||||
label: string;
|
||||
label_en: string | null;
|
||||
label_pl: string | null;
|
||||
time_range: string;
|
||||
translations?: Record<string, string | null>;
|
||||
};
|
||||
|
||||
const maxRow = await queryOne<{ sort: number }>(
|
||||
"select sort from sections order by sort desc limit 1"
|
||||
);
|
||||
const section = await queryOne<Section>(
|
||||
`insert into sections (label, label_en, label_pl, time_range, sort)
|
||||
values ($1, $2, $3, $4, $5) returning *`,
|
||||
[body.label, body.label_en, body.label_pl, body.time_range, (maxRow?.sort ?? 0) + 1]
|
||||
);
|
||||
const row = (await queryOne<SectionRow>(
|
||||
`insert into sections (label, time_range, sort)
|
||||
values ($1, $2, $3) returning key, label, time_range, sort`,
|
||||
[body.label, body.time_range, (maxRow?.sort ?? 0) + 1]
|
||||
))!;
|
||||
if (body.translations) {
|
||||
await replaceTranslations("section_translations", row.key, body.translations);
|
||||
}
|
||||
const [section] = await withTranslations([row]);
|
||||
return NextResponse.json(section, { status: 201 });
|
||||
} catch (err) {
|
||||
const res = authErrorResponse(err);
|
||||
|
||||
+43
-27
@@ -1,21 +1,31 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { query } from "@/lib/db";
|
||||
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 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.
|
||||
// Ü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; textColumn: string }> = {
|
||||
dishes: { idColumn: "id", textColumn: "name" },
|
||||
sections: { idColumn: "key", textColumn: "label" },
|
||||
allergens: { idColumn: "code", textColumn: "name" },
|
||||
additives: { idColumn: "code", textColumn: "name" },
|
||||
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" },
|
||||
};
|
||||
|
||||
async function translateWithDeepL(text: string, targetLang: "EN" | "PL", apiKey: string) {
|
||||
// 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";
|
||||
@@ -71,25 +81,31 @@ export async function POST(request: Request) {
|
||||
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.");
|
||||
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));
|
||||
|
||||
if (persist) {
|
||||
// table/idColumn/textColumn kommen ausschließlich aus dem oben
|
||||
// geprüften ALLOWED_TABLES-Whitelist, nie aus Nutzereingaben –
|
||||
// sicher für die String-Interpolation in der Query.
|
||||
await query(
|
||||
`update ${table} set ${config.textColumn}_en = $1, ${config.textColumn}_pl = $2
|
||||
where ${config.idColumn} = $3`,
|
||||
[en, pl, id]
|
||||
);
|
||||
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.");
|
||||
}
|
||||
|
||||
return NextResponse.json({ configured: true, en, pl });
|
||||
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." },
|
||||
|
||||
Reference in New Issue
Block a user