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
+60 -60
View File
@@ -22,23 +22,23 @@ public sealed class MemoryTool : IAgentTool
public string Name => "Memory";
public string Description => """
Dein Langzeitgedächtnis — überdauert einzelne Läufe und Neustarts.
Your long-term memory — persists across runs and restarts.
Nutze es, um Erkenntnisse festzuhalten, die beim nächsten Lauf noch zählen:
getroffene Entscheidungen, gesicherte Fakten, offene Punkte, Beobachtungen
über Zeit. Prüfe zu Beginn eines Laufs mit 'recall', was du bereits weißt,
statt es erneut herzuleiten.
Use it to retain insights that matter in future runs:
decisions made, verified facts, open action items, observations
over time. At the start of a run, check with 'recall' what you already know,
instead of deriving it again.
Aktionen: remember, recall, forget, list_subjects
Actions: remember, recall, forget, list_subjects
Zwei Bereiche:
- scope='agent' (Standard): nur für dich sichtbar
- scope='shared': für alle Agenten der Instanz sichtbar
Two scopes:
- scope='agent' (default): visible only to you
- scope='shared': visible to all agents in this instance
Zum 'key': Vergib einen, wenn eine Angabe sich später ändern kann
(z.B. key='kursziel_nvda'). Erneutes Merken unter demselben Schlüssel
aktualisiert den Eintrag, statt einen zweiten anzulegen — so wächst dein
Gedächtnis nicht zu.
Regarding 'key': Specify one when an item might change later
(e.g. key='target_price_nvda'). Remembering under the same key
updates the existing entry instead of creating a second one — keeping
your memory clean.
""";
public JsonElement InputSchema { get; } = JsonDocument.Parse("""
@@ -48,54 +48,54 @@ public sealed class MemoryTool : IAgentTool
"action": {
"type": "string",
"enum": ["remember", "recall", "forget", "list_subjects"],
"description": "Die auszuführende Aktion."
"description": "The action to perform."
},
"scope": {
"type": "string",
"enum": ["agent", "shared"],
"description": "Bereich. Standard ist 'agent' (nur für dich)."
"description": "Scope. Default is 'agent' (visible only to you)."
},
"subject": {
"type": "string",
"description": "Worum es geht — z.B. 'NVDA', 'Kunde Meier', 'Projekt Alpha'. Pflicht bei remember."
"description": "What this is about — e.g. 'NVDA', 'Customer Smith', 'Project Alpha'. Required for remember."
},
"content": {
"type": "string",
"description": "Was du dir merken willst. Pflicht bei remember."
"description": "What you want to remember. Required for remember."
},
"key": {
"type": "string",
"description": "Optionaler eindeutiger Schlüssel. Erneutes Merken darunter aktualisiert den Eintrag."
"description": "Optional unique key. Remembering under the same key updates the entry."
},
"category": {
"type": "string",
"enum": ["fact", "decision", "observation", "task", "contact", "other"],
"description": "Art der Erinnerung. Standard 'observation'."
"description": "Type of memory. Default 'observation'."
},
"tags": {
"type": "array",
"items": { "type": "string" },
"description": "Schlagworte zum späteren Wiederfinden."
"description": "Tags for later retrieval."
},
"importance": {
"type": "integer",
"description": "1 (nebensächlich) bis 5 (zentral). Standard 3. Steuert die Reihenfolge beim Abruf."
"description": "1 (minor) to 5 (critical). Default 3. Controls ordering on recall."
},
"query": {
"type": "string",
"description": "Suchtext für recall — wird gegen Betreff, Inhalt und Schlagworte geprüft."
"description": "Search query for recall — matched against subject, content, and tags."
},
"limit": {
"type": "integer",
"description": "Maximale Anzahl Treffer bei recall. Standard 20."
"description": "Maximum number of results for recall. Default 20."
},
"minImportance": {
"type": "integer",
"description": "Nur Erinnerungen ab dieser Wichtigkeit abrufen."
"description": "Only retrieve memories with importance greater than or equal to this."
},
"id": {
"type": "integer",
"description": "Id der zu löschenden Erinnerung (forget)."
"description": "ID of memory to delete (forget)."
}
},
"required": ["action"]
@@ -106,11 +106,11 @@ public sealed class MemoryTool : IAgentTool
JsonElement input, AgentToolContext context, CancellationToken ct)
{
if (context.Memory is not { } repository)
return ToolResult.Fail("Das Gedächtnis ist für diese Instanz nicht verfügbar.");
return ToolResult.Fail("Memory is not available for this instance.");
var action = input.TryGetProperty("action", out var a) ? a.GetString() : null;
if (string.IsNullOrWhiteSpace(action))
return ToolResult.Fail("'action' ist erforderlich.");
return ToolResult.Fail("'action' is required.");
try
{
@@ -120,13 +120,13 @@ public sealed class MemoryTool : IAgentTool
"recall" => await RecallAsync(input, repository, context, ct),
"forget" => await ForgetAsync(input, repository, context, ct),
"list_subjects" => await ListSubjectsAsync(input, repository, context, ct),
_ => ToolResult.Fail($"Unbekannte Aktion: {action}")
_ => ToolResult.Fail($"Unknown action: {action}")
};
}
catch (Exception ex)
{
context.Logger.LogError(ex, "Fehler im Memory-Tool bei Aktion {Action}", action);
return ToolResult.Fail($"Fehler: {ex.Message}");
context.Logger.LogError(ex, "Error in Memory tool for action {Action}", action);
return ToolResult.Fail($"Error: {ex.Message}");
}
}
@@ -137,11 +137,11 @@ public sealed class MemoryTool : IAgentTool
{
var subject = GetString(input, "subject");
if (string.IsNullOrWhiteSpace(subject))
return ToolResult.Fail("'subject' ist erforderlich — worum geht es?");
return ToolResult.Fail("'subject' is required — what is this about?");
var content = GetString(input, "content");
if (string.IsNullOrWhiteSpace(content))
return ToolResult.Fail("'content' ist erforderlich — was soll gemerkt werden?");
return ToolResult.Fail("'content' is required — what should be remembered?");
var scope = GetScope(input);
@@ -160,12 +160,12 @@ public sealed class MemoryTool : IAgentTool
var saved = await repository.RememberAsync(entry, ct);
var scopeLabel = scope == MemoryScope.Shared ? "geteilt" : "privat";
var keyHint = string.IsNullOrWhiteSpace(saved.Key) ? "" : $" | Schlüssel: {saved.Key}";
var scopeLabel = scope == MemoryScope.Shared ? "shared" : "private";
var keyHint = string.IsNullOrWhiteSpace(saved.Key) ? "" : $" | Key: {saved.Key}";
return ToolResult.Ok(
$"Gemerkt (#{saved.Id}, {scopeLabel}){keyHint}\n" +
$"Betreff: {saved.Subject} | Art: {saved.Category} | Wichtigkeit: {saved.Importance}" +
$"Remembered (#{saved.Id}, {scopeLabel}){keyHint}\n" +
$"Subject: {saved.Subject} | Category: {saved.Category} | Importance: {saved.Importance}" +
await HinweiseAsync(saved, scope, repository, context, ct));
}
@@ -193,16 +193,16 @@ public sealed class MemoryTool : IAgentTool
var ohneSchluessel = gleicherBetreff.Count(e => string.IsNullOrWhiteSpace(e.Key));
if (ohneSchluessel > 1)
{
sb.Append($"\nHinweis: Unter dem Betreff '{saved.Subject}' liegen jetzt {ohneSchluessel} Einträge " +
"ohne Schlüssel. Ersetzt dieser Eintrag einen früheren, merke ihn mit 'key' – dann wird " +
"er fortgeschrieben statt verdoppelt. Überholte Einträge entfernst du mit forget (id).");
sb.Append($"\nNote: Under subject '{saved.Subject}' there are now {ohneSchluessel} entries " +
"without a key. If this entry replaces an earlier one, remember it with 'key' – then it will be " +
"updated instead of duplicated. Remove outdated entries with forget (id).");
}
}
if (saved.Content.Length > MaxResultChars / 2)
{
sb.Append($"\nHinweis: Der Eintrag ist {saved.Content.Length} Zeichen lang, ein Abruf zeigt höchstens " +
$"{MaxResultChars} Zeichen. Halte ihn kürzer oder teile ihn auf, sonst wird er beim Abruf gekürzt.");
sb.Append($"\nNote: The entry is {saved.Content.Length} characters long, recall displays at most " +
$"{MaxResultChars} characters. Keep it shorter or split it up, otherwise it will be truncated on recall.");
}
return sb.ToString();
@@ -231,9 +231,9 @@ public sealed class MemoryTool : IAgentTool
{
var total = await repository.CountAsync(scope, context.AgentId, ct);
return ToolResult.Ok(total == 0
? "Noch keine Erinnerungen in diesem Bereich."
: $"Keine Treffer. Der Bereich enthält {total} Erinnerung(en) — " +
"versuche einen anderen Suchbegriff oder 'list_subjects' für einen Überblick.");
? "No memories in this scope yet."
: $"No matches. Scope contains {total} memory entry/entries — " +
"try a different search term or 'list_subjects' for an overview.");
}
return ToolResult.Ok(Render(entries, scope));
@@ -246,16 +246,16 @@ public sealed class MemoryTool : IAgentTool
{
var removed = await repository.ForgetAsync(id, ct);
return ToolResult.Ok(removed
? $"Erinnerung #{id} gelöscht."
: $"Keine Erinnerung mit der Id {id} gefunden.");
? $"Memory #{id} deleted."
: $"No memory found with ID {id}.");
}
var subject = GetString(input, "subject");
if (string.IsNullOrWhiteSpace(subject))
return ToolResult.Fail("Für 'forget' wird entweder 'id' oder 'subject' benötigt.");
return ToolResult.Fail("Either 'id' or 'subject' is required for 'forget'.");
var count = await repository.ForgetBySubjectAsync(GetScope(input), context.AgentId, subject, ct);
return ToolResult.Ok($"{count} Erinnerung(en) zum Betreff '{subject}' gelöscht.");
return ToolResult.Ok($"{count} memory entry/entries deleted for subject '{subject}'.");
}
private static async Task<ToolResult> ListSubjectsAsync(
@@ -266,10 +266,10 @@ public sealed class MemoryTool : IAgentTool
scope, context.AgentId, GetInt(input, "limit") ?? 50, ct);
if (subjects.Count == 0)
return ToolResult.Ok("Noch keine Erinnerungen in diesem Bereich.");
return ToolResult.Ok("No memories in this scope yet.");
var sb = new StringBuilder();
sb.AppendLine($"Betreffs im Bereich '{(scope == MemoryScope.Shared ? "shared" : "agent")}':");
sb.AppendLine($"Subjects in scope '{(scope == MemoryScope.Shared ? "shared" : "agent")}':");
foreach (var (subject, count) in subjects)
sb.AppendLine($" • {subject} ({count})");
@@ -281,7 +281,7 @@ public sealed class MemoryTool : IAgentTool
private static string Render(IReadOnlyList<MemoryEntry> entries, MemoryScope scope)
{
var sb = new StringBuilder();
sb.AppendLine($"{entries.Count} Erinnerung(en), wichtigste zuerst:");
sb.AppendLine($"{entries.Count} memory entry/entries, most important first:");
sb.AppendLine();
var shown = 0;
@@ -304,8 +304,8 @@ public sealed class MemoryTool : IAgentTool
if (entries.Count > shown)
{
sb.AppendLine($"[… {entries.Count - shown} weitere Treffer ausgelassen. " +
"Grenze die Suche ein, um sie zu sehen.]");
sb.AppendLine($"[… {entries.Count - shown} more results omitted. " +
"Narrow down search to view them.]");
}
break;
}
@@ -323,15 +323,15 @@ public sealed class MemoryTool : IAgentTool
private static string FormatEntry(MemoryEntry entry, MemoryScope scope, int? maxChars = null)
{
var header = new StringBuilder();
header.Append($"#{entry.Id} [{entry.Subject}] {entry.Category}, Wichtigkeit {entry.Importance}");
header.Append($"#{entry.Id} [{entry.Subject}] {entry.Category}, Importance {entry.Importance}");
if (!string.IsNullOrWhiteSpace(entry.Key))
header.Append($", Schlüssel: {entry.Key}");
header.Append($", Key: {entry.Key}");
var meta = new List<string> { $"aktualisiert {entry.UpdatedAt:yyyy-MM-dd HH:mm} UTC" };
var meta = new List<string> { $"updated {entry.UpdatedAt:yyyy-MM-dd HH:mm} UTC" };
if (entry.Tags.Count > 0)
meta.Add("Schlagworte: " + string.Join(", ", entry.Tags));
meta.Add("Tags: " + string.Join(", ", entry.Tags));
if (scope == MemoryScope.Shared && !string.IsNullOrWhiteSpace(entry.CreatedBy))
meta.Add($"von {entry.CreatedBy}");
meta.Add($"by {entry.CreatedBy}");
var metaLine = $" ({string.Join(" | ", meta)})";
var content = entry.Content;
@@ -352,8 +352,8 @@ public sealed class MemoryTool : IAgentTool
{
const int Mindestens = 400;
var marke = $"\n [… gekürzt: {{0}} von {content.Length} Zeichen ausgelassen – der Eintrag ist zu groß " +
"für einen Abruf. Teile ihn auf oder halte ihn kürzer.]\n ";
var marke = $"\n [… truncated: {{0}} of {content.Length} characters omitted – entry is too large " +
"for recall. Split it up or keep it shorter.]\n ";
if (content.Length <= platz)
return content;