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
@@ -1,9 +1,9 @@
// ClawdDotNet — Instanzverwaltung
//
// Leitfaden: Übersicht, Anlegen, Starten, Anhalten, Desktop-Fenster nach vorn holen,
// Auswahl der aktiven Instanz für das WebUI. Kein innerHTML, strikt textContent.
// Overview, create, start, stop, bring desktop window to front, active instance selection.
import { api, holeAktiveInstanz, setzeAktiveInstanz } from "../modules/api.js";
import { t } from "../modules/i18n.js";
import {
erstelleElement,
erstelleIcon,
@@ -28,7 +28,7 @@ export async function rendereInstanzen() {
const kopfZeile = erstelleElement("div", { klasse: "ansicht-kopf" });
const titelGruppe = erstelleElement("div", { klasse: "gruppe-horizontal" });
titelGruppe.append(erstelleElement("h1", { text: "Instanzverwaltung" }));
titelGruppe.append(erstelleElement("h1", { text: t("instances.title") }));
const aktionenGruppe = erstelleElement("div", { klasse: "gruppe-horizontal" });
@@ -36,22 +36,22 @@ export async function rendereInstanzen() {
"button",
{
klasse: "schaltflaeche flach",
title: "Instanzliste manuell aktualisieren",
title: t("instances.refresh"),
onclick: () => ladeInstanzen()
},
erstelleIcon(PFADE.aktualisieren, 16),
erstelleElement("span", { text: " Aktualisieren" })
erstelleElement("span", { text: " " + t("instances.refresh") })
);
const neuBtn = erstelleElement(
"button",
{
klasse: "schaltflaeche primaer",
title: "Neue Instanz anlegen",
title: t("instances.newInstance"),
onclick: oeffneNeueInstanzDialog
},
erstelleIcon(PFADE.plus, 16),
erstelleElement("span", { text: " Neue Instanz" })
erstelleElement("span", { text: " " + t("instances.newInstance") })
);
aktionenGruppe.append(aktualisierenBtn, neuBtn);
@@ -64,7 +64,6 @@ export async function rendereInstanzen() {
await ladeInstanzen();
// Hintergrund-Polling alle 4 Sekunden, da sich Zustände ohne Klick ändern können
aktualisierungsIntervall = setInterval(() => {
ladeInstanzen(true);
}, 4000);
@@ -86,7 +85,7 @@ async function ladeInstanzen(istHintergrund = false) {
if (!istHintergrund && raster.children.length === 0) {
raster.replaceChildren(
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Instanzen…" })
erstelleElement("div", { klasse: "hinweis-zentriert", text: t("common.loading") })
);
}
@@ -97,10 +96,10 @@ async function ladeInstanzen(istHintergrund = false) {
if (!Array.isArray(instanzen) || instanzen.length === 0) {
raster.append(
erstelleKarte("Keine Instanzen gefunden", [
erstelleKarte(t("common.none"), [
erstelleElement("p", {
klasse: "gedaempft",
text: "Es sind noch keine Instanzen eingerichtet. Lege mit 'Neue Instanz' eine an."
text: "..."
})
])
);
@@ -114,87 +113,79 @@ async function ladeInstanzen(istHintergrund = false) {
const zustandInfo = instanzZustandInfo(instanz.zustand);
const istLaufend = instanz.zustand === "Running";
const istStumm = instanz.zustand === "Silent";
const istAbgestuerzt = instanz.zustand === "Crashed";
// Instanzkarte
const karteKopfAktionen = erstelleElement("div", { klasse: "gruppe-horizontal" });
if (istAktiv) {
karteKopfAktionen.append(erstellePlakette("Aktiv im WebUI", "erfolg"));
karteKopfAktionen.append(erstellePlakette(t("instances.activeBadge"), "erfolg"));
}
karteKopfAktionen.append(erstellePlakette(zustandInfo.text, zustandInfo.art));
// Details
const dl = erstelleElement("dl", { klasse: "datenliste" });
dl.append(
erstelleElement("dt", { text: "Instanz-Kennung" }),
erstelleElement("dt", { text: "ID" }),
erstelleElement("dd", {}, erstelleElement("code", { text: instanz.id })),
erstelleElement("dt", { text: "Zustand" }),
erstelleElement("dt", { text: t("common.status") }),
erstelleElement("dd", { text: zustandInfo.text }),
erstelleElement("dt", { text: "Prozess (PID)" }),
erstelleElement("dt", { text: "PID" }),
erstelleElement("dd", { text: instanz.prozess ? String(instanz.prozess) : "—" }),
erstelleElement("dt", { text: "Autostart durch Manager" }),
erstelleElement("dt", { text: t("instances.autostart") }),
erstelleElement("dd", {
text: instanz.autostart ? "Aktiviert" : "Aus",
text: instanz.autostart ? (t("common.yes")) : (t("common.no")),
klasse: instanz.autostart ? "" : "gedaempft"
})
);
// Aktionsleiste
const fussAktionen = erstelleElement("div", { klasse: "instanz-aktionen" });
// Auswählen-Knopf
const waehlenBtn = erstelleElement(
"button",
{
klasse: istAktiv ? "schaltflaeche klein" : "schaltflaeche primaer klein",
disabled: istAktiv,
title: istAktiv ? "Diese Instanz wird aktuell im WebUI bedient" : "Diese Instanz für das WebUI auswählen",
title: istAktiv ? t("instances.activeBadge") : t("instances.selectActive"),
onclick: () => {
setzeAktiveInstanz(instanz.id);
zeigeMeldung(`Instanz '${instanz.name}' im WebUI ausgewählt.`, "info");
zeigeMeldung(`'${instanz.name}' selected.`, "info");
ladeInstanzen();
}
},
erstelleElement("span", { text: istAktiv ? "Ausgewählt" : "Auswählen" })
erstelleElement("span", { text: istAktiv ? t("instances.activeBadge") : t("instances.selectActive") })
);
// Starten-Knopf
const startenBtn = erstelleElement(
"button",
{
klasse: "schaltflaeche flach klein",
disabled: istLaufend,
title: istLaufend ? "Instanz läuft bereits" : "Instanz starten",
title: t("instances.start"),
onclick: () => starteInstanz(instanz)
},
erstelleIcon(PFADE.abspielen, 14),
erstelleElement("span", { text: " Starten" })
erstelleElement("span", { text: " " + t("instances.start") })
);
// Anhalten-Knopf
const anhaltenBtn = erstelleElement(
"button",
{
klasse: "schaltflaeche gefahr flach klein",
disabled: !istLaufend && !istStumm,
title: (!istLaufend && !istStumm) ? "Instanz läuft nicht" : "Instanz geordnet anhalten",
title: t("instances.stop"),
onclick: () => stoppeInstanz(instanz)
},
erstelleIcon(PFADE.stop, 14),
erstelleElement("span", { text: " Anhalten" })
erstelleElement("span", { text: " " + t("instances.stop") })
);
// Fenster nach vorn
const fensterBtn = erstelleElement(
"button",
{
klasse: "schaltflaeche flach klein",
disabled: !istLaufend,
title: !istLaufend ? "Instanz läuft nicht" : "Desktop-Fenster dieser Instanz in den Vordergrund holen",
title: t("instances.bringToFront"),
onclick: () => holeFensterNachVorn(instanz)
},
erstelleIcon(PFADE.fenster, 14),
erstelleElement("span", { text: " Fenster" })
erstelleElement("span", { text: " " + t("instances.bringToFront") })
);
fussAktionen.append(waehlenBtn, startenBtn, anhaltenBtn, fensterBtn);
@@ -209,7 +200,7 @@ async function ladeInstanzen(istHintergrund = false) {
} catch (err) {
if (!istHintergrund) {
raster.replaceChildren(
erstelleElement("div", { klasse: "fehler-zentriert", text: `Fehler beim Laden: ${err.message}` })
erstelleElement("div", { klasse: "fehler-zentriert", text: `Error: ${err.message}` })
);
}
}
@@ -220,17 +211,16 @@ async function starteInstanz(instanz) {
const antwort = await api(`/verwaltung/instanzen/${encodeURIComponent(instanz.id)}/start`, {
method: "POST"
});
zeigeMeldung(antwort?.meldung || `Instanz '${instanz.name}' wird gestartet…`, "erfolg");
// Wenn bisher keine Instanz im WebUI gewählt war, diese direkt wählen
zeigeMeldung(antwort?.meldung || `Starting '${instanz.name}'…`, "erfolg");
if (!holeAktiveInstanz()) {
setzeAktiveInstanz(instanz.id);
}
await ladeInstanzen();
} catch (err) {
if (err.lizenzFehlt) {
zeigeMeldung("Start abgelehnt: Ohne gültige Lizenz startet keine Instanz (HTTP 402).", "gefahr", 6000);
zeigeMeldung("License required (HTTP 402).", "gefahr", 6000);
} else {
zeigeMeldung(`Start fehlgeschlagen: ${err.message}`, "gefahr");
zeigeMeldung(`Failed to start: ${err.message}`, "gefahr");
}
}
}
@@ -245,47 +235,40 @@ function oeffneSicherheitsStoppDialog(instanz) {
const inhalt = erstelleElement("div", { klasse: "formular-raster" });
// Sicherheits-Warnbox
const sicherheitsBox = erstelleElement("div", { klasse: "sicherheits-box" });
const boxTitel = erstelleElement(
"h3",
{},
erstelleIcon(PFADE.ausrufezeichen, 18),
erstelleElement("span", { text: "Achtung: Produktivinstanz beenden" })
erstelleElement("span", { text: " Stop Instance" })
);
const boxText1 = erstelleElement("p", {
text: "ClawdDotNet-Instanzen sind auf Dauerbetrieb ausgelegt. Das Beenden stoppt alle aktiven Agenten, Automationen, Web-Zugriffe und Hintergrundaufgaben dieser Instanz geordnet."
text: `Stopping instance '${instanz.name}' will gracefully shut down all active agents and processes.`
});
const boxText2 = erstelleElement("p", {
text: "Um ein versehentliches Beenden durch unbedachtes Klicken zu verhindern, ist die Bestätigung für 10 Sekunden gesperrt."
});
sicherheitsBox.append(boxTitel, boxText1, boxText2);
sicherheitsBox.append(boxTitel, boxText1);
// Instanzdetails
const dl = erstelleElement("dl", { klasse: "datenliste" });
dl.append(
erstelleElement("dt", { text: "Instanz" }),
erstelleElement("dt", { text: "Instance" }),
erstelleElement("dd", {}, erstelleElement("strong", { text: instanz.name })),
erstelleElement("dt", { text: "Kennung" }),
erstelleElement("dt", { text: "ID" }),
erstelleElement("dd", {}, erstelleElement("code", { text: instanz.id })),
erstelleElement("dt", { text: "Prozess (PID)" }),
erstelleElement("dt", { text: "PID" }),
erstelleElement("dd", { text: instanz.prozess ? String(instanz.prozess) : "—" })
);
// Countdown-Anzeige
const countdownBox = erstelleElement("div", {
klasse: "sicherheits-countdown",
text: `Bestätigung gesperrt: Noch ${verbleibend} Sekunden…`
text: `Confirmation locked: ${verbleibend}s…`
});
inhalt.append(sicherheitsBox, dl, countdownBox);
// Aktionen
const abbrechenBtn = erstelleElement("button", {
type: "button",
klasse: "schaltflaeche flach",
onclick: () => modal.schliessen(),
text: "Abbrechen (Instanz läuft weiter)"
text: t("common.cancel")
});
const anhaltenBtn = erstelleElement(
@@ -296,11 +279,11 @@ function oeffneSicherheitsStoppDialog(instanz) {
disabled: true
},
erstelleIcon(PFADE.stop, 14),
erstelleElement("span", { text: ` Instanz anhalten (${verbleibend}s)` })
erstelleElement("span", { text: ` ${t("instances.stop")} (${verbleibend}s)` })
);
const modal = erstelleModal(
`Instanz anhalten — ${instanz.name}`,
`${t("instances.stop")} — ${instanz.name}`,
inhalt,
[abbrechenBtn, anhaltenBtn],
{
@@ -316,17 +299,17 @@ function oeffneSicherheitsStoppDialog(instanz) {
timerId = setInterval(() => {
verbleibend--;
if (verbleibend > 0) {
countdownBox.textContent = `Bestätigung gesperrt: Noch ${verbleibend} Sekunden…`;
countdownBox.textContent = `Confirmation locked: ${verbleibend}s…`;
const textSpan = anhaltenBtn.querySelector("span");
if (textSpan) textSpan.textContent = ` Instanz anhalten (${verbleibend}s)`;
if (textSpan) textSpan.textContent = ` ${t("instances.stop")} (${verbleibend}s)`;
} else {
clearInterval(timerId);
timerId = null;
anhaltenBtn.disabled = false;
const textSpan = anhaltenBtn.querySelector("span");
if (textSpan) textSpan.textContent = " Instanz jetzt endgültig anhalten";
if (textSpan) textSpan.textContent = ` ${t("instances.stop")}`;
countdownBox.className = "sicherheits-countdown abgelaufen";
countdownBox.textContent = "Sperre aufgehoben — Bestätigung jetzt möglich.";
countdownBox.textContent = "Unlocked — confirmation ready.";
}
}, 1000);
@@ -334,19 +317,19 @@ function oeffneSicherheitsStoppDialog(instanz) {
if (verbleibend > 0) return;
anhaltenBtn.disabled = true;
const textSpan = anhaltenBtn.querySelector("span");
if (textSpan) textSpan.textContent = " Wird angehalten…";
if (textSpan) textSpan.textContent = " Stopping…";
try {
const antwort = await api(`/verwaltung/instanzen/${encodeURIComponent(instanz.id)}/stop`, {
method: "POST"
});
modal.schliessen();
zeigeMeldung(antwort?.meldung || `Instanz '${instanz.name}' beendet.`, "info");
zeigeMeldung(antwort?.meldung || `'${instanz.name}' stopped.`, "info");
await ladeInstanzen();
} catch (err) {
zeigeMeldung(`Anhalten fehlgeschlagen: ${err.message}`, "gefahr");
zeigeMeldung(`Stop failed: ${err.message}`, "gefahr");
anhaltenBtn.disabled = false;
if (textSpan) textSpan.textContent = " Instanz jetzt endgültig anhalten";
if (textSpan) textSpan.textContent = ` ${t("instances.stop")}`;
}
});
@@ -358,9 +341,9 @@ async function holeFensterNachVorn(instanz) {
const antwort = await api(`/verwaltung/instanzen/${encodeURIComponent(instanz.id)}/fenster`, {
method: "POST"
});
zeigeMeldung(antwort?.meldung || `Fenster von '${instanz.name}' nach vorn geholt.`, "info");
zeigeMeldung(antwort?.meldung || `Brought '${instanz.name}' to front.`, "info");
} catch (err) {
zeigeMeldung(`Fensteraufruf fehlgeschlagen: ${err.message}`, "warnung");
zeigeMeldung(`Window focus failed: ${err.message}`, "warnung");
}
}
@@ -373,17 +356,17 @@ function oeffneNeueInstanzDialog() {
const nameInput = erstelleElement("input", {
type: "text",
required: true,
placeholder: "z.B. Produktion oder Agenten-Labor",
placeholder: t("instances.instanceNamePlaceholder"),
autocomplete: "off"
});
const hinweis = erstelleElement("p", {
klasse: "hinweis",
text: "Der Instanzmanager legt dafür ein eigenes Verzeichnis mit getrennten Protokollen und Einstellungen an."
text: "..."
});
formular.append(
erstelleElement("label", { text: "Name der neuen Instanz:" }),
erstelleElement("label", { text: t("instances.instanceNameLabel") + ":" }),
nameInput,
hinweis
);
@@ -392,20 +375,20 @@ function oeffneNeueInstanzDialog() {
type: "button",
klasse: "schaltflaeche flach",
onclick: () => modal.schliessen(),
text: "Abbrechen"
text: t("common.cancel")
});
// Kein eigenes onclick und kein form-Attribut: erstelleModal verknüpft Submit-Knöpfe
// im Fuß selbst mit dem Formular. Beides zusammen löste requestSubmit() zweimal aus —
// ein Klick, zwei POST, zwei Instanzen. `disabled` hilft dagegen nicht, denn
// form.requestSubmit() schert sich nicht um den Zustand eines Knopfes.
// No onclick and no form attribute of its own: erstelleModal wires submit buttons in the
// footer to the form itself. Both together fired requestSubmit() twice — one click, two
// POSTs, two instances. `disabled` does not help, because form.requestSubmit() ignores
// a button's state.
const erstellenBtn = erstelleElement("button", {
type: "submit",
klasse: "schaltflaeche primaer",
text: "Instanz anlegen"
text: t("instances.createTitle")
});
const modal = erstelleModal("Neue Instanz anlegen", formular, [abbrechenBtn, erstellenBtn]);
const modal = erstelleModal(t("instances.createTitle"), formular, [abbrechenBtn, erstellenBtn]);
formular.addEventListener("submit", async (e) => {
e.preventDefault();
@@ -420,10 +403,10 @@ function oeffneNeueInstanzDialog() {
body: { name }
});
modal.schliessen();
zeigeMeldung(antwort?.meldung || `Instanz '${name}' erfolgreich angelegt.`, "erfolg");
zeigeMeldung(antwort?.meldung || t("instances.created"), "erfolg");
await ladeInstanzen();
} catch (err) {
zeigeMeldung(`Fehler beim Anlegen: ${err.message}`, "gefahr");
zeigeMeldung(t("instances.createError", { error: err.message }), "gefahr");
} finally {
erstellenBtn.disabled = false;
}