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:
co-authored by
Claude Opus 5.5
parent
69718122f3
commit
ad71682c2d
@@ -47,13 +47,13 @@ public sealed partial class AddAgentViewModel : ViewModelBase
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(AgentId))
|
||||
{
|
||||
StatusMessage = "Bitte eine Agenten-ID eingeben.";
|
||||
StatusMessage = "Please enter an Agent ID.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(DisplayName))
|
||||
{
|
||||
StatusMessage = "Bitte einen Anzeigenamen eingeben.";
|
||||
StatusMessage = "Please enter a display name.";
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ public sealed partial class AddJobViewModel : ViewModelBase
|
||||
private string _cronExpression = "0 * * * *";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _taskMessage = "Führe deine zugewiesenen Aufgaben aus.";
|
||||
private string _taskMessage = "Execute your assigned tasks.";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _runOnStart;
|
||||
@@ -79,13 +79,13 @@ public sealed partial class AddJobViewModel : ViewModelBase
|
||||
{
|
||||
if (SelectedAgent is null)
|
||||
{
|
||||
StatusMessage = "Bitte einen Agenten auswählen.";
|
||||
StatusMessage = "Please select an agent.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(CronExpression))
|
||||
{
|
||||
StatusMessage = "Bitte einen Cron-Ausdruck angeben.";
|
||||
StatusMessage = "Please enter a cron expression.";
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,13 +38,13 @@ public sealed partial class AddServiceViewModel : ViewModelBase
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ServiceName))
|
||||
{
|
||||
StatusMessage = "Bitte einen Dienstnamen eingeben.";
|
||||
StatusMessage = "Please enter a service name.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (ServicePort <= 0 || ServicePort > 65535)
|
||||
{
|
||||
StatusMessage = "Gültigen Port zwischen 1 und 65535 eingeben.";
|
||||
StatusMessage = "Please enter a valid port between 1 and 65535.";
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ public sealed partial class AgentChatsPageViewModel : PageViewModel
|
||||
|
||||
public AgentChatsPageViewModel() : this(null) { }
|
||||
|
||||
public AgentChatsPageViewModel(AppHost? host) : base("Agenten-Chats")
|
||||
public AgentChatsPageViewModel(AppHost? host) : base("nav.agentChats")
|
||||
{
|
||||
if (host is null)
|
||||
{
|
||||
@@ -85,10 +85,8 @@ public sealed partial class AgentChatsPageViewModel : PageViewModel
|
||||
Messages.Clear();
|
||||
|
||||
EmptyText = Agents.Count < 2
|
||||
? "Für einen Verlauf braucht es mindestens zwei Agenten in dieser Instanz."
|
||||
: "Noch keine Nachrichten aufgezeichnet. Agenten können sich bereits gegenseitig "
|
||||
+ "erreichen; mitgeschrieben wird der Wortlaut erst, wenn der Nachrichtenspeicher "
|
||||
+ "aus docs/Agentenkommunikation-Konzept.md steht.";
|
||||
? LocalizationManager.Instance["desktop.agentChats.needTwo"]
|
||||
: LocalizationManager.Instance["desktop.agentChats.empty"];
|
||||
|
||||
IsEmpty = Messages.Count == 0;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
public sealed partial class ToolItemViewModel : ObservableObject
|
||||
public sealed partial class ToolItemViewModel : ViewModelBase
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
@@ -32,7 +32,7 @@ public sealed partial class ToolItemViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
public sealed partial class AgentItemViewModel : ObservableObject
|
||||
public sealed partial class AgentItemViewModel : ViewModelBase
|
||||
{
|
||||
public AgentConfig Config { get; }
|
||||
|
||||
@@ -210,7 +210,7 @@ public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
|
||||
public AgentsPageViewModel() : this(null) { }
|
||||
|
||||
public AgentsPageViewModel(AppHost? host) : base("Agenten")
|
||||
public AgentsPageViewModel(AppHost? host) : base("nav.agents")
|
||||
{
|
||||
_host = host;
|
||||
|
||||
@@ -233,8 +233,10 @@ public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusText = "Entwurfsmodus";
|
||||
StatusText = LocalizationManager.Instance["desktop.status.design"];
|
||||
}
|
||||
|
||||
LocalizationManager.Instance.LanguageChanged += () => OnPropertyChanged(nameof(AllowedToolsHeader));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -274,11 +276,16 @@ public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
SelectedAgent = Agents.FirstOrDefault(a => a.AgentId == previous) ?? Agents.FirstOrDefault();
|
||||
}
|
||||
|
||||
public string AllowedToolsHeader => SelectedAgent is not null
|
||||
? LocalizationManager.Instance.Get("desktop.agents.allowedToolsFor", SelectedAgent.DisplayName)
|
||||
: "";
|
||||
|
||||
partial void OnSelectedAgentChanged(AgentItemViewModel? value)
|
||||
{
|
||||
// Die Werkzeugliste unten gehört zum gewählten Agenten — bei einem Wechsel darf
|
||||
// dort nicht die Auswahl des vorigen stehen bleiben.
|
||||
SelectedTool = value?.AvailableTools.FirstOrDefault();
|
||||
OnPropertyChanged(nameof(AllowedToolsHeader));
|
||||
}
|
||||
|
||||
partial void OnSelectedToolChanged(ToolItemViewModel? value)
|
||||
@@ -352,14 +359,14 @@ public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
|
||||
if (_host.Engine is null)
|
||||
{
|
||||
StatusText = "Agent-Engine ist nicht aktiv (kein API-Key).";
|
||||
StatusText = LocalizationManager.Instance["desktop.chat.noApiKey"];
|
||||
return;
|
||||
}
|
||||
|
||||
var config = SelectedAgent.Config;
|
||||
|
||||
IsRunning = true;
|
||||
StatusText = $"Führe '{config.DisplayName}' aus…";
|
||||
StatusText = $"Running '{config.DisplayName}'…";
|
||||
|
||||
try
|
||||
{
|
||||
@@ -369,7 +376,7 @@ public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
StatusText = outcome.Message;
|
||||
|
||||
if (!outcome.Ok)
|
||||
_logger?.LogWarning("Handlauf des Agenten fehlgeschlagen: {Grund}", outcome.Message);
|
||||
_logger?.LogWarning("Agent execution failed: {Reason}", outcome.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -394,8 +401,8 @@ public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
|
||||
var isIdentity = field == AgentTextField.Identity;
|
||||
var title = isIdentity
|
||||
? $"Identity bearbeiten — {SelectedAgent.DisplayName}"
|
||||
: $"Soul bearbeiten — {SelectedAgent.DisplayName}";
|
||||
? $"Edit Identity — {SelectedAgent.DisplayName}"
|
||||
: $"Edit Soul — {SelectedAgent.DisplayName}";
|
||||
|
||||
var current = isIdentity ? SelectedAgent.Identity : SelectedAgent.Soul;
|
||||
|
||||
@@ -418,7 +425,7 @@ public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
StatusText = $"Ordner nicht gefunden: {path}";
|
||||
StatusText = $"Folder not found: {path}";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -458,8 +465,8 @@ public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
ApplyPendingEdits();
|
||||
_host.AgentAdmin.Save();
|
||||
|
||||
_logger?.LogInformation("Agenten-Konfiguration gespeichert.");
|
||||
StatusText = "Agenten-Konfiguration gespeichert.";
|
||||
_logger?.LogInformation("Agents configuration saved.");
|
||||
StatusText = LocalizationManager.Instance["desktop.agents.saved"];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -74,7 +74,7 @@ public sealed partial class BackupPageViewModel : PageViewModel
|
||||
private int _keepCount = 10;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _statusText = "Bereit";
|
||||
private string _statusText = LocalizationManager.Instance["common.ready"];
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isBusy;
|
||||
@@ -88,7 +88,7 @@ public sealed partial class BackupPageViewModel : PageViewModel
|
||||
|
||||
public BackupPageViewModel() : this(null) { }
|
||||
|
||||
public BackupPageViewModel(AppHost? host) : base("Sicherung")
|
||||
public BackupPageViewModel(AppHost? host) : base("nav.backup")
|
||||
{
|
||||
_host = host;
|
||||
|
||||
|
||||
@@ -35,11 +35,11 @@ public sealed partial class ChatPageViewModel : PageViewModel
|
||||
private bool _isSending;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _statusText = "Bereit";
|
||||
private string _statusText = "Ready";
|
||||
|
||||
public ChatPageViewModel() : this(null) { }
|
||||
|
||||
public ChatPageViewModel(AppHost? host) : base("Chat")
|
||||
public ChatPageViewModel(AppHost? host) : base("nav.chat")
|
||||
{
|
||||
_host = host;
|
||||
|
||||
@@ -57,7 +57,7 @@ public sealed partial class ChatPageViewModel : PageViewModel
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusText = "Entwurfsmodus";
|
||||
StatusText = "Design Mode";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,14 +77,14 @@ public sealed partial class ChatPageViewModel : PageViewModel
|
||||
var isUser = entry.Role.Equals("user", StringComparison.OrdinalIgnoreCase);
|
||||
ChatEntries.Add(new ChatEntryItemViewModel(
|
||||
entry.Role,
|
||||
isUser ? "Du" : SelectedAgent.DisplayName,
|
||||
isUser ? "You" : SelectedAgent.DisplayName,
|
||||
entry.Content,
|
||||
entry.Timestamp,
|
||||
isUser
|
||||
));
|
||||
}
|
||||
|
||||
StatusText = $"Chat-Verlauf für '{SelectedAgent.DisplayName}' geladen ({ChatEntries.Count} Nachrichten).";
|
||||
StatusText = $"Chat history for '{SelectedAgent.DisplayName}' loaded ({ChatEntries.Count} messages).";
|
||||
}
|
||||
|
||||
private void OnEngineChatEntryAdded(string agentId, string role, string text, string? toolName)
|
||||
@@ -94,7 +94,7 @@ public sealed partial class ChatPageViewModel : PageViewModel
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var isUser = role.Equals("user", StringComparison.OrdinalIgnoreCase);
|
||||
var sender = isUser ? "Du" : (string.IsNullOrWhiteSpace(toolName) ? SelectedAgent.DisplayName : $"Tool: {toolName}");
|
||||
var sender = isUser ? "You" : (string.IsNullOrWhiteSpace(toolName) ? SelectedAgent.DisplayName : $"Tool: {toolName}");
|
||||
|
||||
ChatEntries.Add(new ChatEntryItemViewModel(
|
||||
role, sender, text, DateTime.Now, isUser
|
||||
@@ -107,7 +107,7 @@ public sealed partial class ChatPageViewModel : PageViewModel
|
||||
{
|
||||
if (_host?.Engine is null || SelectedAgent is null)
|
||||
{
|
||||
StatusText = "Engine nicht aktiv oder kein Agent ausgewählt.";
|
||||
StatusText = "Engine not active or no agent selected.";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ public sealed partial class ChatPageViewModel : PageViewModel
|
||||
|
||||
InputPrompt = "";
|
||||
IsSending = true;
|
||||
StatusText = $"{SelectedAgent.DisplayName} denkt nach…";
|
||||
StatusText = $"{SelectedAgent.DisplayName} is thinking…";
|
||||
|
||||
try
|
||||
{
|
||||
@@ -126,11 +126,11 @@ public sealed partial class ChatPageViewModel : PageViewModel
|
||||
var result = await Task.Run(() =>
|
||||
_host.Engine.ChatAsync(agent, text, instanceId, CancellationToken.None));
|
||||
|
||||
StatusText = $"{agent.DisplayName} fertig ({result.Status}, {result.TokensUsed:N0} Tokens).";
|
||||
StatusText = $"{agent.DisplayName} finished ({result.Status}, {result.TokensUsed:N0} tokens).";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"Fehler: {ex.Message}";
|
||||
StatusText = $"Error: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -39,7 +39,7 @@ public sealed partial class InfoPageViewModel : PageViewModel
|
||||
|
||||
public InfoPageViewModel() : this(null) { }
|
||||
|
||||
public InfoPageViewModel(AppHost? host) : base("Info")
|
||||
public InfoPageViewModel(AppHost? host) : base("nav.info")
|
||||
{
|
||||
AppVersion = AppHost.AppVersion;
|
||||
BuildSummary = AppHost.BuildSummary;
|
||||
@@ -51,16 +51,16 @@ public sealed partial class InfoPageViewModel : PageViewModel
|
||||
InstancePath = host.InstancePath;
|
||||
LogDirectory = host.LogDirectory;
|
||||
OpenRouterStatus = host.Engine is null
|
||||
? "Deaktiviert (kein API-Key hinterlegt)"
|
||||
: "Aktiv (AgentEngine bereit)";
|
||||
? "Disabled (no API key configured)"
|
||||
: "Active (AgentEngine ready)";
|
||||
}
|
||||
else
|
||||
{
|
||||
InstanceName = "Entwurfsmodus";
|
||||
InstanceName = "Design Mode";
|
||||
InstanceId = "00000000-0000-0000-0000-000000000000";
|
||||
InstancePath = @"C:\ClawdDotNet\Instances\Default";
|
||||
LogDirectory = @"C:\ClawdDotNet\Logs";
|
||||
OpenRouterStatus = "Entwurf";
|
||||
OpenRouterStatus = "Design";
|
||||
}
|
||||
|
||||
LoadAssemblyBuildInfos();
|
||||
|
||||
@@ -28,10 +28,23 @@ public sealed class InstanceChoiceViewModel(InstanceInfo info, bool isCurrent)
|
||||
? Info.InstanceName[..2].ToUpperInvariant()
|
||||
: Info.InstanceName.ToUpperInvariant();
|
||||
|
||||
public string Summary => $"{Info.AgentCount} Agenten · API-Key {Info.ApiKeyStatus.ToLowerInvariant()}";
|
||||
public string Summary
|
||||
{
|
||||
get
|
||||
{
|
||||
var keyStatus = IsReady
|
||||
? LocalizationManager.Instance["desktop.instancePicker.apiKeyConfigured"]
|
||||
: LocalizationManager.Instance["desktop.instancePicker.apiKeyMissing"];
|
||||
return string.Format(
|
||||
LocalizationManager.Instance["desktop.instancePicker.agentsCount"],
|
||||
Info.AgentCount,
|
||||
keyStatus);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Ohne API-Key kann die Instanz keine Agenten starten.</summary>
|
||||
public bool IsReady => Info.ApiKeyStatus.Equals("Konfiguriert", StringComparison.OrdinalIgnoreCase);
|
||||
public bool IsReady => Info.ApiKeyStatus.Equals("Konfiguriert", StringComparison.OrdinalIgnoreCase)
|
||||
|| Info.ApiKeyStatus.Equals("Configured", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>Die gerade laufende Instanz — traegt im Entwurf das Etikett „Aktiv".</summary>
|
||||
public bool IsCurrent { get; } = isCurrent;
|
||||
@@ -75,16 +88,19 @@ public sealed partial class InstancePickerViewModel : ViewModelBase
|
||||
/// </summary>
|
||||
public bool IsSwitchMode => _currentInstancePath is not null;
|
||||
|
||||
public string ConfirmLabel => IsSwitchMode ? "Wechseln" : "Starten";
|
||||
public string ConfirmLabel => IsSwitchMode
|
||||
? LocalizationManager.Instance["desktop.instancePicker.confirmSwitch"]
|
||||
: LocalizationManager.Instance["desktop.instancePicker.confirmStart"];
|
||||
|
||||
/// <summary>Ueberschrift des Dialogs — nicht aus ConfirmLabel zusammengesetzt,
|
||||
/// sonst stuende dort „Instanz Starten".</summary>
|
||||
public string Title => IsSwitchMode ? "Instanz wechseln" : "Instanz wählen";
|
||||
public string Title => IsSwitchMode
|
||||
? LocalizationManager.Instance["desktop.instancePicker.titleSwitch"]
|
||||
: LocalizationManager.Instance["desktop.instancePicker.titleSelect"];
|
||||
|
||||
public string Hint => IsSwitchMode
|
||||
? "Jede Instanz läuft unabhängig mit eigenen Agenten und Daten. "
|
||||
+ "Wechseln startet ClawdDotNet mit der gewählten Instanz neu."
|
||||
: "Jede Instanz läuft unabhängig mit eigenen Agenten und Daten.";
|
||||
? LocalizationManager.Instance["desktop.instancePicker.hintSwitch"]
|
||||
: LocalizationManager.Instance["desktop.instancePicker.hintSelect"];
|
||||
|
||||
/// <param name="currentInstancePath">
|
||||
/// Pfad der laufenden Instanz, oder <c>null</c> beim Start. Steuert Beschriftung und
|
||||
@@ -141,7 +157,7 @@ public sealed partial class InstancePickerViewModel : ViewModelBase
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
ErrorText = "Bitte einen Instanznamen eingeben.";
|
||||
ErrorText = LocalizationManager.Instance["desktop.instancePicker.nameRequired"];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -159,7 +175,9 @@ public sealed partial class InstancePickerViewModel : ViewModelBase
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException
|
||||
or ArgumentException)
|
||||
{
|
||||
ErrorText = $"Instanz konnte nicht angelegt werden: {ex.Message}";
|
||||
ErrorText = string.Format(
|
||||
LocalizationManager.Instance["desktop.instancePicker.createFailed"],
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,15 +33,15 @@ public sealed partial class LogPageViewModel : PageViewModel
|
||||
|
||||
public ObservableCollection<LogLine> Lines { get; } = [];
|
||||
|
||||
public ObservableCollection<string> Modules { get; } = ["Alle"];
|
||||
public ObservableCollection<string> Modules { get; } = [LocalizationManager.Instance["common.all"]];
|
||||
|
||||
public IReadOnlyList<string> Levels { get; } = ["Alle", "Info", "Warn", "Error"];
|
||||
public IReadOnlyList<string> Levels { get; } = [LocalizationManager.Instance["common.all"], "Info", "Warn", "Error"];
|
||||
|
||||
[ObservableProperty]
|
||||
private string _selectedModule = "Alle";
|
||||
private string _selectedModule = LocalizationManager.Instance["common.all"];
|
||||
|
||||
[ObservableProperty]
|
||||
private string _selectedLevel = "Alle";
|
||||
private string _selectedLevel = LocalizationManager.Instance["common.all"];
|
||||
|
||||
/// <summary>
|
||||
/// Hängt die Ansicht am unteren Ende? Sobald jemand nach oben scrollt, soll nicht
|
||||
@@ -50,7 +50,7 @@ public sealed partial class LogPageViewModel : PageViewModel
|
||||
[ObservableProperty]
|
||||
private bool _followTail = true;
|
||||
|
||||
public LogPageViewModel(AppHost? host) : base("Logs")
|
||||
public LogPageViewModel(AppHost? host) : base("nav.logs")
|
||||
{
|
||||
_logDirectory = host?.LogDirectory ?? "";
|
||||
|
||||
@@ -68,7 +68,10 @@ public sealed partial class LogPageViewModel : PageViewModel
|
||||
{
|
||||
if (_tail is null) return;
|
||||
|
||||
var module = SelectedModule == "Alle" ? null : SelectedModule;
|
||||
var isAllModule = string.Equals(SelectedModule, LocalizationManager.Instance["common.all"], StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(SelectedModule, "All", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(SelectedModule, "Alle", StringComparison.OrdinalIgnoreCase);
|
||||
var module = isAllModule ? null : SelectedModule;
|
||||
var level = SelectedLevel switch
|
||||
{
|
||||
"Info" => LogLevelFilter.Info,
|
||||
@@ -96,15 +99,16 @@ public sealed partial class LogPageViewModel : PageViewModel
|
||||
if (_tail is null) return;
|
||||
|
||||
var current = SelectedModule;
|
||||
var allText = LocalizationManager.Instance["common.all"];
|
||||
|
||||
Modules.Clear();
|
||||
Modules.Add("Alle");
|
||||
Modules.Add(allText);
|
||||
|
||||
foreach (var module in _tail.AvailableModules())
|
||||
Modules.Add(module);
|
||||
|
||||
// Auswahl halten, wenn es das Modul noch gibt.
|
||||
SelectedModule = Modules.Contains(current) ? current : "Alle";
|
||||
SelectedModule = Modules.Contains(current) ? current : allText;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
||||
@@ -46,14 +46,19 @@ public sealed partial class MainWindowViewModel : ViewModelBase
|
||||
// ─── Statusleiste ───
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(InstanceStatusLabel))]
|
||||
private string _instanceName = "—";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isApiReachable;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ApiStatusDisplay))]
|
||||
private string _apiStatusText = "unbekannt";
|
||||
|
||||
public string InstanceStatusLabel => LocalizationManager.Instance.Get("desktop.mainWindow.instanceLabel", InstanceName);
|
||||
public string ApiStatusDisplay => LocalizationManager.Instance.Get("desktop.mainWindow.apiStatus", ApiStatusText);
|
||||
|
||||
/// <summary>Kurzinfo zu Tokens und Kosten der letzten Stunde bzw. 24 Stunden.</summary>
|
||||
[ObservableProperty]
|
||||
private string _usageText = "—";
|
||||
@@ -86,7 +91,9 @@ public sealed partial class MainWindowViewModel : ViewModelBase
|
||||
|
||||
public string PageTitle => SelectedPage?.Title ?? "";
|
||||
|
||||
public string ThemeLabel => IsDarkTheme ? "Dunkelmodus" : "Hellmodus";
|
||||
public string ThemeLabel => IsDarkTheme
|
||||
? LocalizationManager.Instance["desktop.mainWindow.themeDark"]
|
||||
: LocalizationManager.Instance["desktop.mainWindow.themeLight"];
|
||||
|
||||
/// <summary>Fusszeile der Seitenleiste.</summary>
|
||||
public string VersionText => $"ClawdDotNet {AppHost.AppVersion} · Avalonia";
|
||||
@@ -128,20 +135,20 @@ public sealed partial class MainWindowViewModel : ViewModelBase
|
||||
{ chat, agents, tasks, tokens, agentChats, backup, logs, settings, info })
|
||||
Pages.Add(page);
|
||||
|
||||
NavGroups.Add(new NavGroupViewModel("Arbeiten",
|
||||
NavGroups.Add(new NavGroupViewModel("desktop.nav.work",
|
||||
[
|
||||
new NavItemViewModel(this, chat, "IconChat"),
|
||||
new NavItemViewModel(this, agents, "IconAgents"),
|
||||
new NavItemViewModel(this, tasks, "IconTasks")
|
||||
]));
|
||||
|
||||
NavGroups.Add(new NavGroupViewModel("Analyse",
|
||||
NavGroups.Add(new NavGroupViewModel("desktop.nav.analytics",
|
||||
[
|
||||
new NavItemViewModel(this, tokens, "IconChart"),
|
||||
new NavItemViewModel(this, agentChats, "IconAgentChats")
|
||||
]));
|
||||
|
||||
NavGroups.Add(new NavGroupViewModel("System",
|
||||
NavGroups.Add(new NavGroupViewModel("desktop.nav.system",
|
||||
[
|
||||
new NavItemViewModel(this, backup, "IconBackup"),
|
||||
new NavItemViewModel(this, logs, "IconLogs"),
|
||||
@@ -149,6 +156,19 @@ public sealed partial class MainWindowViewModel : ViewModelBase
|
||||
new NavItemViewModel(this, info, "IconInfo")
|
||||
]));
|
||||
|
||||
LocalizationManager.Instance.LanguageChanged += () =>
|
||||
{
|
||||
OnPropertyChanged(nameof(PageTitle));
|
||||
OnPropertyChanged(nameof(ThemeLabel));
|
||||
OnPropertyChanged(nameof(InstanceStatusLabel));
|
||||
OnPropertyChanged(nameof(ApiStatusDisplay));
|
||||
if (_host is null)
|
||||
{
|
||||
InstanceName = LocalizationManager.Instance["desktop.status.design"];
|
||||
ApiStatusText = LocalizationManager.Instance["desktop.status.design"];
|
||||
}
|
||||
};
|
||||
|
||||
// Die Leiste blendet sich aus, solange nichts anliegt — dafür muss die Ansicht
|
||||
// von Zu- und Abgängen erfahren, nicht nur von der Sammlung selbst.
|
||||
Notifications.CollectionChanged += (_, _) => OnPropertyChanged(nameof(HasNotifications));
|
||||
@@ -191,8 +211,8 @@ public sealed partial class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
if (host.Status is not { } status)
|
||||
{
|
||||
ApiStatusText = "kein API-Key";
|
||||
UsageText = "keine Nutzungsdaten";
|
||||
ApiStatusText = LocalizationManager.Instance["desktop.status.noApiKey"];
|
||||
UsageText = LocalizationManager.Instance["desktop.status.noUsageData"];
|
||||
CreditsText = "";
|
||||
return;
|
||||
}
|
||||
@@ -200,13 +220,15 @@ public sealed partial class MainWindowViewModel : ViewModelBase
|
||||
void Update()
|
||||
{
|
||||
IsApiReachable = status.IsApiReachable;
|
||||
ApiStatusText = status.IsApiReachable ? "OK" : "nicht erreichbar";
|
||||
ApiStatusText = status.IsApiReachable
|
||||
? LocalizationManager.Instance["desktop.status.reachable"]
|
||||
: LocalizationManager.Instance["desktop.status.unreachable"];
|
||||
UsageText = status.CreditsText;
|
||||
UsageTooltip = status.CreditsTooltip;
|
||||
|
||||
CreditsText = status.CreditRemaining is { } remaining && status.CreditBalance is { } total
|
||||
? $"Credits: ${remaining:F2} von ${total:F2}"
|
||||
: "Credits: —";
|
||||
? LocalizationManager.Instance.Get("desktop.mainWindow.credits", remaining, total)
|
||||
: LocalizationManager.Instance["desktop.mainWindow.creditsNone"];
|
||||
}
|
||||
|
||||
status.OnStatusUpdated += () => Dispatcher.UIThread.Post(Update);
|
||||
@@ -294,15 +316,22 @@ public sealed partial class MainWindowViewModel : ViewModelBase
|
||||
}
|
||||
|
||||
/// <summary>Eine Gruppe der Navigation („Arbeiten", „Analyse", „System").</summary>
|
||||
public sealed partial class NavGroupViewModel(string title, IReadOnlyList<NavItemViewModel> items)
|
||||
: ObservableObject
|
||||
public sealed partial class NavGroupViewModel : ObservableObject
|
||||
{
|
||||
public string Title { get; } = title;
|
||||
public IReadOnlyList<NavItemViewModel> Items { get; } = items;
|
||||
private readonly string _titleKey;
|
||||
public string Title => LocalizationManager.Instance.Get(_titleKey);
|
||||
public IReadOnlyList<NavItemViewModel> Items { get; }
|
||||
|
||||
/// <summary>Im eingeklappten Zustand entfällt die Gruppenüberschrift.</summary>
|
||||
[ObservableProperty]
|
||||
private bool _isTitleVisible = true;
|
||||
|
||||
public NavGroupViewModel(string titleKey, IReadOnlyList<NavItemViewModel> items)
|
||||
{
|
||||
_titleKey = titleKey;
|
||||
Items = items;
|
||||
LocalizationManager.Instance.LanguageChanged += () => OnPropertyChanged(nameof(Title));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -313,14 +342,14 @@ public sealed partial class NavGroupViewModel(string title, IReadOnlyList<NavIte
|
||||
/// zwar, ist aber bei übersetzten Bindungen ein Ausdruck mit Typumwandlung, den man beim
|
||||
/// nächsten Lesen erst wieder entziffern muss.</para>
|
||||
/// </summary>
|
||||
public sealed partial class NavItemViewModel(
|
||||
MainWindowViewModel owner, PageViewModel page, string iconKey) : ObservableObject
|
||||
public sealed partial class NavItemViewModel : ObservableObject
|
||||
{
|
||||
public PageViewModel Page { get; } = page;
|
||||
private readonly MainWindowViewModel _owner;
|
||||
public PageViewModel Page { get; }
|
||||
public string Title => Page.Title;
|
||||
|
||||
/// <summary>Name der Geometrie in <c>Styles/Icons.axaml</c>.</summary>
|
||||
public string IconKey { get; } = iconKey;
|
||||
public string IconKey { get; }
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isSelected;
|
||||
@@ -328,8 +357,16 @@ public sealed partial class NavItemViewModel(
|
||||
[ObservableProperty]
|
||||
private bool _isCollapsed;
|
||||
|
||||
public NavItemViewModel(MainWindowViewModel owner, PageViewModel page, string iconKey)
|
||||
{
|
||||
_owner = owner;
|
||||
Page = page;
|
||||
IconKey = iconKey;
|
||||
LocalizationManager.Instance.LanguageChanged += () => OnPropertyChanged(nameof(Title));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Select() => owner.SelectedPage = Page;
|
||||
private void Select() => _owner.SelectedPage = Page;
|
||||
}
|
||||
|
||||
/// <summary>Eine Zeile der Benachrichtigungsleiste.</summary>
|
||||
@@ -346,7 +383,14 @@ public sealed partial class NotificationViewModel(
|
||||
}
|
||||
|
||||
/// <summary>Ein Bereich des Hauptfensters.</summary>
|
||||
public abstract partial class PageViewModel(string title) : ViewModelBase
|
||||
public abstract partial class PageViewModel : ViewModelBase
|
||||
{
|
||||
public string Title { get; } = title;
|
||||
private readonly string _titleKey;
|
||||
public string Title => LocalizationManager.Instance.Get(_titleKey);
|
||||
|
||||
protected PageViewModel(string titleKey)
|
||||
{
|
||||
_titleKey = titleKey;
|
||||
LocalizationManager.Instance.LanguageChanged += () => OnPropertyChanged(nameof(Title));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,15 @@ public sealed partial class RestoreBackupViewModel : ViewModelBase
|
||||
public bool NeedsPassphrase => _manifest.HasSecrets;
|
||||
|
||||
public string SecretsDescription => _manifest.HasSecrets
|
||||
? $"{_manifest.SecretCount}, mit Passphrase geschützt"
|
||||
? $"{_manifest.SecretCount}, passphrase protected"
|
||||
: _manifest.SecretCount > 0
|
||||
? $"{_manifest.SecretCount} entfernt"
|
||||
: "keine";
|
||||
? $"{_manifest.SecretCount} excluded"
|
||||
: "none";
|
||||
|
||||
public string InstanceInfoText => string.Format(L["desktop.dialog.backupInstance"], InstanceName);
|
||||
public string CreatedAtText => string.Format(L["desktop.dialog.backupCreated"], CreatedAt);
|
||||
public string FileCountText => string.Format(L["desktop.dialog.backupFiles"], FileCount);
|
||||
public string SecretsText => string.Format(L["desktop.dialog.backupSecrets"], SecretsDescription);
|
||||
|
||||
[ObservableProperty]
|
||||
private string _targetDirectory = "";
|
||||
@@ -50,13 +55,13 @@ public sealed partial class RestoreBackupViewModel : ViewModelBase
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(TargetDirectory))
|
||||
{
|
||||
StatusMessage = "Bitte einen Zielordner angeben.";
|
||||
StatusMessage = "Please specify a target directory.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (NeedsPassphrase && string.IsNullOrEmpty(Passphrase))
|
||||
{
|
||||
StatusMessage = "Bitte Passphrase für geschützte Zugangsdaten eingeben.";
|
||||
StatusMessage = "Please enter the passphrase for protected credentials.";
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using ClawdDotNet.App.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ClawdDotNet.Desktop.Services;
|
||||
|
||||
namespace ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
@@ -107,13 +108,32 @@ public sealed partial class SettingsPageViewModel : PageViewModel
|
||||
[ObservableProperty]
|
||||
private string _statusText = "Bereit";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _selectedLanguage = LocalizationManager.DefaultLanguage;
|
||||
|
||||
public IReadOnlyList<LanguageOption> AvailableLanguages => LocalizationManager.Instance.AvailableLanguages;
|
||||
|
||||
public LanguageOption? SelectedLanguageOption
|
||||
{
|
||||
get => AvailableLanguages.FirstOrDefault(l => l.Code.Equals(SelectedLanguage, StringComparison.OrdinalIgnoreCase)) ?? AvailableLanguages.FirstOrDefault();
|
||||
set
|
||||
{
|
||||
if (value is not null && value.Code != SelectedLanguage)
|
||||
{
|
||||
SelectedLanguage = value.Code;
|
||||
OnPropertyChanged();
|
||||
LocalizationManager.Instance.SetLanguage(value.Code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<string> LogLevels { get; } = ["Debug", "Info", "Warn", "Error"];
|
||||
public List<string> UpdateChannels { get; } = ["prod", "beta", "dev"];
|
||||
public List<string> Environments { get; } = ["production", "development", "staging"];
|
||||
|
||||
public SettingsPageViewModel() : this(null) { }
|
||||
|
||||
public SettingsPageViewModel(AppHost? host) : base("Einstellungen")
|
||||
public SettingsPageViewModel(AppHost? host) : base("nav.settings")
|
||||
{
|
||||
_host = host;
|
||||
|
||||
@@ -124,7 +144,7 @@ public sealed partial class SettingsPageViewModel : PageViewModel
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusText = "Entwurfsmodus";
|
||||
StatusText = LocalizationManager.Instance["desktop.status.design"];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +167,8 @@ public sealed partial class SettingsPageViewModel : PageViewModel
|
||||
UpdateCheckEnabled = s.UpdateCheckEnabled;
|
||||
UpdateChannel = s.UpdateChannel;
|
||||
LicenseKey = s.LicenseKey;
|
||||
SelectedLanguage = s.Language ?? LocalizationManager.DefaultLanguage;
|
||||
OnPropertyChanged(nameof(SelectedLanguageOption));
|
||||
|
||||
var inst = _host.Instance;
|
||||
InstanceName = inst.InstanceName;
|
||||
@@ -177,8 +199,10 @@ public sealed partial class SettingsPageViewModel : PageViewModel
|
||||
s.UpdateCheckEnabled = UpdateCheckEnabled;
|
||||
s.UpdateChannel = UpdateChannel;
|
||||
s.LicenseKey = LicenseKey.Trim();
|
||||
s.Language = SelectedLanguage;
|
||||
|
||||
_host.Settings.Save();
|
||||
LocalizationManager.Instance.SetLanguage(SelectedLanguage);
|
||||
|
||||
var inst = _host.Instance;
|
||||
inst.InstanceName = InstanceName.Trim();
|
||||
@@ -190,13 +214,13 @@ public sealed partial class SettingsPageViewModel : PageViewModel
|
||||
|
||||
_host.SaveInstance(new InstanceChange(InstanceChangeKind.Settings));
|
||||
|
||||
_logger?.LogInformation("Einstellungen und Instanz-Konfiguration gespeichert.");
|
||||
_logger?.LogInformation("Settings and instance configuration saved.");
|
||||
|
||||
// Das Protokoll-Verzeichnis greift erst beim naechsten Start: Die
|
||||
// Protokollierung steht seit dem Aufbau des AppHost und schreibt bereits.
|
||||
StatusText = string.Equals(inst.LogDirectory, _logDirectoryAtLoad, StringComparison.Ordinal)
|
||||
? "Einstellungen erfolgreich gespeichert."
|
||||
: "Gespeichert. Das Protokoll-Verzeichnis gilt ab dem nächsten Start.";
|
||||
? LocalizationManager.Instance["desktop.settings.saved"]
|
||||
: LocalizationManager.Instance["desktop.settings.savedLogRestart"];
|
||||
|
||||
_logDirectoryAtLoad = inst.LogDirectory;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ public sealed partial class TasksPageViewModel : PageViewModel
|
||||
|
||||
public TasksPageViewModel() : this(null) { }
|
||||
|
||||
public TasksPageViewModel(AppHost? host) : base("Aufgaben")
|
||||
public TasksPageViewModel(AppHost? host) : base("nav.tasks")
|
||||
{
|
||||
_host = host;
|
||||
|
||||
@@ -122,7 +122,7 @@ public sealed partial class TasksPageViewModel : PageViewModel
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusText = "Entwurfsmodus";
|
||||
StatusText = LocalizationManager.Instance["desktop.status.design"];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,15 +73,15 @@ public sealed partial class TokenUsagePageViewModel : PageViewModel
|
||||
|
||||
public TokenUsagePageViewModel() : this(null) { }
|
||||
|
||||
public TokenUsagePageViewModel(AppHost? host) : base("Tokennutzung")
|
||||
public TokenUsagePageViewModel(AppHost? host) : base("nav.usage")
|
||||
{
|
||||
_host = host;
|
||||
|
||||
if (host?.Usage is null)
|
||||
{
|
||||
StatusText = host is null
|
||||
? "Entwurfsmodus"
|
||||
: "Keine Verbrauchsdatenbank für diese Instanz.";
|
||||
? "Design mode"
|
||||
: "No usage database for this instance.";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ public sealed partial class TokenUsagePageViewModel : PageViewModel
|
||||
TotalRuns = daily.Sum(d => d.RunCount).ToString("N0");
|
||||
|
||||
IsEmpty = tokens == 0 && runs.Count == 0;
|
||||
StatusText = IsEmpty ? "In den letzten sieben Tagen ist kein Lauf verzeichnet." : "";
|
||||
StatusText = IsEmpty ? LocalizationManager.Instance["desktop.usage.empty"] : "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using ClawdDotNet.Desktop.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace ClawdDotNet.Desktop.ViewModels;
|
||||
@@ -11,5 +12,11 @@ namespace ClawdDotNet.Desktop.ViewModels;
|
||||
/// mit Benachrichtigung. Das spart gegenüber der WinForms-Fassung, in der die
|
||||
/// Oberfläche bei jeder Änderung von Hand nachgezogen wurde
|
||||
/// (38 Stellen mit <c>Invoke</c>/<c>BeginInvoke</c>).
|
||||
///
|
||||
/// <para><see cref="L"/> macht die Sprachpakete in jeder Ansicht als
|
||||
/// <c>{Binding L[schluessel]}</c> erreichbar.</para>
|
||||
/// </summary>
|
||||
public abstract class ViewModelBase : ObservableObject;
|
||||
public abstract class ViewModelBase : ObservableObject
|
||||
{
|
||||
public LocalizationManager L => LocalizationManager.Instance;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user