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
@@ -4,6 +4,7 @@
import { api } from "../modules/api.js";
import { abonnieren } from "../modules/events.js";
import { t } from "../modules/i18n.js";
import {
erstelleElement,
erstelleIcon,
@@ -30,7 +31,7 @@ export async function rendereAgenten(parameter = {}) {
const kopfZeile = erstelleElement("div", { klasse: "ansicht-kopf" });
kopfZeile.append(
erstelleElement("h1", { text: "Agenten" }),
erstelleElement("h1", { text: t("agents.title") }),
erstelleElement(
"button",
{
@@ -38,7 +39,7 @@ export async function rendereAgenten(parameter = {}) {
onclick: oeffneNeuerAgentDialog
},
erstelleIcon(PFADE.plus, 16),
erstelleElement("span", { text: " Neuer Agent" })
erstelleElement("span", { text: ` ${t("agents.newAgent")}` })
)
);
@@ -66,7 +67,7 @@ async function ladeUndRendereListe(zielId = null) {
if (!listeContainer) return;
listeContainer.replaceChildren(
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Agenten…" })
erstelleElement("div", { klasse: "hinweis-zentriert", text: t("agents.loading") })
);
try {
@@ -76,12 +77,12 @@ async function ladeUndRendereListe(zielId = null) {
if (!agenten || agenten.length === 0) {
const leerKasten = erstelleElement("div", { klasse: "hinweis-zentriert" });
leerKasten.append(
erstelleElement("p", { klasse: "gedaempft", text: "In dieser Instanz sind noch keine Agenten eingerichtet." }),
erstelleElement("p", { klasse: "gedaempft", text: t("agents.empty") }),
erstelleElement("button", {
klasse: "schaltflaeche primaer klein",
onclick: oeffneNeuerAgentDialog,
style: "margin-top: 10px;"
}, erstelleIcon(PFADE.plus, 14), erstelleElement("span", { text: " Ersten Agenten anlegen" }))
}, erstelleIcon(PFADE.plus, 14), erstelleElement("span", { text: ` ${t("agents.createFirst")}` }))
);
listeContainer.append(leerKasten);
rendereDetails(null);
@@ -105,20 +106,20 @@ async function ladeUndRendereListe(zielId = null) {
const top = erstelleElement("div", { klasse: "agent-karte-top" });
const name = erstelleElement("strong", { text: a.displayName });
const status = a.isRunning
? erstellePlakette("Beschäftigt", "warnung")
: erstellePlakette("Bereit", "neutral");
? erstellePlakette(t("status.busy"), "warnung")
: erstellePlakette(t("status.ready"), "neutral");
top.append(name, status);
const meta = erstelleElement("div", { klasse: "agent-karte-meta" });
meta.append(
erstelleElement("span", { klasse: "gedaempft", text: `ID: ${a.agentId}` }),
erstelleElement("span", { klasse: "gedaempft", text: ` · Modell: ${a.model}` })
erstelleElement("span", { klasse: "gedaempft", text: `${t("agents.id")}: ${a.agentId}` }),
erstelleElement("span", { klasse: "gedaempft", text: ` · ${t("agents.model")}: ${a.model}` })
);
const werkzeugeZaehler = a.tools ? a.tools.filter((t) => t.enabled).length : 0;
const werkzeugeText = erstelleElement("div", {
klasse: "agent-karte-werkzeuge gedaempft",
text: `${werkzeugeZaehler} Werkzeuge aktiv`
text: t("agents.toolsActive", { count: werkzeugeZaehler })
});
eintrag.append(top, meta, werkzeugeText);
@@ -128,9 +129,9 @@ async function ladeUndRendereListe(zielId = null) {
rendereDetails(auswahlId);
} catch (err) {
const text = err.instanzUnerreichbar
? "Die ausgewählte Instanz läuft derzeit nicht. Bitte starte sie zuerst im Kopfbereich oder unter 'Instanzen'."
? (t("instances.instanceOfflineError") || err.message)
: (err.instanzKonflikt
? "Keine aktive Instanz gewählt. Bitte wähle oben eine Instanz aus."
? (t("instances.noInstanceActiveError") || err.message)
: err.message);
listeContainer.replaceChildren(
erstelleElement("div", { klasse: "fehler-zentriert", text })
@@ -146,8 +147,8 @@ async function rendereDetails(agentId) {
if (!agentId) {
detailContainer.append(
erstelleKarte("Agent-Details", [
erstelleElement("p", { klasse: "gedaempft", text: "Wähle links einen Agenten aus." })
erstelleKarte(t("agents.details"), [
erstelleElement("p", { klasse: "gedaempft", text: t("agents.selectPrompt") })
])
);
return;
@@ -163,24 +164,24 @@ async function rendereDetails(agentId) {
"button",
{
klasse: "schaltflaeche flach",
title: "Mit diesem Agenten chatten",
title: t("agents.chatTooltip"),
onclick: () => {
window.location.hash = `#/chat/${encodeURIComponent(agent.agentId)}`;
}
},
erstelleIcon(PFADE.chat, 14),
erstelleElement("span", { text: " Chatten" })
erstelleElement("span", { text: ` ${t("agents.chat")}` })
);
const loeschenBtn = erstelleElement(
"button",
{
klasse: "schaltflaeche gefahr flach",
title: "Diesen Agenten löschen",
title: t("agents.deleteTooltip"),
onclick: () => loescheAgent(agent.agentId, agent.displayName)
},
erstelleIcon(PFADE.muelleimer, 14),
erstelleElement("span", { text: " Löschen" })
erstelleElement("span", { text: ` ${t("agents.delete")}` })
);
aktionen.append(zumChatBtn, loeschenBtn);
@@ -188,10 +189,10 @@ async function rendereDetails(agentId) {
// Stammdaten-Karte
const stammdatenListe = erstelleElement("dl", { klasse: "datenliste" });
const felder = [
["Name", agent.displayName],
["Kennung (ID)", agent.agentId],
["Modell", agent.model],
["Beschreibung", agent.description || "—"]
[t("agents.name"), agent.displayName],
[t("agents.id"), agent.agentId],
[t("agents.model"), agent.model],
[t("agents.description"), agent.description || "—"]
];
for (const [bez, wert] of felder) {
@@ -208,7 +209,7 @@ async function rendereDetails(agentId) {
if (!agent.tools || agent.tools.length === 0) {
werkzeugeListe.append(
erstelleElement("p", { klasse: "gedaempft", text: "Keine Werkzeuge für diesen Agenten konfiguriert." })
erstelleElement("p", { klasse: "gedaempft", text: t("agents.noTools") })
);
} else {
for (const tool of agent.tools) {
@@ -228,11 +229,11 @@ async function rendereDetails(agentId) {
"button",
{
klasse: "schaltflaeche flach klein",
title: "Werkzeug-Einstellungen einsehen",
title: t("agents.configTooltip"),
onclick: () => zeigeWerkzeugEinstellungen(agent.agentId, tool.name)
},
erstelleIcon(PFADE.einstellungen, 14),
erstelleElement("span", { text: " Konfiguration" })
erstelleElement("span", { text: ` ${t("agents.configuration")}` })
);
const toggleLabel = erstelleElement("label", { klasse: "schalter" });
@@ -247,10 +248,16 @@ async function rendereDetails(agentId) {
{ method: "PUT" }
);
tool.enabled = aktiv;
zeigeMeldung(`Werkzeug '${tool.name}' ${aktiv ? "aktiviert" : "deaktiviert"}.`, "info");
zeigeMeldung(
t("agents.toolToggled", {
tool: tool.name,
status: aktiv ? t("agents.enabled") : t("agents.disabled")
}),
"info"
);
} catch (err) {
e.target.checked = !aktiv;
zeigeMeldung(`Änderung fehlgeschlagen: ${err.message}`, "gefahr");
zeigeMeldung(t("agents.saveFailed", { error: err.message }), "gefahr");
}
}
});
@@ -263,7 +270,7 @@ async function rendereDetails(agentId) {
}
}
const werkzeugeKarte = erstelleKarte("Verfügbare Werkzeuge", [werkzeugeListe]);
const werkzeugeKarte = erstelleKarte(t("agents.availableTools"), [werkzeugeListe]);
detailContainer.append(basisKarte, werkzeugeKarte);
}
@@ -277,14 +284,14 @@ function oeffneNeuerAgentDialog() {
const idFeld = erstelleElement("input", {
type: "text",
required: true,
placeholder: "z.B. scout",
placeholder: t("agents.idPlaceholder"),
id: "neu-agent-id"
});
const nameFeld = erstelleElement("input", {
type: "text",
required: true,
placeholder: "z.B. Scout Agent",
placeholder: t("agents.namePlaceholder"),
id: "neu-agent-name"
});
@@ -298,18 +305,18 @@ function oeffneNeuerAgentDialog() {
const descFeld = erstelleElement("textarea", {
rows: "3",
placeholder: "Optionale Beschreibung des Agenten…",
placeholder: t("agents.descPlaceholder"),
id: "neu-agent-desc"
});
formular.append(
erstelleElement("label", { text: "Agent-ID (eindeutiger Schlüssel):" }),
erstelleElement("label", { text: t("agents.idLabel") }),
idFeld,
erstelleElement("label", { text: "Anzeigename:" }),
erstelleElement("label", { text: t("agents.nameLabel") }),
nameFeld,
erstelleElement("label", { text: "Modell (OpenRouter):" }),
erstelleElement("label", { text: t("agents.modelLabel") }),
modellFeld,
erstelleElement("label", { text: "Beschreibung:" }),
erstelleElement("label", { text: t("agents.descLabel") }),
descFeld
);
@@ -320,7 +327,7 @@ function oeffneNeuerAgentDialog() {
klasse: "schaltflaeche flach",
onclick: () => modal.schliessen()
},
erstelleElement("span", { text: "Abbrechen" })
erstelleElement("span", { text: t("common.cancel") })
);
const speichernBtn = erstelleElement(
@@ -333,10 +340,10 @@ function oeffneNeuerAgentDialog() {
formular.requestSubmit();
}
},
erstelleElement("span", { text: "Agent anlegen" })
erstelleElement("span", { text: t("agents.createButton") })
);
const modal = erstelleModal("Neuen Agenten anlegen", formular, [abbrechenBtn, speichernBtn]);
const modal = erstelleModal(t("agents.newAgentTitle"), formular, [abbrechenBtn, speichernBtn]);
formular.addEventListener("submit", async (e) => {
e.preventDefault();
@@ -353,10 +360,10 @@ function oeffneNeuerAgentDialog() {
}
});
modal.schliessen();
zeigeMeldung(`Agent '${nameFeld.value.trim()}' wurde erfolgreich angelegt.`, "erfolg");
zeigeMeldung(t("agents.createdSuccess", { name: nameFeld.value.trim() }), "erfolg");
await ladeUndRendereListe(idFeld.value.trim());
} catch (err) {
zeigeMeldung(`Fehler beim Anlegen: ${err.message}`, "gefahr");
zeigeMeldung(t("agents.createFailed", { error: err.message }), "gefahr");
} finally {
speichernBtn.disabled = false;
}
@@ -366,30 +373,30 @@ function oeffneNeuerAgentDialog() {
}
async function loescheAgent(agentId, name) {
if (!confirm(`Soll der Agent '${name}' (${agentId}) wirklich gelöscht werden?`)) return;
if (!confirm(t("agents.deleteConfirm", { name, id: agentId }))) return;
try {
await api(`/agents/${encodeURIComponent(agentId)}`, { method: "DELETE" });
zeigeMeldung(`Agent '${name}' wurde gelöscht.`, "info");
zeigeMeldung(t("agents.deleted", { name }), "info");
await ladeUndRendereListe();
} catch (err) {
zeigeMeldung(`Löschen fehlgeschlagen: ${err.message}`, "gefahr");
zeigeMeldung(t("agents.deleteFailed", { error: err.message }), "gefahr");
}
}
async function zeigeWerkzeugEinstellungen(agentId, toolName) {
const box = erstelleElement("div", { klasse: "einstellungen-box" });
box.append(erstelleElement("p", { klasse: "gedaempft", text: "Lade Konfiguration…" }));
box.append(erstelleElement("p", { klasse: "gedaempft", text: t("agents.loadingSettings") }));
const schliessenBtn = erstelleElement("button", {
type: "button",
klasse: "schaltflaeche flach",
onclick: () => modal.schliessen(),
text: "Schließen"
text: t("common.close")
});
const modal = erstelleModal(
`Einstellungen: ${toolName} (${agentId})`,
t("agents.settingsTitle", { tool: toolName, agent: agentId }),
box,
[schliessenBtn]
);
@@ -403,13 +410,13 @@ async function zeigeWerkzeugEinstellungen(agentId, toolName) {
const hinweis = erstelleElement("p", {
klasse: "hinweis",
text: "Geheime Werte (Passwörter, Schlüssel) werden vom Server maskiert ausgegeben."
text: t("agents.secretHint")
});
box.append(hinweis);
if (!daten || Object.keys(daten).length === 0) {
box.append(
erstelleElement("p", { klasse: "gedaempft", text: "Keine individuellen Einstellungen hinterlegt." })
erstelleElement("p", { klasse: "gedaempft", text: t("agents.noSettings") })
);
} else {
const dl = erstelleElement("dl", { klasse: "datenliste" });
@@ -422,7 +429,7 @@ async function zeigeWerkzeugEinstellungen(agentId, toolName) {
}
} catch (err) {
box.replaceChildren(
erstelleElement("p", { klasse: "fehler", text: `Laden fehlgeschlagen: ${err.message}` })
erstelleElement("p", { klasse: "fehler", text: t("agents.loadFailed", { error: err.message }) })
);
}
@@ -431,11 +438,14 @@ async function zeigeWerkzeugEinstellungen(agentId, toolName) {
await zeigeWerkzeugrechte(agentId, toolName, rechteBox);
}
const WIRKUNG = {
auto: ["läuft", "erfolg"],
approve: ["einzeln freigeben", "warnung"],
deny: ["gesperrt", "gefahr"]
};
function getWirkung(effective) {
const map = {
auto: [t("agents.effectAuto"), "erfolg"],
approve: [t("agents.effectApprove"), "warnung"],
deny: [t("agents.effectDeny"), "gefahr"]
};
return map[effective] ?? [effective, "neutral"];
}
/**
* #641: Rechte je Operation. "erlaubt" laeuft ohne Rueckfrage, "freigabe" wird einzeln
@@ -448,17 +458,17 @@ async function zeigeWerkzeugrechte(agentId, toolName, box) {
try {
rechte = await api(pfad);
} catch (err) {
box.replaceChildren(erstelleElement("p", { klasse: "gedaempft", text: `Rechte nicht lesbar: ${err.message}` }));
box.replaceChildren(erstelleElement("p", { klasse: "gedaempft", text: t("agents.loadFailed", { error: err.message }) }));
return;
}
box.replaceChildren(erstelleElement("h3", { text: "Rechte" }));
box.replaceChildren(erstelleElement("h3", { text: t("agents.rights") }));
if (!rechte.operations || rechte.operations.length === 0) {
box.append(
erstelleElement("p", {
klasse: "gedaempft",
text: "Dieses Werkzeug kennt keine einzelnen Operationen. Es gilt die Standardregel."
text: t("agents.noOperations")
})
);
return;
@@ -468,14 +478,14 @@ async function zeigeWerkzeugrechte(agentId, toolName, box) {
erstelleElement("p", {
klasse: "hinweis",
text: rechte.configured
? "Erlaubte Operationen laufen ohne Rückfrage, alle anderen werden einzeln zur Freigabe vorgelegt, verbotene gar nicht erst."
: "Noch keine Rechte gesetzt: Es gilt die Vorgabe des Werkzeugs. Mit dem Speichern werden die Einstellungen unten verbindlich."
? t("agents.configuredHint")
: t("agents.notConfiguredHint")
})
);
const tabelle = erstelleElement("table", { klasse: "daten-tabelle rechte-tabelle" });
const kopf = erstelleElement("tr", {});
for (const titel of ["Operation", "Einstellung", "Wirkung"]) {
for (const titel of [t("agents.operation"), t("agents.setting"), t("agents.effect")]) {
kopf.append(erstelleElement("th", { text: titel }));
}
tabelle.append(kopf);
@@ -485,9 +495,9 @@ async function zeigeWerkzeugrechte(agentId, toolName, box) {
const select = erstelleElement(
"select",
{ disabled: op.alwaysApproval },
erstelleElement("option", { value: "erlaubt", text: "erlaubt" }),
erstelleElement("option", { value: "freigabe", text: "einzeln freigeben" }),
erstelleElement("option", { value: "verboten", text: "verboten" })
erstelleElement("option", { value: "erlaubt", text: t("agents.allowed") }),
erstelleElement("option", { value: "freigabe", text: t("agents.singleApproval") }),
erstelleElement("option", { value: "verboten", text: t("agents.forbidden") })
);
// Ohne gesetzte Rechte die tatsaechliche Wirkung vorbelegen - sonst wuerde das erste
// Speichern z.B. das freie Lesen still in eine Freigabepflicht verwandeln.
@@ -496,14 +506,14 @@ async function zeigeWerkzeugrechte(agentId, toolName, box) {
: op.effective === "auto" ? "erlaubt" : op.effective === "deny" ? "verboten" : "freigabe";
auswahl.set(op.operation, select);
const [wirkungText, wirkungArt] = WIRKUNG[op.effective] ?? [op.effective, "neutral"];
const [wirkungText, wirkungArt] = getWirkung(op.effective);
const wirkung = erstelleElement("td", {}, erstellePlakette(wirkungText, wirkungArt));
if (op.standingApprovalId) {
wirkung.append(" ", erstellePlakette(`Dauerfreigabe #${op.standingApprovalId}`, "neutral"));
wirkung.append(" ", erstellePlakette(t("agents.standingApproval", { id: op.standingApprovalId }), "neutral"));
}
wirkung.append(erstelleElement("div", {
klasse: "grund",
text: op.alwaysApproval ? "Änderungen an Agenten brauchen immer eine Freigabe." : op.reason
text: op.alwaysApproval ? t("agents.alwaysApprovalReason") : op.reason
}));
const zeile = erstelleElement("tr", {});
@@ -515,7 +525,7 @@ async function zeigeWerkzeugrechte(agentId, toolName, box) {
const speichern = erstelleElement("button", {
type: "button",
klasse: "schaltflaeche primaer klein",
text: "Rechte speichern",
text: t("agents.saveRights"),
onclick: async () => {
const allowed = [];
const denied = [];
@@ -527,10 +537,10 @@ async function zeigeWerkzeugrechte(agentId, toolName, box) {
speichern.disabled = true;
try {
const res = await api(pfad, { method: "PUT", body: { allowed, denied } });
zeigeMeldung(res?.message ?? "Rechte gespeichert.", "erfolg");
zeigeMeldung(res?.message ?? t("agents.rightsSaved"), "erfolg");
await zeigeWerkzeugrechte(agentId, toolName, box);
} catch (err) {
zeigeMeldung(`Speichern fehlgeschlagen: ${err.message}`, "gefahr");
zeigeMeldung(t("agents.saveFailed", { error: err.message }), "gefahr");
speichern.disabled = false;
}
}