Nutzer-Wunsch 20.08.2026: "will ich in der admin seite das selbst gestalten können, mit bild und text und am besten auch einen link." Auf Rueckfrage entschieden: Titel/Text nur auf Deutsch eingeben, die anderen 4 Sprachen werden beim Speichern automatisch uebersetzt (MyMemory, 0€, kein API-Key) -- bewusste Ausnahme von der sonst geltenden "immer echte Uebersetzung"-Regel, klar dokumentiert und im Admin-UI selbst als Hinweis sichtbar. de-CH bekommt denselben deutschen Text (Dialekt ist keine von Uebersetzungs-APIs unterstuetzte Zielsprache). Backend (server-internal): - routes/events.js: getEventsPublic (oeffentlich, keine Session), saveEvent + uploadEventImage (beide hinter neuer EVENTS_MANAGE- Berechtigung, Owner immer erlaubt). Speichert in der bereits bestehenden app_settings-Tabelle (2 feste Slots) statt einer neuen Tabelle -- es gibt nie mehr als genau 2 Events. - lib/translate.js: MyMemory-Anbindung mit "fail closed auf Deutsch" pro Sprache, falls der Dienst mal nicht antwortet. - Bild-Upload per multer, zufaelliger Dateiname (crypto.randomUUID, verhindert Path-Traversal ueber den Originalnamen komplett), 5 MB Limit, nur jpeg/png/webp, Ablage unter /var/lib/dogfather-internal/ uploads/events (NICHT im Git-Ordner -- uebersteht Deploys), oeffentlich ausgeliefert unter /uploads. - Neue Berechtigung EVENTS_MANAGE im Katalog (Gruppe "Startseite"). Frontend: - index.html: laedt /events-of-year beim Aufruf, ueberschreibt pro Slot Datum/Titel/Text/Bild/Link NUR wenn dort tatsaechlich etwas gespeichert ist -- bleibt der Abruf aus oder ist ein Slot leer, bleibt der bisherige fest eingebaute Standardinhalt (dieselben zwei echten Events) stehen. Kein Blocker, kein sichtbarer Fehler bei Ausfall. - verwaltung.html: neue Sektion "🏆 Event des Jahres" (nur mit EVENTS_MANAGE sichtbar), 2 Karten mit Bild-Upload+Vorschau, Datum, Titel, Text, Link, eigenem Speichern-Knopf pro Event. Ausfuehrlich getestet, weil server-internal wegen fehlender Visual- Studio-Build-Tools auf dieser Windows-Maschine nicht lokal mit echtem better-sqlite3 laufen kann: routes/events.js komplett isoliert gegen eine Fake-DB getestet (11 Szenarien: oeffentlicher Abruf, fehlende Session, Session ohne Recht, Owner, Rolle MIT EVENTS_MANAGE, alle Validierungen, Bild-Upload inkl. falscher Dateityp, Abruf des hochgeladenen Bilds). index.html per Playwright mit echtem Netzwerk-Mocking gegen zwei Szenarien getestet (API nicht erreichbar -> Standardinhalt bleibt; API liefert echte Daten -> nur der befuellte Slot wird ueberschrieben, der leere bleibt Standard). Dabei einen echten CSS-Bug gefunden und behoben (display:block auf .jahres-event-bild überschrieb die [hidden]-Regel des Browsers, leeres Bild waere immer sichtbar gewesen). verwaltung.html per Playwright mit gemocktem Login+API end-to-end getestet: Formular wird korrekt vorbefuellt, Bild-Upload + Speichern senden die richtigen Daten. WICHTIG: server-internal laeuft unter einem eigenen Systembenutzer (dogiintern), auf den ich (claudian) bewusst KEINEN Zugriff habe -- diese Aenderung kann ich anders als sonst nicht selbst bis auf den Server bringen. Dogi muss den Deploy-Schritt fuer server-internal selbst ausfuehren (git pull + npm install + Neustart des dogiintern-Dienstes). Co-Authored-By: Claude Opus 5 <[email protected]>
59 lines
3.0 KiB
JavaScript
59 lines
3.0 KiB
JavaScript
/* =====================================================================
|
|
lib/translate.js — automatische Übersetzung für admin-gepflegte Texte
|
|
(aktuell nur "Event des Jahres" auf der Startseite, 20.08.2026).
|
|
|
|
Bewusste Entscheidung (Nutzer-Rückfrage 20.08.2026): Dogi tippt Titel/Text
|
|
nur auf Deutsch in die Verwaltung ein, die anderen Sprachen entstehen
|
|
automatisch beim Speichern — NICHT die sonst auf der ganzen Seite geltende
|
|
Regel "immer echte, geprüfte Übersetzungen", sondern eine bewusste
|
|
Ausnahme für diesen einen admin-gepflegten Bereich. Maschinen-Übersetzung
|
|
ist NIE so gut wie eine echte — für schnell änderbare Startseiten-Teaser
|
|
akzeptabel, für Rechtstexte/feste Seiteninhalte weiterhin NICHT verwenden.
|
|
|
|
de-CH bekommt bewusst NICHT automatisch übersetzt: der Website-Dialekt
|
|
(Solothurn/Zürich-nahe Schreibweise, siehe main.js-Kommentar zu I18N_CORE)
|
|
ist keine von Übersetzungs-APIs unterstützte Zielsprache — eine
|
|
automatische "Übersetzung" nach de-CH würde bestenfalls Standarddeutsch
|
|
zurückgeben, das wäre nicht ehrlich als "Dialekt" beschriftet. de-CH
|
|
bekommt stattdessen exakt denselben Text wie de.
|
|
|
|
MyMemory (https://mymemory.translated.net) statt Google/DeepL: völlig
|
|
kostenlos, kein API-Key/Anmeldung nötig (passt zu "Kosten möglichst 0€"),
|
|
für kurze Teaser-Texte ausreichende Qualität. Absichtlich KEIN lokal
|
|
gehosteter Übersetzungsdienst (z.B. LibreTranslate) — bei der seltenen
|
|
Nutzung (2 kurze Texte, gelegentlich geändert) wäre ein dauerhaft
|
|
laufender zusätzlicher Dienst auf dem ohnehin schon mehrfach belegten
|
|
Server unverhältnismäßig viel Aufwand für den tatsächlichen Bedarf. */
|
|
|
|
const SPRACHEN_ZIEL = ["en", "fr", "pt"];
|
|
|
|
async function uebersetzeEinzeln(text, ziel) {
|
|
if (!text || !text.trim()) return "";
|
|
const url = `https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=de|${ziel}`;
|
|
const res = await fetch(url, { signal: AbortSignal.timeout(8000) });
|
|
if (!res.ok) throw new Error(`Übersetzungsdienst antwortete mit ${res.status}`);
|
|
const data = await res.json();
|
|
const uebersetzt = data?.responseData?.translatedText;
|
|
if (!uebersetzt || typeof uebersetzt !== "string") throw new Error("Keine Übersetzung erhalten.");
|
|
return uebersetzt;
|
|
}
|
|
|
|
/** Übersetzt einen deutschen Text nach en/fr/pt, de-CH = 1:1 Kopie von de.
|
|
Bei Fehlern (Dienst nicht erreichbar etc.) fällt die jeweilige Sprache
|
|
auf den deutschen Text zurück, statt die ganze Speicherung scheitern zu
|
|
lassen — "fail closed" mit sichtbarem Ergebnis statt kaputter Anzeige. */
|
|
export async function uebersetzeAlleSprachen(textDe) {
|
|
const ergebnis = { de: textDe || "", "de-CH": textDe || "" };
|
|
await Promise.all(
|
|
SPRACHEN_ZIEL.map(async (ziel) => {
|
|
try {
|
|
ergebnis[ziel] = await uebersetzeEinzeln(textDe, ziel);
|
|
} catch (err) {
|
|
console.error(`[translate] ${ziel} fehlgeschlagen, falle auf Deutsch zurück:`, err?.message || err);
|
|
ergebnis[ziel] = textDe || "";
|
|
}
|
|
})
|
|
);
|
|
return ergebnis;
|
|
}
|