Files
Speisekarte/lib/db/translations.ts
T
florian.wilde 2a2c5affd5 feat: Tage als geschlossen markierbar (Feiertage o. Ä.)
Im Wochenplan lässt sich pro Tag ein optionaler, mehrsprachiger
Schließungsgrund hinterlegen. Geschlossene Tage werden auf der
öffentlichen Speisekarte (Desktop + mobil) und im RSS-Feed als
"Geschlossen" statt mit Essenszeiten angezeigt, im Wochenplan bleibt
"Mahlzeit hinzufügen" für diesen Tag deaktiviert.
2026-07-20 12:35:00 +02:00

88 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
| "closed_day_translations";
const ID_COLUMN: Record<TranslationTable, string> = {
dish_translations: "dish_id",
section_translations: "section_key",
allergen_translations: "allergen_code",
additive_translations: "additive_code",
closed_day_translations: "closed_date",
};
/** 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]
);
}
}