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
+31 -32
View File
@@ -12,23 +12,22 @@ public sealed class WebFetchTool : IAgentTool
public string Name => "WebFetch";
public string Description => """
Ruft Webseiten, JSON-/Text-Schnittstellen und RSS/Atom-Feeds ab. Nur Domains aus der
Whitelist, kein JavaScript-Rendering.
Fetches web pages, JSON/text APIs, and RSS/Atom feeds. Only whitelist domains, no JavaScript rendering.
fetch: Bei HTML kommt standardmaessig nur der Hauptinhalt als kompaktes Markdown
(Ueberschriften, Absaetze, Listen, Tabellen, Links) - der guenstigste Weg. Mit
"selector" (CSS, z.B. "table", "#kurse", "article h2") holst du gezielt nur den Teil,
den du brauchst - noch guenstiger. Linkziele (URLs) kommen nur mit links=true - nur
setzen, wenn du die Adressen wirklich brauchst. mode="full" liefert den ganzen Seitentext und kostet
ein Vielfaches: nur verwenden, wenn der Hauptinhalt fehlt oder unvollstaendig ist.
JSON, Text und XML kommen unveraendert. Brauchst du aus einer grossen JSON-Antwort nur
bestimmte Eintraege (z.B. eine Stadt), nimm "filter": dann kommen nur die Objekte, die den
Text enthalten - viel guenstiger als Blaettern. Lange Inhalte kommen in Abschnitten: die Antwort
nennt nextOffset - nur weiterlesen, wenn der Rest fuer den Auftrag wirklich gebraucht wird.
fetch: For HTML, by default only the main content is returned as compact Markdown
(headings, paragraphs, lists, tables, links) - the most cost-effective way. With
"selector" (CSS, e.g. "table", "#content", "article h2") you selectively retrieve only the part
you need - even more economical. Link targets (URLs) are only included with links=true - only
set if you really need the URLs. mode="full" returns the full page text and costs
much more: only use when main content is missing or incomplete.
JSON, text and XML are returned unchanged. If you only need specific entries from a large JSON response
(e.g., a city), use "filter": only objects containing the text will be returned - much cheaper
than pagination. Long content is returned in chunks: the response includes nextOffset - only continue
reading if the remainder is really needed for the task.
rss: Titel, Link, Datum und kurze Beschreibung je Eintrag ("limit" begrenzt die Anzahl).
rss: Title, link, date, and short description per item ("limit" caps the count).
Waehle immer den fuer den Zweck guenstigsten Weg.
Always choose the most efficient path for your purpose.
""";
/// <summary>
@@ -58,21 +57,21 @@ public sealed class WebFetchTool : IAgentTool
"action": {
"type": "string",
"enum": ["fetch", "rss"],
"description": "fetch=Seite oder Schnittstelle abrufen, rss=RSS/Atom-Feed lesen"
"description": "fetch=fetch webpage or API, rss=read RSS/Atom feed"
},
"url": { "type": "string" },
"selector":{ "type": "string",
"description": "fetch, optional: CSS-Selektor fuer den benoetigten Teil, z.B. 'table', 'article', '#inhalt'" },
"description": "fetch, optional: CSS selector for the required section, e.g. 'table', 'article', '#content'" },
"mode": { "type": "string", "enum": ["auto", "full"],
"description": "fetch: auto (Standard) = Hauptinhalt als Markdown; full = ganzer Seitentext, nur wenn auto nicht reicht" },
"description": "fetch: auto (default) = main content as Markdown; full = full page text, only if auto is insufficient" },
"links": { "type": "boolean",
"description": "fetch: Linkziele als [Text](URL) mitliefern (Standard false - kostet auf linkreichen Seiten viel)" },
"description": "fetch: include link targets as [Text](URL) (default false - expensive on link-heavy pages)" },
"filter": { "type": "string",
"description": "fetch, nur JSON/JSONP: nur Eintraege liefern, die diesen Text enthalten (ohne Gross-/Kleinschreibung), z.B. 'Berlin'" },
"description": "fetch, JSON/JSONP only: only return entries containing this text (case-insensitive), e.g. 'Berlin'" },
"offset": { "type": "integer",
"description": "fetch: Fortsetzung eines langen Inhalts - Wert aus nextOffset der vorigen Antwort" },
"description": "fetch: continuation of long content - value from nextOffset of previous response" },
"limit": { "type": "integer",
"description": "rss: hoechstens so viele Eintraege (Standard 20, maximal 50)" }
"description": "rss: maximum number of items (default 20, max 50)" }
}
}
""").RootElement.Clone();
@@ -91,7 +90,7 @@ public sealed class WebFetchTool : IAgentTool
? JsonSerializer.Deserialize<Dictionary<string, object?>>(je.GetRawText())
: context.ToolConfig;
if (config == null) return ToolResult.Fail("WebFetch Konfiguration fehlt.");
if (config == null) return ToolResult.Fail("WebFetch configuration missing.");
var allowedDomains = config.GetValueOrDefault("allowedDomains") is JsonElement ad
? ad.EnumerateArray().Select(x => x.GetString()!).ToList()
@@ -105,13 +104,13 @@ public sealed class WebFetchTool : IAgentTool
{
"fetch" => await FetchPageAsync(url, allowedDomains, config, input, ct),
"rss" => await FetchRssAsync(url, allowedDomains, input, ct),
_ => ToolResult.Fail($"Unbekannte Aktion: {action}")
_ => ToolResult.Fail($"Unknown action: {action}")
};
}
catch (Exception ex)
{
context.Logger.LogError(ex, "Fehler in WebFetch");
return ToolResult.Fail($"Fehler: {ex.Message}");
context.Logger.LogError(ex, "Error in WebFetch");
return ToolResult.Fail($"Error: {ex.Message}");
}
}
@@ -130,12 +129,12 @@ public sealed class WebFetchTool : IAgentTool
// Compiler das nicht an. Die Pruefung sagt dasselbe nachvollziehbar und ersetzt
// das Ausrufezeichen, das hier stand.
if (response is null)
return ToolResult.Fail($"Keine Antwort von {url}.");
return ToolResult.Fail($"No response from {url}.");
using var _ = response;
if (!response.IsSuccessStatusCode)
return ToolResult.Fail($"Seite nicht erreichbar: {url} → HTTP {(int)response.StatusCode} {response.ReasonPhrase}");
return ToolResult.Fail($"Page unreachable: {url} → HTTP {(int)response.StatusCode} {response.ReasonPhrase}");
var body = await response.Content.ReadAsStringAsync(ct);
var maxKb = GetConfigInt(config, "maxResponseKb", 512);
@@ -214,12 +213,12 @@ public sealed class WebFetchTool : IAgentTool
// Compiler das nicht an. Die Pruefung sagt dasselbe nachvollziehbar und ersetzt
// das Ausrufezeichen, das hier stand.
if (response is null)
return ToolResult.Fail($"Keine Antwort von {url}.");
return ToolResult.Fail($"No response from {url}.");
using var _ = response;
if (!response.IsSuccessStatusCode)
return ToolResult.Fail($"RSS-Feed nicht erreichbar: {url} → HTTP {(int)response.StatusCode} {response.ReasonPhrase}");
return ToolResult.Fail($"RSS feed unreachable: {url} → HTTP {(int)response.StatusCode} {response.ReasonPhrase}");
var xml = await response.Content.ReadAsStringAsync(ct);
var doc = XDocument.Parse(xml);
@@ -305,7 +304,7 @@ public sealed class WebFetchTool : IAgentTool
{
if (!UrlGuard.TryValidate(current, allowedDomains, out var uri, out var error))
{
var context = hop == 0 ? "" : $" (Weiterleitung {hop} von {url})";
var context = hop == 0 ? "" : $" (Redirect {hop} from {url})";
return (null, ToolResult.Fail($"{error}{context}"));
}
@@ -318,7 +317,7 @@ public sealed class WebFetchTool : IAgentTool
response.Dispose();
if (location is null)
return (null, ToolResult.Fail($"Weiterleitung ohne Zieladresse bei {current}."));
return (null, ToolResult.Fail($"Redirect without target address at {current}."));
// Relative Weiterleitungen gegen die aktuelle Adresse auflösen.
current = location.IsAbsoluteUri
@@ -327,7 +326,7 @@ public sealed class WebFetchTool : IAgentTool
}
return (null, ToolResult.Fail(
$"Mehr als {MaxRedirects} Weiterleitungen ausgehend von {url} — abgebrochen."));
$"More than {MaxRedirects} redirects starting from {url} — aborted."));
}
private static bool IsRedirect(HttpStatusCode status) => status is