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
@@ -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;
}