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:
@@ -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");
|
||||
}}
|
||||
|
||||
+31
-34
@@ -6,13 +6,12 @@ import { formatMessage } from "@/lib/i18n/shared";
|
||||
import { localized } from "@/lib/types";
|
||||
import type { CodeItem, Dish } from "@/lib/types";
|
||||
import ChipPicker from "@/components/ChipPicker";
|
||||
import { useDict, useLocale } from "@/components/i18n/LocaleProvider";
|
||||
import { useDict, useLanguages, useLocale } from "@/components/i18n/LocaleProvider";
|
||||
|
||||
interface DishForm {
|
||||
id: string | null; // null = neues Gericht
|
||||
name: string;
|
||||
nameEn: string;
|
||||
namePl: string;
|
||||
translations: Record<string, string>;
|
||||
price: string;
|
||||
allergens: string[];
|
||||
additives: string[];
|
||||
@@ -21,8 +20,7 @@ interface DishForm {
|
||||
const EMPTY_FORM: DishForm = {
|
||||
id: null,
|
||||
name: "",
|
||||
nameEn: "",
|
||||
namePl: "",
|
||||
translations: {},
|
||||
price: "",
|
||||
allergens: [],
|
||||
additives: [],
|
||||
@@ -35,6 +33,7 @@ export default function DishManager({
|
||||
}) {
|
||||
const dict = useDict();
|
||||
const locale = useLocale();
|
||||
const languages = useLanguages().filter((l) => l.code !== "de");
|
||||
const [dishes, setDishes] = useState<Dish[]>([]);
|
||||
const [allergens, setAllergens] = useState<CodeItem[]>([]);
|
||||
const [additives, setAdditives] = useState<CodeItem[]>([]);
|
||||
@@ -93,11 +92,10 @@ export default function DishManager({
|
||||
}
|
||||
const payload = {
|
||||
name: form.name.trim(),
|
||||
name_en: form.nameEn.trim() || null,
|
||||
name_pl: form.namePl.trim() || null,
|
||||
price,
|
||||
allergens: form.allergens,
|
||||
additives: form.additives,
|
||||
translations: form.translations,
|
||||
};
|
||||
const res = form.id
|
||||
? await fetch(`/api/dishes/${form.id}`, {
|
||||
@@ -218,7 +216,9 @@ export default function DishManager({
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data?.error ?? `Fehler (${res.status})`);
|
||||
setForm((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) {
|
||||
@@ -352,31 +352,29 @@ export default function DishManager({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="label" htmlFor="dish-name-en">
|
||||
{dict.dishManager.nameEnLabel}
|
||||
</label>
|
||||
<input
|
||||
id="dish-name-en"
|
||||
className="input"
|
||||
value={form.nameEn}
|
||||
onChange={(e) => setForm({ ...form, nameEn: e.target.value })}
|
||||
/>
|
||||
{languages.length > 0 && (
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{languages.map((lang) => (
|
||||
<div key={lang.code}>
|
||||
<label className="label" htmlFor={`dish-name-${lang.code}`}>
|
||||
{lang.label}
|
||||
</label>
|
||||
<input
|
||||
id={`dish-name-${lang.code}`}
|
||||
className="input"
|
||||
value={form.translations[lang.code] ?? ""}
|
||||
onChange={(e) =>
|
||||
setForm({
|
||||
...form,
|
||||
translations: { ...form.translations, [lang.code]: e.target.value },
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<label className="label" htmlFor="dish-name-pl">
|
||||
{dict.dishManager.namePlLabel}
|
||||
</label>
|
||||
<input
|
||||
id="dish-name-pl"
|
||||
className="input"
|
||||
value={form.namePl}
|
||||
onChange={(e) => setForm({ ...form, namePl: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{translateEnabled && (
|
||||
)}
|
||||
{translateEnabled && languages.length > 0 && (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -495,8 +493,7 @@ export default function DishManager({
|
||||
setForm({
|
||||
id: dish.id,
|
||||
name: dish.name,
|
||||
nameEn: dish.name_en ?? "",
|
||||
namePl: dish.name_pl ?? "",
|
||||
translations: { ...dish.translations },
|
||||
price: String(dish.price).replace(".", ","),
|
||||
allergens: dish.allergens ?? [],
|
||||
additives: dish.additives ?? [],
|
||||
|
||||
@@ -7,13 +7,12 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import { formatMessage } from "@/lib/i18n/shared";
|
||||
import { localized } from "@/lib/types";
|
||||
import type { Section } from "@/lib/types";
|
||||
import { useDict, useLocale } from "@/components/i18n/LocaleProvider";
|
||||
import { useDict, useLanguages, useLocale } from "@/components/i18n/LocaleProvider";
|
||||
|
||||
interface SectionForm {
|
||||
key: string | null; // null = neue Essenszeit
|
||||
label: string;
|
||||
labelEn: string;
|
||||
labelPl: string;
|
||||
translations: Record<string, string>;
|
||||
timeFrom: string;
|
||||
timeTo: string;
|
||||
}
|
||||
@@ -21,8 +20,7 @@ interface SectionForm {
|
||||
const EMPTY_FORM: SectionForm = {
|
||||
key: null,
|
||||
label: "",
|
||||
labelEn: "",
|
||||
labelPl: "",
|
||||
translations: {},
|
||||
timeFrom: "",
|
||||
timeTo: "",
|
||||
};
|
||||
@@ -35,6 +33,7 @@ function splitTimeRange(range: string): { from: string; to: string } {
|
||||
export default function SectionManager() {
|
||||
const dict = useDict();
|
||||
const locale = useLocale();
|
||||
const languages = useLanguages().filter((l) => l.code !== "de");
|
||||
const [sections, setSections] = useState<Section[]>([]);
|
||||
const [form, setForm] = useState<SectionForm | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -61,9 +60,8 @@ export default function SectionManager() {
|
||||
if (!label || !form.timeFrom || !form.timeTo) return;
|
||||
const payload = {
|
||||
label,
|
||||
label_en: form.labelEn.trim() || null,
|
||||
label_pl: form.labelPl.trim() || null,
|
||||
time_range: `${form.timeFrom} - ${form.timeTo}`,
|
||||
translations: form.translations,
|
||||
};
|
||||
|
||||
const res = form.key
|
||||
@@ -160,30 +158,28 @@ export default function SectionManager() {
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="label" htmlFor="section-label-en">
|
||||
{dict.sectionManager.nameEnLabel}
|
||||
</label>
|
||||
<input
|
||||
id="section-label-en"
|
||||
className="input"
|
||||
value={form.labelEn}
|
||||
onChange={(e) => setForm({ ...form, labelEn: e.target.value })}
|
||||
/>
|
||||
{languages.length > 0 && (
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{languages.map((lang) => (
|
||||
<div key={lang.code}>
|
||||
<label className="label" htmlFor={`section-label-${lang.code}`}>
|
||||
{lang.label}
|
||||
</label>
|
||||
<input
|
||||
id={`section-label-${lang.code}`}
|
||||
className="input"
|
||||
value={form.translations[lang.code] ?? ""}
|
||||
onChange={(e) =>
|
||||
setForm({
|
||||
...form,
|
||||
translations: { ...form.translations, [lang.code]: e.target.value },
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<label className="label" htmlFor="section-label-pl">
|
||||
{dict.sectionManager.namePlLabel}
|
||||
</label>
|
||||
<input
|
||||
id="section-label-pl"
|
||||
className="input"
|
||||
value={form.labelPl}
|
||||
onChange={(e) => setForm({ ...form, labelPl: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-3">
|
||||
<div className="max-w-32">
|
||||
<label className="label" htmlFor="section-time-from">
|
||||
@@ -267,8 +263,7 @@ export default function SectionManager() {
|
||||
setForm({
|
||||
key: section.key,
|
||||
label: section.label,
|
||||
labelEn: section.label_en ?? "",
|
||||
labelPl: section.label_pl ?? "",
|
||||
translations: { ...section.translations },
|
||||
timeFrom: from,
|
||||
timeTo: to,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user