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
+28 -27
View File
@@ -7,7 +7,7 @@ import { useCallback, useEffect, useState } from "react";
import { formatMessage } from "@/lib/i18n/shared";
import { localized } from "@/lib/types";
import type { CodeItem } from "@/lib/types";
import { useDict, useLocale } from "@/components/i18n/LocaleProvider";
import { useDict, useLanguages, useLocale } from "@/components/i18n/LocaleProvider";
export default function CodeListManager({
table,
@@ -18,6 +18,7 @@ export default function CodeListManager({
}) {
const dict = useDict();
const locale = useLocale();
const languages = useLanguages().filter((l) => l.code !== "de");
const title =
table === "allergens" ? dict.codeList.allergensTitle : dict.codeList.additivesTitle;
const codeLabel =
@@ -30,8 +31,7 @@ export default function CodeListManager({
original: string | null; // code vor der Bearbeitung, null = neu
code: string;
name: string;
nameEn: string;
namePl: string;
translations: Record<string, string>;
} | null>(null);
const [error, setError] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
@@ -60,8 +60,7 @@ export default function CodeListManager({
const payload = {
code,
name,
name_en: editing.nameEn.trim() || null,
name_pl: editing.namePl.trim() || null,
translations: editing.translations,
};
const res = editing.original
@@ -118,7 +117,9 @@ export default function CodeListManager({
const data = await res.json();
if (!res.ok) throw new Error(data?.error ?? `Fehler (${res.status})`);
setEditing((prev) =>
prev ? { ...prev, nameEn: data.en ?? prev.nameEn, namePl: data.pl ?? prev.namePl } : prev
prev
? { ...prev, translations: { ...prev.translations, ...(data.translations ?? {}) } }
: prev
);
setTranslateState("done");
} catch (err) {
@@ -202,7 +203,7 @@ export default function CodeListManager({
type="button"
className="btn btn-add"
onClick={() => {
setEditing({ original: null, code: "", name: "", nameEn: "", namePl: "" });
setEditing({ original: null, code: "", name: "", translations: {} });
setTranslateState("idle");
}}
>
@@ -237,25 +238,26 @@ export default function CodeListManager({
required
/>
</div>
<div className="grid gap-2 sm:grid-cols-2">
<div>
<label className="label">{dict.dishManager.nameEnLabel}</label>
<input
className="input"
value={editing.nameEn}
onChange={(e) => setEditing({ ...editing, nameEn: e.target.value })}
/>
{languages.length > 0 && (
<div className="grid gap-2 sm:grid-cols-2">
{languages.map((lang) => (
<div key={lang.code}>
<label className="label">{lang.label}</label>
<input
className="input"
value={editing.translations[lang.code] ?? ""}
onChange={(e) =>
setEditing({
...editing,
translations: { ...editing.translations, [lang.code]: e.target.value },
})
}
/>
</div>
))}
</div>
<div>
<label className="label">{dict.dishManager.namePlLabel}</label>
<input
className="input"
value={editing.namePl}
onChange={(e) => setEditing({ ...editing, namePl: e.target.value })}
/>
</div>
</div>
{translateEnabled && (
)}
{translateEnabled && languages.length > 0 && (
<div>
<button
type="button"
@@ -304,8 +306,7 @@ export default function CodeListManager({
original: item.code,
code: item.code,
name: item.name,
nameEn: item.name_en ?? "",
namePl: item.name_pl ?? "",
translations: { ...item.translations },
});
setTranslateState("idle");
}}