i18n: Englisch als Hauptsprache, Sprachpakete fuer Desktop und WebUI (#661)

Umbau durch Gemini Flash, Durchsicht und Nacharbeit durch Claude.

Umfang:
- Desktop: LocalizationManager (JSON-Pakete, Rueckfall auf Englisch,
  Umschalten zur Laufzeit), Localize-Markup-Erweiterung, Sprachwahl in den
  Einstellungen (AppSettings.Language, Vorgabe "en"); 317 Schluessel je Sprache
- WebUI: alle Ansichten auf t()/data-i18n, 656 Schluessel je Sprache
- Agenten-Werkzeuge: Beschreibungen, Schemata und Meldungen auf Englisch
- Tests an die englischen Meldungen angepasst, keine Pruefung abgeschwaecht

Nacharbeit bei der Durchsicht:
- ~25 Kommentarbloecke mit Begruendungen waren ersatzlos entfallen, darunter
  sicherheitsrelevante (NTFS-Datenstrom beim shared:-Praefix, Bremse gegen
  Agentenschleifen, doppelter POST beim Anlegen einer Instanz). Wieder
  eingesetzt: auf Englisch, wo die Datei umgestellt ist, sonst im Wortlaut.
- Drei WebUI-Schluessel fehlten (logs.paused, instances.instanceOfflineError,
  instances.noInstanceActiveError). Weil t() bei fehlendem Schluessel den
  Schluessel selbst liefert, griff der "|| err.message"-Rueckfall nie — die
  Oberflaeche haette den Schluesselnamen angezeigt.

Geprueft: Schluesselgleichheit en/de, Platzhalter je Schluessel, jeder im
Code benutzte Schluessel vorhanden, node --check fuer alle Module,
1.220 Tests gruen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-24 09:43:51 +02:00
co-authored by Claude Opus 5.5
parent 69718122f3
commit ad71682c2d
97 changed files with 4103 additions and 1928 deletions
@@ -7,6 +7,7 @@
// Strikt DOM API, kein innerHTML.
import { api, holeAktiveInstanz } from "../modules/api.js";
import { t } from "../modules/i18n.js";
import {
erstelleElement,
erstelleKarte,
@@ -29,11 +30,11 @@ function formatiereBytes(bytes) {
/** Was eine Sicherung schützt — als Plakette. */
function schutzPlakette(b) {
if (b.fremdeLizenz) return erstellePlakette("Fremde Lizenz", "warnung");
if (b.secrets === "Unlesbar") return erstellePlakette("Unlesbar", "gefahr");
if (b.verschluesselt) return erstellePlakette(`Lizenz (${b.secretCount} Zugangsdaten)`, "erfolg");
if (b.secrets === "Passphrase") return erstellePlakette(`Passphrase (${b.secretCount})`, "neutral");
return erstellePlakette("Unverschlüsselt, ohne Zugangsdaten", "neutral");
if (b.fremdeLizenz) return erstellePlakette(t("backup.foreignLicense"), "warnung");
if (b.secrets === "Unlesbar") return erstellePlakette(t("backup.unreadable"), "gefahr");
if (b.verschluesselt) return erstellePlakette(t("backup.licenseProtection", { count: b.secretCount }), "erfolg");
if (b.secrets === "Passphrase") return erstellePlakette(t("backup.passphraseProtection", { count: b.secretCount }), "neutral");
return erstellePlakette(t("backup.unencryptedProtection"), "neutral");
}
function checkbox(id, text, an) {
@@ -63,20 +64,20 @@ export async function rendereSicherung() {
const kopf = erstelleElement("div", { klasse: "ansicht-kopf" });
kopf.append(
erstelleElement("div", { klasse: "gruppe-horizontal" },
erstelleElement("h1", { text: "Datensicherung & Wiederherstellung" })),
erstelleElement("h1", { text: t("backup.title") })),
erstelleElement("div", { klasse: "gruppe-horizontal" },
erstelleElement("button", {
klasse: "schaltflaeche flach",
title: "Sicherungen neu laden",
title: t("backup.refreshTooltip"),
disabled: !instanzId,
onclick: () => ladeSicherungen()
}, erstelleIcon(PFADE.aktualisieren, 16), erstelleElement("span", { text: " Aktualisieren" })),
}, erstelleIcon(PFADE.aktualisieren, 16), erstelleElement("span", { text: ` ${t("backup.refresh")}` })),
erstelleElement("button", {
klasse: "schaltflaeche primaer",
title: "Neue Sicherung erstellen",
title: t("backup.newBackupTooltip"),
disabled: !instanzId,
onclick: () => oeffneSicherungsDialog()
}, erstelleIcon(PFADE.plus, 16), erstelleElement("span", { text: " Neue Sicherung" }))
}, erstelleIcon(PFADE.plus, 16), erstelleElement("span", { text: ` ${t("backup.newBackup")}` }))
)
);
layout.append(kopf);
@@ -87,10 +88,10 @@ export async function rendereSicherung() {
if (!instanzId) {
inhaltBereich.append(
erstelleKarte("Keine Instanz ausgewählt", [
erstelleKarte(t("backup.noInstance"), [
erstelleElement("p", {
klasse: "hinweis",
text: "Bitte wähle oben im Kopfbereich eine Instanz aus, um deren Sicherungen zu verwalten."
text: t("backup.selectInstancePrompt")
})
])
);
@@ -107,7 +108,7 @@ export async function rendereSicherung() {
// ─── Liste ───
async function ladeSicherungen() {
inhaltBereich.replaceChildren(
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Sicherungen…" })
erstelleElement("div", { klasse: "hinweis-zentriert", text: t("backup.loading") })
);
try {
@@ -115,12 +116,10 @@ export async function rendereSicherung() {
const backups = daten?.backups ?? [];
infoBereich.replaceChildren(
erstelleKarte("Sicherungsverzeichnis", [
erstelleKarte(t("backup.backupDir"), [
erstelleElement("p", {
klasse: "gedaempft",
text: "Sicherungen werden als Ganzes mit dem Lizenzschlüssel des Instanzmanagers "
+ "verschlüsselt und enthalten die Zugangsdaten. Auf einer anderen Maschine lassen "
+ "sie sich mit demselben Lizenzschlüssel zurückspielen."
text: t("backup.backupDirHint")
}),
erstelleElement("div", { klasse: "ordner-pfad-anzeige" },
erstelleIcon(PFADE.server, 14),
@@ -130,10 +129,10 @@ export async function rendereSicherung() {
if (backups.length === 0) {
inhaltBereich.replaceChildren(
erstelleKarte("Vorhandene Sicherungen", [
erstelleKarte(t("backup.existingBackups", { count: 0 }), [
erstelleElement("p", {
klasse: "gedaempft",
text: "Für diese Instanz gibt es noch keine Sicherung. Lege über die Schaltfläche oben eine an."
text: t("backup.noBackups")
})
])
);
@@ -146,13 +145,13 @@ export async function rendereSicherung() {
const aktionen = erstelleElement("td", { klasse: "spalte-aktionen" },
erstelleElement("button", {
klasse: "schaltflaeche klein sekundär",
title: "Diese Sicherung wiederherstellen",
title: t("backup.restoreTooltip"),
disabled: b.secrets === "Unlesbar",
onclick: () => oeffneRestoreDialog(b)
}, erstelleIcon(PFADE.abspielen, 14), erstelleElement("span", { text: " Wiederherstellen" })),
}, erstelleIcon(PFADE.abspielen, 14), erstelleElement("span", { text: ` ${t("backup.restore")}` })),
erstelleElement("button", {
klasse: "schaltflaeche klein gefahr flach",
title: "Sicherungsdatei unwiderruflich löschen",
title: t("backup.deleteTooltip"),
onclick: () => oeffneLoeschenDialog(b.fileName)
}, erstelleIcon(PFADE.muelleimer, 14))
);
@@ -163,7 +162,7 @@ export async function rendereSicherung() {
erstelleElement("td", { text: formatiereDatumZeit(b.createdAt) }),
erstelleElement("td", { text: formatiereBytes(b.sizeBytes) }),
erstelleElement("td", {}, schutzPlakette(b)),
erstelleElement("td", { text: b.fileCount > 0 ? `${b.fileCount} Dateien` : "—" }),
erstelleElement("td", { text: b.fileCount > 0 ? t("backup.fileCount", { count: b.fileCount }) : "—" }),
aktionen
));
}
@@ -171,15 +170,15 @@ export async function rendereSicherung() {
const tabelle = erstelleElement("table", { klasse: "tabelle sicherungen-tabelle" },
erstelleElement("thead", {},
erstelleElement("tr", {},
erstelleElement("th", { text: "Archivdatei" }),
erstelleElement("th", { text: "Erstellt am" }),
erstelleElement("th", { text: "Größe" }),
erstelleElement("th", { text: "Schutz" }),
erstelleElement("th", { text: "Dateien" }),
erstelleElement("th", { klasse: "spalte-aktionen", text: "Aktionen" }))),
erstelleElement("th", { text: t("backup.archiveFile") }),
erstelleElement("th", { text: t("backup.createdAt") }),
erstelleElement("th", { text: t("backup.size") }),
erstelleElement("th", { text: t("backup.protection") }),
erstelleElement("th", { text: t("backup.files") }),
erstelleElement("th", { klasse: "spalte-aktionen", text: t("backup.actions") }))),
tbody);
inhaltBereich.replaceChildren(erstelleKarte(`Vorhandene Sicherungen (${backups.length})`, [tabelle]));
inhaltBereich.replaceChildren(erstelleKarte(t("backup.existingBackups", { count: backups.length }), [tabelle]));
} catch (err) {
inhaltBereich.replaceChildren(
erstelleKarte("Fehler", [
@@ -192,14 +191,13 @@ export async function rendereSicherung() {
// ─── Neue Sicherung ───
function oeffneSicherungsDialog() {
const form = erstelleElement("form", { klasse: "formular" });
const chats = checkbox("backup-chk-chats", "Chatverläufe sichern", true);
const logs = checkbox("backup-chk-logs", "Protokolldateien (Logs) einschließen", false);
const chats = checkbox("backup-chk-chats", t("backup.includeChats"), true);
const logs = checkbox("backup-chk-logs", t("backup.includeLogs"), false);
form.append(
erstelleElement("p", {
klasse: "form-hilfe",
text: "Die Sicherung wird mit dem Lizenzschlüssel des Instanzmanagers verschlüsselt und "
+ "enthält die Zugangsdaten. Ist ein FTP-Upload eingerichtet, wird sie anschließend hochgeladen."
text: t("backup.dialogHint")
}),
chats.gruppe,
logs.gruppe
@@ -207,13 +205,13 @@ export async function rendereSicherung() {
let modal;
const speichernBtn = erstelleElement("button", {
type: "submit", klasse: "schaltflaeche primaer", text: "Sicherung erstellen"
type: "submit", klasse: "schaltflaeche primaer", text: t("backup.createButton")
});
form.addEventListener("submit", async (e) => {
e.preventDefault();
speichernBtn.disabled = true;
speichernBtn.textContent = "Erstelle Sicherung…";
speichernBtn.textContent = t("backup.creating");
try {
const res = await api(`/verwaltung/instanzen/${pfadInstanz}/backups`, {
@@ -222,19 +220,19 @@ export async function rendereSicherung() {
});
// Ein gescheiterter Upload steht in der Meldung; die Sicherung liegt trotzdem lokal.
const warnt = (res.meldung || "").includes("gescheitert");
zeigeMeldung(res.meldung || "Sicherung erstellt.", warnt ? "warnung" : "erfolg", warnt ? 8000 : undefined);
zeigeMeldung(res.meldung || t("backup.created"), warnt ? "warnung" : "erfolg", warnt ? 8000 : undefined);
modal.schliessen();
ladeSicherungen();
} catch (err) {
zeigeMeldung(`Fehler beim Sichern: ${err.message}`, "gefahr");
zeigeMeldung(t("backup.createFailed", { error: err.message }), "gefahr");
speichernBtn.disabled = false;
speichernBtn.textContent = "Sicherung erstellen";
speichernBtn.textContent = t("backup.createButton");
}
});
modal = erstelleModal("Neue Sicherung erstellen", form, [
modal = erstelleModal(t("backup.dialogTitle"), form, [
erstelleElement("button", {
type: "button", klasse: "schaltflaeche", text: "Abbrechen", onclick: () => modal.schliessen()
type: "button", klasse: "schaltflaeche", text: t("common.cancel"), onclick: () => modal.schliessen()
}),
speichernBtn
]);
@@ -255,26 +253,26 @@ export async function rendereSicherung() {
form.append(erstelleElement("div", { klasse: "hinweis-box warnung" },
erstelleIcon(PFADE.ausrufezeichen, 18),
erstelleElement("div", {},
erstelleElement("strong", { text: "Achtung: Überschreiben bestehender Daten" }),
erstelleElement("strong", { text: t("backup.overwriteWarningTitle") }),
erstelleElement("p", {
text: `Die Wiederherstellung überschreibt die aktuellen Konfigurationen und Daten der Instanz mit dem Stand vom ${formatiereDatumZeit(backup.createdAt)}.`
text: t("backup.overwriteWarningText", { date: formatiereDatumZeit(backup.createdAt) })
}),
erstelleElement("p", {
text: "Die Instanz wird dafür geordnet angehalten und danach wieder gestartet."
text: t("backup.instanceStopNotice")
}))));
// Nur ältere, unverschlüsselte Sicherungen mit Passphrase brauchen sie noch.
let passInput = null;
if (backup.secrets === "Passphrase" && !backup.verschluesselt) {
passInput = erstelleElement("input", {
type: "password", klasse: "eingabe", placeholder: "Passphrase der Sicherung…", required: true
type: "password", klasse: "eingabe", placeholder: t("backup.passphrasePlaceholder"), required: true
});
form.append(feld("Passphrase für Zugangsdaten", passInput));
form.append(feld(t("backup.passphraseLabel"), passInput));
}
let modal;
const restoreBtn = erstelleElement("button", {
type: "submit", klasse: "schaltflaeche warnung", text: "Jetzt wiederherstellen"
type: "submit", klasse: "schaltflaeche warnung", text: t("backup.restoreNow")
});
form.addEventListener("submit", async (e) => {
@@ -282,36 +280,36 @@ export async function rendereSicherung() {
const passphrase = passInput?.value?.trim() || null;
if (passInput && !passphrase) {
zeigeMeldung("Bitte gib die Passphrase an.", "warnung");
zeigeMeldung(t("backup.passphraseRequired"), "warnung");
passInput.focus();
return;
}
restoreBtn.disabled = true;
restoreBtn.textContent = "Stelle wieder her…";
restoreBtn.textContent = t("backup.restoring");
try {
const res = await stelleWiederHer(backup, { passphrase });
zeigeMeldung(res.meldung || "Wiederherstellung abgeschlossen.", "erfolg");
zeigeMeldung(res.meldung || t("backup.restoreSuccess"), "erfolg");
modal.schliessen();
ladeSicherungen();
} catch (err) {
// Our own key does not match — the backup comes from another installation.
// Nothing was stopped for this; the server checks that beforehand.
if (err.code === "fremde_lizenz") {
// Der eigene Schlüssel passt nicht - die Sicherung stammt von einer anderen
// Installation. Angehalten wurde dafür nichts; das prüft der Server vorher.
modal.schliessen();
frageLizenzschluessel(backup, err.message);
return;
}
zeigeMeldung(`Wiederherstellung fehlgeschlagen: ${err.message}`, "gefahr");
zeigeMeldung(t("backup.restoreFailed", { error: err.message }), "gefahr");
restoreBtn.disabled = false;
restoreBtn.textContent = "Jetzt wiederherstellen";
restoreBtn.textContent = t("backup.restoreNow");
}
});
modal = erstelleModal(`Sicherung wiederherstellen: ${backup.fileName}`, form, [
modal = erstelleModal(t("backup.restoreTitle", { file: backup.fileName }), form, [
erstelleElement("button", {
type: "button", klasse: "schaltflaeche", text: "Abbrechen", onclick: () => modal.schliessen()
type: "button", klasse: "schaltflaeche", text: t("common.cancel"), onclick: () => modal.schliessen()
}),
restoreBtn
]);
@@ -327,20 +325,19 @@ export async function rendereSicherung() {
const eingabe = erstelleElement("input", {
type: "password",
klasse: "eingabe",
placeholder: "Lizenzschlüssel der Installation, von der die Sicherung stammt…",
placeholder: t("backup.sourceLicensePlaceholder"),
autocomplete: "off",
required: true
});
form.append(
erstelleElement("p", { text: grund }),
feld("Lizenzschlüssel der Quelle", eingabe,
"Groß- und Kleinschreibung sowie Leerzeichen spielen keine Rolle.")
feld(t("backup.sourceLicenseLabel"), eingabe, t("backup.sourceLicenseHint"))
);
let modal;
const okBtn = erstelleElement("button", {
type: "submit", klasse: "schaltflaeche warnung", text: "Mit diesem Schlüssel wiederherstellen"
type: "submit", klasse: "schaltflaeche warnung", text: t("backup.restoreWithKey")
});
form.addEventListener("submit", async (e) => {
@@ -349,26 +346,26 @@ export async function rendereSicherung() {
if (!lizenzschluessel) { eingabe.focus(); return; }
okBtn.disabled = true;
okBtn.textContent = "Stelle wieder her…";
okBtn.textContent = t("backup.restoring");
try {
const res = await stelleWiederHer(backup, { lizenzschluessel });
zeigeMeldung(res.meldung || "Wiederherstellung abgeschlossen.", "erfolg");
zeigeMeldung(res.meldung || t("backup.restoreSuccess"), "erfolg");
modal.schliessen();
ladeSicherungen();
} catch (err) {
zeigeMeldung(err.code === "fremde_lizenz"
? "Dieser Lizenzschlüssel passt nicht zu der Sicherung."
: `Wiederherstellung fehlgeschlagen: ${err.message}`, "gefahr");
? t("backup.keyMismatch")
: t("backup.restoreFailed", { error: err.message }), "gefahr");
okBtn.disabled = false;
okBtn.textContent = "Mit diesem Schlüssel wiederherstellen";
okBtn.textContent = t("backup.restoreWithKey");
eingabe.select();
}
});
modal = erstelleModal("Lizenzschlüssel der Quelle erforderlich", form, [
modal = erstelleModal(t("backup.foreignLicenseTitle"), form, [
erstelleElement("button", {
type: "button", klasse: "schaltflaeche", text: "Abbrechen", onclick: () => modal.schliessen()
type: "button", klasse: "schaltflaeche", text: t("common.cancel"), onclick: () => modal.schliessen()
}),
okBtn
]);
@@ -382,28 +379,28 @@ export async function rendereSicherung() {
const loeschenBtn = erstelleElement("button", {
type: "button",
klasse: "schaltflaeche gefahr",
text: "Löschen",
text: t("common.delete"),
onclick: async () => {
loeschenBtn.disabled = true;
try {
const res = await api(`/verwaltung/instanzen/${pfadInstanz}/backups/${encodeURIComponent(fileName)}`, {
method: "DELETE"
});
zeigeMeldung(res.meldung || "Sicherung gelöscht.", "erfolg");
zeigeMeldung(res.meldung || t("backup.deleted"), "erfolg");
modal.schliessen();
ladeSicherungen();
} catch (err) {
zeigeMeldung(`Löschen fehlgeschlagen: ${err.message}`, "gefahr");
zeigeMeldung(t("backup.deleteFailed", { error: err.message }), "gefahr");
loeschenBtn.disabled = false;
}
}
});
modal = erstelleModal("Sicherung löschen",
erstelleElement("p", { text: `Soll die Sicherungsdatei '${fileName}' wirklich unwiderruflich gelöscht werden?` }),
modal = erstelleModal(t("backup.deleteTitle"),
erstelleElement("p", { text: t("backup.deleteConfirm", { file: fileName }) }),
[
erstelleElement("button", {
type: "button", klasse: "schaltflaeche", text: "Abbrechen", onclick: () => modal.schliessen()
type: "button", klasse: "schaltflaeche", text: t("common.cancel"), onclick: () => modal.schliessen()
}),
loeschenBtn
]);
@@ -416,13 +413,13 @@ export async function rendereSicherung() {
// ─── FTP-Upload (für alle Instanzen) ───
async function rendereFtp(bereich) {
bereich.replaceChildren(erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade FTP-Einstellungen…" }));
bereich.replaceChildren(erstelleElement("div", { klasse: "hinweis-zentriert", text: t("backup.ftpLoading") }));
let stand;
try {
stand = await api("/verwaltung/sicherung/ftp");
} catch (err) {
bereich.replaceChildren(erstelleKarte("FTP-Upload", [
bereich.replaceChildren(erstelleKarte(t("backup.ftpTitle"), [
erstelleElement("p", { klasse: "fehler-meldung", text: `Einstellungen nicht ladbar: ${err.message}` })
]));
return;
@@ -430,7 +427,7 @@ async function rendereFtp(bereich) {
const form = erstelleElement("form", { klasse: "formular" });
const an = checkbox("ftp-an", "Fertige Sicherungen automatisch hochladen", stand.enabled);
const an = checkbox("ftp-an", t("backup.ftpAutoUpload"), stand.enabled);
const host = erstelleElement("input", { type: "text", klasse: "eingabe", value: stand.host ?? "", placeholder: "ftp.example.org" });
const port = erstelleElement("input", { type: "number", klasse: "eingabe", value: String(stand.port ?? 21), min: "1", max: "65535" });
const benutzer = erstelleElement("input", { type: "text", klasse: "eingabe", value: stand.username ?? "", autocomplete: "off" });
@@ -438,40 +435,39 @@ async function rendereFtp(bereich) {
type: "password",
klasse: "eingabe",
autocomplete: "new-password",
placeholder: stand.kennwortGesetzt ? "(gespeichert — leer lassen für unverändert)" : "Kennwort"
placeholder: stand.kennwortGesetzt ? t("backup.ftpPasswordSaved") : t("backup.ftpPassword")
});
const ordner = erstelleElement("input", { type: "text", klasse: "eingabe", value: stand.remoteDirectory ?? "/clawd-backups" });
const behalten = erstelleElement("input", { type: "number", klasse: "eingabe", value: String(stand.keepCount ?? 14), min: "1", max: "365" });
const tls = checkbox("ftp-tls", "Nur mit TLS verbinden", stand.requireTls);
const tls = checkbox("ftp-tls", t("backup.ftpRequireTls"), stand.requireTls);
form.append(
erstelleElement("p", {
klasse: "form-hilfe",
text: "Die Sicherungen sind verschlüsselt — wer den Server liest, liest nichts Brauchbares. "
+ "Das Kennwort des Servers geht ohne TLS aber im Klartext über die Leitung."
text: t("backup.ftpHint")
}),
an.gruppe,
feld("Server", host),
feld("Port", port),
feld("Benutzer", benutzer),
feld("Kennwort", kennwort),
feld("Zielordner", ordner, "Darunter bekommt jede Instanz einen eigenen Ordner."),
feld("Sicherungen je Instanz behalten", behalten, "Ältere werden auf dem Server gelöscht."),
feld(t("backup.ftpServer"), host),
feld(t("backup.ftpPort"), port),
feld(t("backup.ftpUser"), benutzer),
feld(t("backup.ftpPassword"), kennwort),
feld(t("backup.ftpDirectory"), ordner, t("backup.ftpDirectoryHint")),
feld(t("backup.ftpKeepCount"), behalten, t("backup.ftpKeepCountHint")),
tls.gruppe
);
const speichernBtn = erstelleElement("button", { type: "submit", klasse: "schaltflaeche primaer", text: "Speichern" });
const speichernBtn = erstelleElement("button", { type: "submit", klasse: "schaltflaeche primaer", text: t("common.save") });
const testBtn = erstelleElement("button", {
type: "button",
klasse: "schaltflaeche",
text: "Verbindung testen",
text: t("backup.ftpTest"),
onclick: async () => {
testBtn.disabled = true;
try {
const res = await api("/verwaltung/sicherung/ftp/test", { method: "POST" });
zeigeMeldung(res.meldung, res.ok ? "erfolg" : "warnung", 8000);
} catch (err) {
zeigeMeldung(`Test fehlgeschlagen: ${err.message}`, "gefahr");
zeigeMeldung(t("backup.ftpTestFailed", { error: err.message }), "gefahr");
} finally {
testBtn.disabled = false;
}
@@ -495,11 +491,11 @@ async function rendereFtp(bereich) {
keepCount: Number(behalten.value) || 14
}
});
zeigeMeldung(res.meldung || "Gespeichert.", "erfolg");
zeigeMeldung(res.meldung || t("backup.ftpSaved"), "erfolg");
kennwort.value = "";
rendereFtp(bereich);
} catch (err) {
zeigeMeldung(`Speichern fehlgeschlagen: ${err.message}`, "gefahr");
zeigeMeldung(t("backup.ftpSaveFailed", { error: err.message }), "gefahr");
} finally {
speichernBtn.disabled = false;
}
@@ -507,5 +503,5 @@ async function rendereFtp(bereich) {
form.append(erstelleElement("div", { klasse: "gruppe-horizontal" }, testBtn, speichernBtn));
bereich.replaceChildren(erstelleKarte("FTP-Upload der Sicherungen", [form]));
bereich.replaceChildren(erstelleKarte(t("backup.ftpTitle"), [form]));
}