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,11 +1,14 @@
// ClawdDotNet — Aufgaben (Taskboard)
//
// Seit #594 die einzige Stelle für geplante Arbeit. Der frühere Reiter „Geplante Läufe (Jobs)“
// bearbeitete die Alt-Konfiguration scheduler/toolJobs; nach der ersten Übernahme ins Taskboard
// wirkte dort keine Änderung mehr. Termin, nächster und letzter Lauf, Pausieren und Sofortlauf
// stehen jetzt an der Aufgabe selbst.
// Central taskboard view for scheduled and manual tasks.
//
// Since #594 the only place for scheduled work. The former tab "Scheduled runs (Jobs)"
// edited the legacy config scheduler/toolJobs; after the first migration into the taskboard
// no change made there had any effect. Schedule, next and last run, pause and run-now
// now live on the task itself.
import { api } from "../modules/api.js";
import { t } from "../modules/i18n.js";
import {
erstelleElement,
erstelleIcon,
@@ -18,16 +21,19 @@ import {
zeigeMeldung
} from "../modules/dom.js";
const STATUS_TEXT = {
Backlog: "Pausiert",
Todo: "Bereit",
InProgress: "Läuft",
InReview: "Im Review",
Done: "Erledigt",
Canceled: "Verworfen",
Blocked: "Blockiert",
Archived: "Archiviert"
};
function holeStatusText(status) {
switch (status) {
case "Backlog": return t("status.paused");
case "Todo": return t("status.ready");
case "InProgress": return t("status.running");
case "InReview": return t("status.inReview");
case "Done": return t("status.done");
case "Canceled": return t("status.canceled");
case "Blocked": return t("status.blocked");
case "Archived": return t("status.archived");
default: return status || t("common.unknown");
}
}
const STATUS_ART = {
Backlog: "warnung",
@@ -37,7 +43,6 @@ const STATUS_ART = {
Blocked: "warnung"
};
// Laufstatus aus AgentRunStatus (#577/#581 brachten EmptyResponse, Incomplete, RepeatedFailure).
const LAUF_ART = {
Completed: "erfolg",
Truncated: "warnung",
@@ -49,7 +54,6 @@ const LAUF_ART = {
Failed: "gefahr"
};
const TERMIN_ART = { cron: "Cron", every: "Intervall", at: "Zeitpunkt" };
const ABGESCHLOSSEN = ["Done", "Canceled", "Archived"];
let zeigeAbgeschlossene = false;
@@ -73,12 +77,12 @@ export async function rendereAufgaben() {
ladeTaskboard();
}
}),
erstelleElement("span", { text: "Erledigte zeigen" })
erstelleElement("span", { text: t("tasks.showCompleted") })
);
const neuladen = erstelleElement(
"button",
{ klasse: "schaltflaeche flach", title: "Neu laden", onclick: () => ladeTaskboard() },
{ klasse: "schaltflaeche flach", title: t("common.refresh"), onclick: () => ladeTaskboard() },
erstelleIcon(PFADE.aktualisieren, 16)
);
@@ -86,11 +90,11 @@ export async function rendereAufgaben() {
"button",
{ klasse: "schaltflaeche primaer", onclick: () => oeffneAufgabenDialog(null) },
erstelleIcon(PFADE.plus, 16),
erstelleElement("span", { text: " Neue Aufgabe" })
erstelleElement("span", { text: " " + t("tasks.newTask") })
);
kopf.append(
erstelleElement("h1", { text: "Aufgaben" }),
erstelleElement("h1", { text: t("tasks.title") }),
erstelleElement("div", { klasse: "gruppe-horizontal" }, abgeschlossenSchalter, neuladen, neu)
);
@@ -105,7 +109,7 @@ async function ladeTaskboard() {
if (!container) return;
container.replaceChildren(
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Taskboard…" })
erstelleElement("div", { klasse: "hinweis-zentriert", text: t("common.loading") })
);
try {
@@ -115,10 +119,10 @@ async function ladeTaskboard() {
if (tasks.length === 0) {
container.append(
erstelleKarte("Keine Aufgaben", [
erstelleKarte(t("tasks.empty"), [
erstelleElement("p", {
klasse: "gedaempft",
text: "Keine offenen Aufgaben. Wiederkehrende Arbeit legst du über „Neue Aufgabe“ mit einem Termin an."
text: "..."
})
])
);
@@ -130,7 +134,7 @@ async function ladeTaskboard() {
container.append(raster);
} catch (err) {
container.replaceChildren(
erstelleKarte("Taskboard nicht verfügbar", [
erstelleKarte("Taskboard Error", [
erstelleElement("p", { klasse: "gedaempft", text: err.message })
])
);
@@ -138,8 +142,8 @@ async function ladeTaskboard() {
}
function terminText(task) {
if (!task.when) return "einmalig";
const art = TERMIN_ART[task.when.kind] || task.when.kind;
if (!task.when) return t("tasks.scheduleNone");
const art = task.when.kind;
return `${art} ${task.when.value} (${task.when.timeZone || "UTC"})`;
}
@@ -150,25 +154,25 @@ function erstelleAufgabenKarte(task) {
erstelleElement(
"div",
{ klasse: "task-status-zeile gruppe-horizontal" },
erstellePlakette(STATUS_TEXT[task.status] || task.status, STATUS_ART[task.status] || "neutral"),
erstellePlakette(holeStatusText(task.status), STATUS_ART[task.status] || "neutral"),
erstelleElement("span", { klasse: "gedaempft text-klein", text: `${task.id} · ${task.assignee || "—"}` })
)
);
zeilen.push(erstelleElement("p", { klasse: "gedaempft text-klein", text: `Termin: ${terminText(task)}` }));
zeilen.push(erstelleElement("p", { klasse: "gedaempft text-klein", text: `${t("tasks.schedule")}: ${terminText(task)}` }));
zeilen.push(
erstelleElement("p", {
klasse: "gedaempft text-klein",
text: `Nächster Lauf: ${task.nextRunUtc ? formatiereDatumZeit(task.nextRunUtc) : "—"}`
text: `Next: ${task.nextRunUtc ? formatiereDatumZeit(task.nextRunUtc) : "—"}`
})
);
const letzter = erstelleElement("p", { klasse: "gedaempft text-klein" }, "Letzter Lauf: ");
const letzter = erstelleElement("p", { klasse: "gedaempft text-klein" }, "Last: ");
if (task.lastRun) {
letzter.append(
`${formatiereDatumZeit(task.lastRun.occurredAt)} `,
erstellePlakette(task.lastRun.status, LAUF_ART[task.lastRun.status] || "neutral"),
` ${task.lastRun.costIsKnown ? formatiereKosten(task.lastRun.costUsd) : "Kosten unbekannt"}`
` ${task.lastRun.costIsKnown ? formatiereKosten(task.lastRun.costUsd) : "—"}`
);
if (task.lastRun.resultRef) letzter.title = task.lastRun.resultRef;
} else {
@@ -189,9 +193,9 @@ function erstelleAufgabenKarte(task) {
aktionen.append(
erstelleElement(
"button",
{ klasse: "schaltflaeche flach klein", title: "Jetzt ausführen", onclick: () => fuehreAus(task) },
{ klasse: "schaltflaeche flach klein", title: t("tasks.runNow"), onclick: () => fuehreAus(task) },
erstelleIcon(PFADE.abspielen, 14),
erstelleElement("span", { text: " Jetzt" })
erstelleElement("span", { text: " " + t("tasks.runNow") })
)
);
}
@@ -200,7 +204,7 @@ function erstelleAufgabenKarte(task) {
aktionen.append(
erstelleElement("button", {
klasse: "schaltflaeche flach klein",
text: "Pausieren",
text: t("tasks.pause"),
onclick: () => setzeAktiv(task, false)
})
);
@@ -208,7 +212,7 @@ function erstelleAufgabenKarte(task) {
aktionen.append(
erstelleElement("button", {
klasse: "schaltflaeche flach klein",
text: "Aktivieren",
text: t("tasks.resume"),
onclick: () => setzeAktiv(task, true)
})
);
@@ -217,32 +221,32 @@ function erstelleAufgabenKarte(task) {
aktionen.append(
erstelleElement("button", {
klasse: "schaltflaeche flach klein",
text: "Bearbeiten",
text: t("common.edit"),
onclick: () => oeffneAufgabenDialog(task)
})
);
zeilen.push(aktionen);
return erstelleKarte(task.title || `Aufgabe ${task.id}`, zeilen);
return erstelleKarte(task.title || `Task ${task.id}`, zeilen);
}
async function fuehreAus(task) {
try {
const res = await api(`/tasks/${encodeURIComponent(task.id)}/run`, { method: "POST" });
zeigeMeldung(res?.message || "Lauf angestoßen.", "erfolg");
zeigeMeldung(res?.message || t("tasks.runTriggered"), "erfolg");
setTimeout(ladeTaskboard, 1500);
} catch (err) {
zeigeMeldung(`Nicht gestartet: ${err.message}`, "gefahr");
zeigeMeldung(`Error: ${err.message}`, "gefahr");
}
}
async function setzeAktiv(task, aktiv) {
try {
const res = await api(`/tasks/${encodeURIComponent(task.id)}/enabled?enabled=${aktiv}`, { method: "PUT" });
zeigeMeldung(res?.message || (aktiv ? "Aktiviert." : "Pausiert."), "info");
zeigeMeldung(res?.message || (aktiv ? t("tasks.resume") : t("tasks.pause")), "info");
await ladeTaskboard();
} catch (err) {
zeigeMeldung(`Fehler: ${err.message}`, "gefahr");
zeigeMeldung(`Error: ${err.message}`, "gefahr");
}
}
@@ -251,25 +255,24 @@ async function oeffneAufgabenDialog(task) {
try {
agenten = (await api("/agents")) || [];
} catch {
// Ohne Agentenliste bleibt nur „Mensch“ und eine vorhandene Zuordnung.
//
}
const formular = erstelleElement("form", { klasse: "formular-raster" });
const titel = erstelleElement("input", { type: "text", required: true, value: task?.title || "" });
const beschreibung = erstelleElement("textarea", { rows: "5", placeholder: "Auftrag an den Agenten…" });
const beschreibung = erstelleElement("textarea", { rows: "5", placeholder: "..." });
beschreibung.value = task?.body || "";
// #598: Frischer Lauf zuerst und als Vorgabe - im Chatverlauf wächst der Kontext mit jedem Lauf.
const zuordnung = erstelleElement("select", { required: true });
for (const a of agenten) {
zuordnung.append(
erstelleElement("option", { value: `@new:${a.agentId}`, text: `${a.displayName} – frischer Lauf` }),
erstelleElement("option", { value: `@${a.agentId}`, text: `${a.displayName} – im Chatverlauf` })
erstelleElement("option", { value: `@new:${a.agentId}`, text: `${a.displayName} (New Run)` }),
erstelleElement("option", { value: `@${a.agentId}`, text: `${a.displayName} (Chat History)` })
);
}
zuordnung.append(erstelleElement("option", { value: "@human", text: "Mensch (kein Agentenlauf)" }));
zuordnung.append(erstelleElement("option", { value: "@human", text: "Human (No Agent)" }));
if (task?.assignee && ![...zuordnung.options].some((o) => o.value === task.assignee)) {
zuordnung.append(erstelleElement("option", { value: task.assignee, text: task.assignee }));
}
@@ -277,10 +280,10 @@ async function oeffneAufgabenDialog(task) {
const terminArt = erstelleElement("select");
terminArt.append(
erstelleElement("option", { value: "", text: "Einmalig" }),
erstelleElement("option", { value: "cron", text: "Cron (z.B. stündlich)" }),
erstelleElement("option", { value: "every", text: "Intervall" }),
erstelleElement("option", { value: "at", text: "Zeitpunkt" })
erstelleElement("option", { value: "", text: t("tasks.scheduleNone") }),
erstelleElement("option", { value: "cron", text: "Cron" }),
erstelleElement("option", { value: "every", text: t("tasks.scheduleInterval") }),
erstelleElement("option", { value: "at", text: t("tasks.scheduleAt") })
);
terminArt.value = task?.when?.kind || "";
@@ -289,7 +292,7 @@ async function oeffneAufgabenDialog(task) {
const standardZone = Intl.DateTimeFormat().resolvedOptions().timeZone || "Europe/Berlin";
const zeitzone = erstelleElement("input", { type: "text", value: task?.when?.timeZone || standardZone });
const abnahme = erstelleElement("textarea", { rows: "2", placeholder: "Woran das Ergebnis gemessen wird (optional)" });
const abnahme = erstelleElement("textarea", { rows: "2", placeholder: "..." });
abnahme.value = task?.acceptance || "";
const hinweis = erstelleElement("p", { klasse: "gedaempft text-klein" });
@@ -298,45 +301,43 @@ async function oeffneAufgabenDialog(task) {
const wiederkehrend = terminArt.value === "cron" || terminArt.value === "every";
const imVerlauf = zuordnung.value.startsWith("@") && !zuordnung.value.startsWith("@new") && zuordnung.value !== "@human";
hinweis.hidden = !(wiederkehrend && imVerlauf);
hinweis.textContent =
"⚠ Im Chatverlauf wächst der Kontext mit jedem Lauf – und mit ihm die Kosten je Lauf. " +
"Für wiederkehrende Aufgaben ist „frischer Lauf“ meist die bessere Wahl.";
hinweis.textContent = "⚠ Notice: Running recurring tasks in chat history increases context and costs.";
const ohneTermin = terminArt.value === "";
terminWert.disabled = ohneTermin;
zeitzone.disabled = ohneTermin;
terminWert.placeholder =
{ cron: "z.B. 0 7-22/3 * * *", every: "z.B. 30m, 2h, 1d", at: "z.B. 2026-09-15T08:00" }[terminArt.value] || "";
{ cron: "e.g. 0 7-22/3 * * *", every: "e.g. 30m, 2h, 1d", at: "e.g. 2026-09-15T08:00" }[terminArt.value] || "";
};
zuordnung.addEventListener("change", aktualisiere);
terminArt.addEventListener("change", aktualisiere);
aktualisiere();
formular.append(
erstelleElement("label", { text: "Titel:" }), titel,
erstelleElement("label", { text: "Auftrag:" }), beschreibung,
erstelleElement("label", { text: "Ausführen durch:" }), zuordnung,
erstelleElement("label", { text: "Termin:" }), terminArt,
erstelleElement("label", { text: "Wert:" }), terminWert,
erstelleElement("label", { text: "Zeitzone:" }), zeitzone,
erstelleElement("label", { text: "Abnahme:" }), abnahme,
erstelleElement("label", { text: t("common.name") + ":" }), titel,
erstelleElement("label", { text: t("tasks.taskMessage") + ":" }), beschreibung,
erstelleElement("label", { text: t("tasks.agent") + ":" }), zuordnung,
erstelleElement("label", { text: t("tasks.scheduleType") + ":" }), terminArt,
erstelleElement("label", { text: "Value:" }), terminWert,
erstelleElement("label", { text: "Timezone:" }), zeitzone,
erstelleElement("label", { text: "Acceptance Criteria:" }), abnahme,
erstelleElement("span"), hinweis
);
const abbrechen = erstelleElement("button", {
type: "button",
klasse: "schaltflaeche flach",
text: "Abbrechen",
text: t("common.cancel"),
onclick: () => modal.schliessen()
});
const speichern = erstelleElement("button", {
type: "submit",
klasse: "schaltflaeche primaer",
text: task ? "Speichern" : "Anlegen"
text: task ? t("common.save") : t("tasks.newTask")
});
const modal = erstelleModal(task ? "Aufgabe bearbeiten" : "Neue Aufgabe", formular, [abbrechen, speichern]);
const modal = erstelleModal(task ? t("common.edit") : t("tasks.createTitle"), formular, [abbrechen, speichern]);
formular.addEventListener("submit", async (e) => {
e.preventDefault();
@@ -359,11 +360,11 @@ async function oeffneAufgabenDialog(task) {
body
});
modal.schliessen();
const text = res?.message || "Gespeichert.";
zeigeMeldung(text, text.includes("Hinweis:") ? "warnung" : "erfolg", text.includes("Hinweis:") ? 9000 : 4000);
const text = res?.message || t("tasks.saved");
zeigeMeldung(text, "erfolg");
await ladeTaskboard();
} catch (err) {
zeigeMeldung(`Nicht gespeichert: ${err.message}`, "gefahr", 8000);
zeigeMeldung(`Error: ${err.message}`, "gefahr");
} finally {
speichern.disabled = false;
}