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:
@@ -0,0 +1,85 @@
|
||||
import "server-only";
|
||||
import { query } from "@/lib/db";
|
||||
|
||||
// Gemeinsame Zugriffsschicht für die vier Übersetzungstabellen aus
|
||||
// db/migrations/0006_content_translations.sql. Die Wert-Spalte heißt
|
||||
// in allen vieren bewusst einheitlich "name" (auch bei
|
||||
// section_translations, obwohl sections.label selbst anders heißt) –
|
||||
// vereinfacht diese generischen Helfer, statt zusätzlich eine Spalten-
|
||||
// Namens-Zuordnung pflegen zu müssen.
|
||||
export type TranslationTable =
|
||||
| "dish_translations"
|
||||
| "section_translations"
|
||||
| "allergen_translations"
|
||||
| "additive_translations";
|
||||
|
||||
const ID_COLUMN: Record<TranslationTable, string> = {
|
||||
dish_translations: "dish_id",
|
||||
section_translations: "section_key",
|
||||
allergen_translations: "allergen_code",
|
||||
additive_translations: "additive_code",
|
||||
};
|
||||
|
||||
/** Alle Übersetzungen einer Tabelle, gruppiert nach Eltern-ID. */
|
||||
export async function loadTranslations(
|
||||
table: TranslationTable
|
||||
): Promise<Map<string, Record<string, string>>> {
|
||||
const idCol = ID_COLUMN[table];
|
||||
const rows = await query<{ id: string; language_code: string; name: string }>(
|
||||
`select ${idCol} as id, language_code, name from ${table}`
|
||||
);
|
||||
return groupByParent(rows);
|
||||
}
|
||||
|
||||
/** Übersetzungen nur für eine Teilmenge von Eltern-IDs laden (z. B. die
|
||||
* Gerichte des aktuell angezeigten Wochenplan-Zeitraums). */
|
||||
export async function loadTranslationsFor(
|
||||
table: TranslationTable,
|
||||
ids: string[]
|
||||
): Promise<Map<string, Record<string, string>>> {
|
||||
if (ids.length === 0) return new Map();
|
||||
const idCol = ID_COLUMN[table];
|
||||
const rows = await query<{ id: string; language_code: string; name: string }>(
|
||||
`select ${idCol} as id, language_code, name from ${table} where ${idCol} = any($1)`,
|
||||
[ids]
|
||||
);
|
||||
return groupByParent(rows);
|
||||
}
|
||||
|
||||
function groupByParent(
|
||||
rows: { id: string; language_code: string; name: string }[]
|
||||
): Map<string, Record<string, string>> {
|
||||
const map = new Map<string, Record<string, string>>();
|
||||
for (const r of rows) {
|
||||
if (!map.has(r.id)) map.set(r.id, {});
|
||||
map.get(r.id)![r.language_code] = r.name;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/** Übersetzungen eines Eintrags für die übergebenen Sprachen setzen –
|
||||
* ein leerer/nur-Leerzeichen-Wert löscht die Zeile für diese Sprache
|
||||
* wieder (z. B. wenn ein Formularfeld geleert wird), statt eine leere
|
||||
* Übersetzung zu speichern. */
|
||||
export async function replaceTranslations(
|
||||
table: TranslationTable,
|
||||
id: string,
|
||||
translations: Record<string, string | null | undefined>
|
||||
): Promise<void> {
|
||||
const idCol = ID_COLUMN[table];
|
||||
for (const [languageCode, rawValue] of Object.entries(translations)) {
|
||||
const value = rawValue?.trim();
|
||||
if (!value) {
|
||||
await query(`delete from ${table} where ${idCol} = $1 and language_code = $2`, [
|
||||
id,
|
||||
languageCode,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
await query(
|
||||
`insert into ${table} (${idCol}, language_code, name) values ($1, $2, $3)
|
||||
on conflict (${idCol}, language_code) do update set name = excluded.name`,
|
||||
[id, languageCode, value]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user