From ad71682c2d8792eff6505d72eb1a4ebdd5edd768 Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 24 Sep 2026 09:43:51 +0200 Subject: [PATCH] i18n: Englisch als Hauptsprache, Sprachpakete fuer Desktop und WebUI (#661) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/ClawdDotNet.App/Settings/AppSettings.cs | 6 + src/ClawdDotNet.Core/Tasks/TaskHinweise.cs | 8 +- src/ClawdDotNet.Desktop/App.axaml.cs | 54 +- .../ClawdDotNet.Desktop.csproj | 11 + .../Services/LocalizationManager.cs | 199 ++++++ .../Services/LocalizeExtension.cs | 30 + .../ViewModels/AddAgentViewModel.cs | 4 +- .../ViewModels/AddJobViewModel.cs | 6 +- .../ViewModels/AddServiceViewModel.cs | 4 +- .../ViewModels/AgentChatsPageViewModel.cs | 8 +- .../ViewModels/AgentsPageViewModel.cs | 31 +- .../ViewModels/BackupPageViewModel.cs | 4 +- .../ViewModels/ChatPageViewModel.cs | 20 +- .../ViewModels/InfoPageViewModel.cs | 10 +- .../ViewModels/InstancePickerViewModel.cs | 36 +- .../ViewModels/LogPageViewModel.cs | 20 +- .../ViewModels/MainWindowViewModel.cs | 84 ++- .../ViewModels/RestoreBackupViewModel.cs | 15 +- .../ViewModels/SettingsPageViewModel.cs | 34 +- .../ViewModels/TasksPageViewModel.cs | 4 +- .../ViewModels/TokenUsagePageViewModel.cs | 8 +- .../ViewModels/ViewModelBase.cs | 9 +- .../Views/AddAgentWindow.axaml | 16 +- .../Views/AddJobWindow.axaml | 28 +- .../Views/AddServiceWindow.axaml | 16 +- .../Views/AgentChatsPageView.axaml | 4 +- .../Views/AgentsPageView.axaml | 61 +- .../Views/BackupPageView.axaml | 62 +- .../Views/ChatPageView.axaml | 10 +- .../Views/InfoPageView.axaml | 36 +- .../Views/InstancePickerWindow.axaml | 12 +- .../Views/LicenseWindow.axaml | 10 +- .../Views/LogPageView.axaml | 12 +- .../Views/MainWindow.axaml | 14 +- .../Views/RestoreBackupWindow.axaml | 28 +- .../Views/SettingsPageView.axaml | 58 +- .../Views/TasksPageView.axaml | 64 +- .../Views/TextEditorWindow.axaml | 6 +- .../Views/TextEditorWindow.axaml.cs | 3 + .../Views/TokenUsagePageView.axaml | 22 +- .../Views/ToolSettingsView.axaml | 76 +-- src/ClawdDotNet.Desktop/i18n/de.json | 339 ++++++++++ src/ClawdDotNet.Desktop/i18n/en.json | 339 ++++++++++ src/ClawdDotNet.Manager.Web/wwwroot/app.js | 33 +- .../wwwroot/i18n/de.json | 594 +++++++++++++++++- .../wwwroot/i18n/en.json | 544 +++++++++++++++- .../wwwroot/index.html | 90 +-- .../wwwroot/modules/dom.js | 18 +- .../wwwroot/modules/i18n.js | 21 + .../wwwroot/views/agent-chats.js | 53 +- .../wwwroot/views/agents.js | 148 +++-- .../wwwroot/views/backup.js | 180 +++--- .../wwwroot/views/chat.js | 63 +- .../wwwroot/views/info.js | 45 +- .../wwwroot/views/instances.js | 139 ++-- .../wwwroot/views/login.js | 5 +- .../wwwroot/views/logs.js | 54 +- .../wwwroot/views/services.js | 63 +- .../wwwroot/views/settings.js | 147 +++-- .../wwwroot/views/staging.js | 86 +-- .../wwwroot/views/tasks.js | 139 ++-- .../wwwroot/views/usage.js | 100 +-- .../AgentCommTool.cs | 24 +- .../AgentEditorTool.cs | 114 ++-- .../AgentSpawnTool.cs | 26 +- .../DatabaseTool.cs | 29 +- .../DirectAPITool.cs | 16 +- src/ClawdDotNet.Tools.FTP/FtpTool.cs | 66 +- src/ClawdDotNet.Tools.FileRW/FileRWTool.cs | 188 +++--- src/ClawdDotNet.Tools.FileRW/WorkspacePath.cs | 8 +- src/ClawdDotNet.Tools.Mail/MailTool.cs | 60 +- src/ClawdDotNet.Tools.Memory/MemoryTool.cs | 120 ++-- .../RocketChatClient.cs | 24 +- .../RocketChatConfig.cs | 8 +- .../RocketChatTool.cs | 241 ++++--- .../WorkspaceFile.cs | 10 +- .../SocialMediaManagerTool.cs | 283 ++++----- .../YouTubeUrl.cs | 20 +- .../TaskboardTool.cs | 160 ++--- .../TelegramTool.cs | 46 +- .../TelegramClientManager.cs | 2 +- .../TelegramClientTool.cs | 24 +- .../HtmlExtraktor.cs | 6 +- src/ClawdDotNet.Tools.WebFetch/JsonFilter.cs | 8 +- src/ClawdDotNet.Tools.WebFetch/UrlGuard.cs | 14 +- .../WebFetchTool.cs | 63 +- src/ClawdDotNet.Tools.WebFetch/WebInhalt.cs | 10 +- .../WebMonitorTool.cs | 26 +- .../FileRW/NachschauTests.cs | 2 +- .../FileRW/SammelLesenTests.cs | 30 +- .../FileRW/VerzeichnisListeTests.cs | 20 +- .../Memory/MemoryToolTests.cs | 16 +- .../Taskboard/DublettenTests.cs | 12 +- .../Vorpruefung/VorpruefungTests.cs | 2 +- .../WebFetch/HtmlExtraktorTests.cs | 4 +- .../WebFetch/JsonFilterTests.cs | 4 +- .../WebFetch/UrlGuardAufloesungTests.cs | 2 +- 97 files changed, 4103 insertions(+), 1928 deletions(-) create mode 100644 src/ClawdDotNet.Desktop/Services/LocalizationManager.cs create mode 100644 src/ClawdDotNet.Desktop/Services/LocalizeExtension.cs create mode 100644 src/ClawdDotNet.Desktop/i18n/de.json create mode 100644 src/ClawdDotNet.Desktop/i18n/en.json diff --git a/src/ClawdDotNet.App/Settings/AppSettings.cs b/src/ClawdDotNet.App/Settings/AppSettings.cs index 7926b07..325061d 100644 --- a/src/ClawdDotNet.App/Settings/AppSettings.cs +++ b/src/ClawdDotNet.App/Settings/AppSettings.cs @@ -64,6 +64,12 @@ public sealed class AppSettings [JsonPropertyName("theme")] public string Theme { get; set; } = "Light"; + [Category("UI")] + [DisplayName("Sprache")] + [Description("Oberflächensprache ('en' für Englisch, 'de' für Deutsch).")] + [JsonPropertyName("language")] + public string Language { get; set; } = "en"; + [Category("UI")] [DisplayName("Seitenleiste eingeklappt")] [Description("Merkt sich, ob die Navigation auf Symbolbreite zusammengeklappt war.")] diff --git a/src/ClawdDotNet.Core/Tasks/TaskHinweise.cs b/src/ClawdDotNet.Core/Tasks/TaskHinweise.cs index cda8191..b253264 100644 --- a/src/ClawdDotNet.Core/Tasks/TaskHinweise.cs +++ b/src/ClawdDotNet.Core/Tasks/TaskHinweise.cs @@ -24,9 +24,9 @@ public static class TaskHinweise return null; var agentId = TaskAssignee.AgentId(task.Assignee); - return $"Hinweis: '{task.Assignee}' führt jeden Lauf im gespeicherten Chatverlauf des Agenten fort. " - + "Bei einer wiederkehrenden Aufgabe wächst dieser Verlauf mit jedem Lauf – und mit ihm die " - + "Kosten je Lauf, bis die Kompaktierung greift. Für wiederkehrende Aufgaben ist " - + $"'@new:{agentId}' (frischer Lauf) meist die bessere Wahl."; + return $"Note: '{task.Assignee}' continues each run in the stored chat history of the agent. " + + "For a recurring task, this history grows with each run — and with it the " + + "cost per run until compaction kicks in. For recurring tasks, " + + $"'@new:{agentId}' (fresh run) is usually the better choice."; } } diff --git a/src/ClawdDotNet.Desktop/App.axaml.cs b/src/ClawdDotNet.Desktop/App.axaml.cs index 26e002a..4102e1d 100644 --- a/src/ClawdDotNet.Desktop/App.axaml.cs +++ b/src/ClawdDotNet.Desktop/App.axaml.cs @@ -111,6 +111,7 @@ public partial class App : Application _managed = CommandLine.IsManaged(desktop.Args); ApplySavedThemeEarly(); + ApplySavedLanguageEarly(); desktop.ShutdownRequested += async (_, _) => { @@ -156,6 +157,23 @@ public partial class App : Application } } + /// + /// Setzt die gespeicherte Sprache, bevor das erste Fenster erscheint. + /// + private static void ApplySavedLanguageEarly() + { + try + { + var settings = new SettingsManager(); + settings.Load(); + LocalizationManager.Instance.SetLanguage(settings.AppSettings.Language); + } + catch (Exception) + { + LocalizationManager.Instance.SetLanguage(LocalizationManager.DefaultLanguage); + } + } + private async Task StartAsync(IClassicDesktopStyleApplicationLifetime desktop) { var result = await AppHost.StartAsync(new AppHost.Callbacks @@ -183,6 +201,7 @@ public partial class App : Application } _host = result.Host; + LocalizationManager.Instance.SetLanguage(_host.Settings.AppSettings.Language); HookErrorReporting(_host); HookLicenseWatch(_host, desktop); @@ -664,27 +683,28 @@ public partial class App : Application => await Dispatcher.UIThread.InvokeAsync(async () => { var completion = new TaskCompletionSource(); + var l = LocalizationManager.Instance; var managerBtn = new Button { - Content = "Instanzmanager starten (Empfohlen)", + Content = l["desktop.startMode.managerBtn"], Classes = { "primary" } }; var switchBtn = new Button { - Content = "Instanz trotzdem wechseln" + Content = l["desktop.standaloneSwitch.switchAnyway"] }; var cancelBtn = new Button { - Content = "Abbrechen", + Content = l["common.cancel"], IsCancel = true }; var window = new Window { - Title = "ClawdDotNet – Instanzwechsel (Standalone)", + Title = l["desktop.standaloneSwitch.title"], Width = 540, SizeToContent = SizeToContent.Height, CanResize = false, @@ -698,15 +718,12 @@ public partial class App : Application { new TextBlock { - Text = "Achtung: Standalone-Modus", + Text = l["desktop.standaloneSwitch.warning"], Classes = { "h1" } }, new TextBlock { - Text = "Diese Instanz läuft eigenständig ohne Instanzmanager.\n\n" - + "Wenn du die Instanz hier wechselst, wird diese Instanz vollständig beendet " - + "und alle laufenden Agenten und Hintergrunddienste werden gestoppt.\n\n" - + "Empfehlung: Starte stattdessen den Instanzmanager, um mehrere Instanzen gleichzeitig und unabhängig voneinander zu betreiben.", + Text = l["desktop.standaloneSwitch.text"], TextWrapping = TextWrapping.Wrap }, new StackPanel @@ -752,27 +769,28 @@ public partial class App : Application => await Dispatcher.UIThread.InvokeAsync(async () => { var completion = new TaskCompletionSource(); + var l = LocalizationManager.Instance; var managerBtn = new Button { - Content = "Instanzmanager starten (Empfohlen)", + Content = l["desktop.startMode.managerBtn"], Classes = { "primary" } }; var standaloneBtn = new Button { - Content = "Als Standalone-Instanz starten" + Content = l["desktop.startMode.standaloneBtn"] }; var cancelBtn = new Button { - Content = "Abbrechen", + Content = l["common.cancel"], IsCancel = true }; var window = new Window { - Title = "ClawdDotNet – Startmodus wählen", + Title = l["desktop.startMode.title"], Width = 540, SizeToContent = SizeToContent.Height, CanResize = false, @@ -785,16 +803,12 @@ public partial class App : Application { new TextBlock { - Text = "Wie möchtest du ClawdDotNet starten?", + Text = l["desktop.startMode.question"], Classes = { "h1" } }, new TextBlock { - Text = "• Instanzmanager (Empfohlen):\n" - + " Startet den zentralen Hintergrund-Manager und öffnet die Verwaltung im Browser. " - + "Ermöglicht den stabilen Dauerbetrieb mehrerer Instanzen parallel ohne Absturzrisiko.\n\n" - + "• Standalone-Modus:\n" - + " Startet genau eine Instanz direkt als einzelnes Desktop-Fenster.", + Text = l["desktop.startMode.explanation"], TextWrapping = TextWrapping.Wrap }, new StackPanel @@ -948,7 +962,7 @@ public partial class App : Application var completion = new TaskCompletionSource(); var input = new TextBox(); - var ok = new Button { Content = "OK", IsDefault = true }; + var ok = new Button { Content = LocalizationManager.Instance["common.ok"], IsDefault = true }; var window = new Window { diff --git a/src/ClawdDotNet.Desktop/ClawdDotNet.Desktop.csproj b/src/ClawdDotNet.Desktop/ClawdDotNet.Desktop.csproj index 1880c96..d26a64b 100644 --- a/src/ClawdDotNet.Desktop/ClawdDotNet.Desktop.csproj +++ b/src/ClawdDotNet.Desktop/ClawdDotNet.Desktop.csproj @@ -18,6 +18,10 @@ false + + + + @@ -38,6 +42,13 @@ + + + + PreserveNewest + + + diff --git a/src/ClawdDotNet.Desktop/Services/LocalizationManager.cs b/src/ClawdDotNet.Desktop/Services/LocalizationManager.cs new file mode 100644 index 0000000..c64bb3c --- /dev/null +++ b/src/ClawdDotNet.Desktop/Services/LocalizationManager.cs @@ -0,0 +1,199 @@ +using System.ComponentModel; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text.Json; + +namespace ClawdDotNet.Desktop.Services; + +public sealed record LanguageOption(string Code, string Name) +{ + public override string ToString() => Name; +} + +/// +/// Manages application localization for Avalonia UI. +/// Supports JSON language packs with fallback to English, dynamic language switching, +/// and live UI re-binding via indexer property notification. +/// +public sealed class LocalizationManager : INotifyPropertyChanged +{ + public static LocalizationManager Instance { get; } = new(); + + public const string DefaultLanguage = "en"; + public const string FallbackLanguage = "en"; + + private readonly Dictionary _fallbackStrings = new(StringComparer.OrdinalIgnoreCase); + private Dictionary _activeStrings = new(StringComparer.OrdinalIgnoreCase); + private string _currentLanguage = DefaultLanguage; + + public IReadOnlyList AvailableLanguages { get; } = + [ + new("en", "English"), + new("de", "Deutsch") + ]; + + public event PropertyChangedEventHandler? PropertyChanged; + public event Action? LanguageChanged; + + public string CurrentLanguage + { + get => _currentLanguage; + private set + { + if (_currentLanguage != value) + { + _currentLanguage = value; + OnPropertyChanged(); + } + } + } + + [IndexerName("Item")] + public string this[string key] => Get(key); + + private LocalizationManager() + { + LoadFallbackPack(); + SetLanguage(DefaultLanguage); + } + + private void LoadFallbackPack() + { + _fallbackStrings.Clear(); + var json = LoadLanguageJson(FallbackLanguage); + if (!string.IsNullOrWhiteSpace(json)) + { + ParseJsonIntoDictionary(json, _fallbackStrings); + } + } + + public void SetLanguage(string? languageCode) + { + var target = string.IsNullOrWhiteSpace(languageCode) ? DefaultLanguage : languageCode.Trim().ToLowerInvariant(); + + if (target == FallbackLanguage) + { + _activeStrings = _fallbackStrings; + } + else + { + var pack = new Dictionary(StringComparer.OrdinalIgnoreCase); + var json = LoadLanguageJson(target); + if (!string.IsNullOrWhiteSpace(json)) + { + ParseJsonIntoDictionary(json, pack); + } + _activeStrings = pack; + } + + CurrentLanguage = target; + + // Notify indexer bindings so Avalonia refreshes all localized strings on screen + OnPropertyChanged("Item"); + OnPropertyChanged("Item[]"); + LanguageChanged?.Invoke(); + } + + public string Get(string key, params object[] args) + { + if (string.IsNullOrEmpty(key)) return ""; + + string? template = null; + if (!_activeStrings.TryGetValue(key, out template)) + { + _fallbackStrings.TryGetValue(key, out template); + } + + template ??= key; + + if (args.Length > 0) + { + try + { + return string.Format(template, args); + } + catch + { + return template; + } + } + + return template; + } + + private static string? LoadLanguageJson(string langCode) + { + // 1. Try file on disk in AppContext.BaseDirectory/i18n/{langCode}.json + var localPath = Path.Combine(AppContext.BaseDirectory, "i18n", $"{langCode}.json"); + if (File.Exists(localPath)) + { + try + { + return File.ReadAllText(localPath); + } + catch + { + // Fall through to embedded resource + } + } + + // 2. Try embedded resource in ClawdDotNet assembly + var assembly = Assembly.GetExecutingAssembly(); + var resourceName = assembly.GetManifestResourceNames() + .FirstOrDefault(r => r.EndsWith($"i18n.{langCode}.json", StringComparison.OrdinalIgnoreCase)); + + if (resourceName is not null) + { + using var stream = assembly.GetManifestResourceStream(resourceName); + if (stream is not null) + { + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + } + + return null; + } + + private static void ParseJsonIntoDictionary(string json, Dictionary dict) + { + try + { + using var doc = JsonDocument.Parse(json); + FlattenElement("", doc.RootElement, dict); + } + catch + { + // Invalid JSON ignored + } + } + + private static void FlattenElement(string prefix, JsonElement element, Dictionary dict) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + foreach (var prop in element.EnumerateObject()) + { + var childPrefix = string.IsNullOrEmpty(prefix) ? prop.Name : $"{prefix}.{prop.Name}"; + FlattenElement(childPrefix, prop.Value, dict); + } + break; + + case JsonValueKind.String: + dict[prefix] = element.GetString() ?? ""; + break; + + case JsonValueKind.Number: + case JsonValueKind.True: + case JsonValueKind.False: + dict[prefix] = element.ToString(); + break; + } + } + + private void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} diff --git a/src/ClawdDotNet.Desktop/Services/LocalizeExtension.cs b/src/ClawdDotNet.Desktop/Services/LocalizeExtension.cs new file mode 100644 index 0000000..2e89a57 --- /dev/null +++ b/src/ClawdDotNet.Desktop/Services/LocalizeExtension.cs @@ -0,0 +1,30 @@ +using Avalonia.Data; +using Avalonia.Markup.Xaml; + +namespace ClawdDotNet.Desktop.Services; + +/// +/// Markup extension for Avalonia XAML: +/// Usage: Text="{services:Localize nav.chat}" +/// Updates automatically when language changes. +/// +public sealed class LocalizeExtension : MarkupExtension +{ + public string Key { get; set; } = ""; + + public LocalizeExtension() { } + + public LocalizeExtension(string key) + { + Key = key; + } + + public override object ProvideValue(IServiceProvider serviceProvider) + { + return new Binding($"[{Key}]") + { + Source = LocalizationManager.Instance, + Mode = BindingMode.OneWay + }; + } +} diff --git a/src/ClawdDotNet.Desktop/ViewModels/AddAgentViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/AddAgentViewModel.cs index 5b016ed..c10ef19 100644 --- a/src/ClawdDotNet.Desktop/ViewModels/AddAgentViewModel.cs +++ b/src/ClawdDotNet.Desktop/ViewModels/AddAgentViewModel.cs @@ -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; } diff --git a/src/ClawdDotNet.Desktop/ViewModels/AddJobViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/AddJobViewModel.cs index 516f0fa..f070f11 100644 --- a/src/ClawdDotNet.Desktop/ViewModels/AddJobViewModel.cs +++ b/src/ClawdDotNet.Desktop/ViewModels/AddJobViewModel.cs @@ -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; } diff --git a/src/ClawdDotNet.Desktop/ViewModels/AddServiceViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/AddServiceViewModel.cs index 6ec6fd7..6b2f8fe 100644 --- a/src/ClawdDotNet.Desktop/ViewModels/AddServiceViewModel.cs +++ b/src/ClawdDotNet.Desktop/ViewModels/AddServiceViewModel.cs @@ -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; } diff --git a/src/ClawdDotNet.Desktop/ViewModels/AgentChatsPageViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/AgentChatsPageViewModel.cs index c67bd78..35b319a 100644 --- a/src/ClawdDotNet.Desktop/ViewModels/AgentChatsPageViewModel.cs +++ b/src/ClawdDotNet.Desktop/ViewModels/AgentChatsPageViewModel.cs @@ -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; } diff --git a/src/ClawdDotNet.Desktop/ViewModels/AgentsPageViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/AgentsPageViewModel.cs index 7a3da72..e513bb8 100644 --- a/src/ClawdDotNet.Desktop/ViewModels/AgentsPageViewModel.cs +++ b/src/ClawdDotNet.Desktop/ViewModels/AgentsPageViewModel.cs @@ -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)); } /// @@ -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"]; } /// diff --git a/src/ClawdDotNet.Desktop/ViewModels/BackupPageViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/BackupPageViewModel.cs index 7ca6943..92b362a 100644 --- a/src/ClawdDotNet.Desktop/ViewModels/BackupPageViewModel.cs +++ b/src/ClawdDotNet.Desktop/ViewModels/BackupPageViewModel.cs @@ -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; diff --git a/src/ClawdDotNet.Desktop/ViewModels/ChatPageViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/ChatPageViewModel.cs index cb8a3d6..e9db1bb 100644 --- a/src/ClawdDotNet.Desktop/ViewModels/ChatPageViewModel.cs +++ b/src/ClawdDotNet.Desktop/ViewModels/ChatPageViewModel.cs @@ -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 { diff --git a/src/ClawdDotNet.Desktop/ViewModels/InfoPageViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/InfoPageViewModel.cs index 1fb208b..e1c4aef 100644 --- a/src/ClawdDotNet.Desktop/ViewModels/InfoPageViewModel.cs +++ b/src/ClawdDotNet.Desktop/ViewModels/InfoPageViewModel.cs @@ -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(); diff --git a/src/ClawdDotNet.Desktop/ViewModels/InstancePickerViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/InstancePickerViewModel.cs index 349e58b..17fc056 100644 --- a/src/ClawdDotNet.Desktop/ViewModels/InstancePickerViewModel.cs +++ b/src/ClawdDotNet.Desktop/ViewModels/InstancePickerViewModel.cs @@ -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); + } + } /// Ohne API-Key kann die Instanz keine Agenten starten. - public bool IsReady => Info.ApiKeyStatus.Equals("Konfiguriert", StringComparison.OrdinalIgnoreCase); + public bool IsReady => Info.ApiKeyStatus.Equals("Konfiguriert", StringComparison.OrdinalIgnoreCase) + || Info.ApiKeyStatus.Equals("Configured", StringComparison.OrdinalIgnoreCase); /// Die gerade laufende Instanz — traegt im Entwurf das Etikett „Aktiv". public bool IsCurrent { get; } = isCurrent; @@ -75,16 +88,19 @@ public sealed partial class InstancePickerViewModel : ViewModelBase /// 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"]; /// Ueberschrift des Dialogs — nicht aus ConfirmLabel zusammengesetzt, /// sonst stuende dort „Instanz Starten". - 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"]; /// /// Pfad der laufenden Instanz, oder null 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); } } diff --git a/src/ClawdDotNet.Desktop/ViewModels/LogPageViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/LogPageViewModel.cs index b573dd6..a34f12c 100644 --- a/src/ClawdDotNet.Desktop/ViewModels/LogPageViewModel.cs +++ b/src/ClawdDotNet.Desktop/ViewModels/LogPageViewModel.cs @@ -33,15 +33,15 @@ public sealed partial class LogPageViewModel : PageViewModel public ObservableCollection Lines { get; } = []; - public ObservableCollection Modules { get; } = ["Alle"]; + public ObservableCollection Modules { get; } = [LocalizationManager.Instance["common.all"]]; - public IReadOnlyList Levels { get; } = ["Alle", "Info", "Warn", "Error"]; + public IReadOnlyList 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"]; /// /// 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] diff --git a/src/ClawdDotNet.Desktop/ViewModels/MainWindowViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/MainWindowViewModel.cs index 8dda372..8003868 100644 --- a/src/ClawdDotNet.Desktop/ViewModels/MainWindowViewModel.cs +++ b/src/ClawdDotNet.Desktop/ViewModels/MainWindowViewModel.cs @@ -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); + /// Kurzinfo zu Tokens und Kosten der letzten Stunde bzw. 24 Stunden. [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"]; /// Fusszeile der Seitenleiste. 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 } /// Eine Gruppe der Navigation („Arbeiten", „Analyse", „System"). -public sealed partial class NavGroupViewModel(string title, IReadOnlyList items) - : ObservableObject +public sealed partial class NavGroupViewModel : ObservableObject { - public string Title { get; } = title; - public IReadOnlyList Items { get; } = items; + private readonly string _titleKey; + public string Title => LocalizationManager.Instance.Get(_titleKey); + public IReadOnlyList Items { get; } /// Im eingeklappten Zustand entfällt die Gruppenüberschrift. [ObservableProperty] private bool _isTitleVisible = true; + + public NavGroupViewModel(string titleKey, IReadOnlyList items) + { + _titleKey = titleKey; + Items = items; + LocalizationManager.Instance.LanguageChanged += () => OnPropertyChanged(nameof(Title)); + } } /// @@ -313,14 +342,14 @@ public sealed partial class NavGroupViewModel(string title, IReadOnlyList /// -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; /// Name der Geometrie in Styles/Icons.axaml. - 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; } /// Eine Zeile der Benachrichtigungsleiste. @@ -346,7 +383,14 @@ public sealed partial class NotificationViewModel( } /// Ein Bereich des Hauptfensters. -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)); + } } diff --git a/src/ClawdDotNet.Desktop/ViewModels/RestoreBackupViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/RestoreBackupViewModel.cs index d72a847..d93bf37 100644 --- a/src/ClawdDotNet.Desktop/ViewModels/RestoreBackupViewModel.cs +++ b/src/ClawdDotNet.Desktop/ViewModels/RestoreBackupViewModel.cs @@ -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; } diff --git a/src/ClawdDotNet.Desktop/ViewModels/SettingsPageViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/SettingsPageViewModel.cs index d04b76e..adcca0a 100644 --- a/src/ClawdDotNet.Desktop/ViewModels/SettingsPageViewModel.cs +++ b/src/ClawdDotNet.Desktop/ViewModels/SettingsPageViewModel.cs @@ -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 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 LogLevels { get; } = ["Debug", "Info", "Warn", "Error"]; public List UpdateChannels { get; } = ["prod", "beta", "dev"]; public List 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; } diff --git a/src/ClawdDotNet.Desktop/ViewModels/TasksPageViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/TasksPageViewModel.cs index fad9c58..9051afc 100644 --- a/src/ClawdDotNet.Desktop/ViewModels/TasksPageViewModel.cs +++ b/src/ClawdDotNet.Desktop/ViewModels/TasksPageViewModel.cs @@ -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"]; } } diff --git a/src/ClawdDotNet.Desktop/ViewModels/TokenUsagePageViewModel.cs b/src/ClawdDotNet.Desktop/ViewModels/TokenUsagePageViewModel.cs index 562133b..b6068b9 100644 --- a/src/ClawdDotNet.Desktop/ViewModels/TokenUsagePageViewModel.cs +++ b/src/ClawdDotNet.Desktop/ViewModels/TokenUsagePageViewModel.cs @@ -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"] : ""; } /// diff --git a/src/ClawdDotNet.Desktop/ViewModels/ViewModelBase.cs b/src/ClawdDotNet.Desktop/ViewModels/ViewModelBase.cs index b453a19..0b15a99 100644 --- a/src/ClawdDotNet.Desktop/ViewModels/ViewModelBase.cs +++ b/src/ClawdDotNet.Desktop/ViewModels/ViewModelBase.cs @@ -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 Invoke/BeginInvoke). +/// +/// macht die Sprachpakete in jeder Ansicht als +/// {Binding L[schluessel]} erreichbar. /// -public abstract class ViewModelBase : ObservableObject; +public abstract class ViewModelBase : ObservableObject +{ + public LocalizationManager L => LocalizationManager.Instance; +} diff --git a/src/ClawdDotNet.Desktop/Views/AddAgentWindow.axaml b/src/ClawdDotNet.Desktop/Views/AddAgentWindow.axaml index 8106bec..4704a23 100644 --- a/src/ClawdDotNet.Desktop/Views/AddAgentWindow.axaml +++ b/src/ClawdDotNet.Desktop/Views/AddAgentWindow.axaml @@ -8,7 +8,7 @@ Padding="16" WindowStartupLocation="CenterOwner" CanResize="False" - Title="Neuen Agenten anlegen" + Title="{Binding L[desktop.dialog.addAgent]}" x:Class="ClawdDotNet.Desktop.Views.AddAgentWindow" x:DataType="vm:AddAgentViewModel"> @@ -16,8 +16,8 @@ - @@ -42,7 +42,7 @@ IsEnabled="{Binding SelectedAgent, Converter={x:Static ObjectConverters.IsNotNull}}"> - + @@ -52,7 +52,7 @@ IsEnabled="{Binding !IsRunning}"> - + @@ -62,7 +62,7 @@ IsEnabled="{Binding SelectedAgent, Converter={x:Static ObjectConverters.IsNotNull}}"> - + @@ -70,7 +70,7 @@ IsEnabled="{Binding SelectedAgent, Converter={x:Static ObjectConverters.IsNotNull}}"> - + @@ -80,23 +80,23 @@ IsEnabled="{Binding SelectedAgent, Converter={x:Static ObjectConverters.IsNotNull}}"> - + - - - + + + @@ -128,29 +128,29 @@ - + - + - + - + - + @@ -158,13 +158,13 @@ - + - + @@ -173,38 +173,38 @@ - - + + - + - + - + - + - + - + @@ -225,13 +225,12 @@ + Text="{Binding AllowedToolsHeader}" /> - - + + @@ -258,7 +257,7 @@ - + @@ -267,7 +266,7 @@ IsEnabled="{Binding ToolSettings, Converter={x:Static ObjectConverters.IsNotNull}}"> - + diff --git a/src/ClawdDotNet.Desktop/Views/BackupPageView.axaml b/src/ClawdDotNet.Desktop/Views/BackupPageView.axaml index a671a3d..4ded51f 100644 --- a/src/ClawdDotNet.Desktop/Views/BackupPageView.axaml +++ b/src/ClawdDotNet.Desktop/Views/BackupPageView.axaml @@ -18,41 +18,41 @@ - - + + - + + PlaceholderText="{Binding L[desktop.backup.targetFolderPlaceholder]}" /> - @@ -69,36 +69,36 @@ - - + + - + - + - + + Text="{Binding L[desktop.backup.autoHint]}" /> - + - + - @@ -47,11 +47,11 @@ - - + - + - + - + - + @@ -81,8 +81,8 @@ - - + + - - - - + + + + diff --git a/src/ClawdDotNet.Desktop/Views/InstancePickerWindow.axaml b/src/ClawdDotNet.Desktop/Views/InstancePickerWindow.axaml index c382a88..5768ab3 100644 --- a/src/ClawdDotNet.Desktop/Views/InstancePickerWindow.axaml +++ b/src/ClawdDotNet.Desktop/Views/InstancePickerWindow.axaml @@ -36,12 +36,12 @@ + PlaceholderText="{Binding L[desktop.instancePicker.newInstancePlaceholder]}" /> @@ -50,7 +50,7 @@ - - diff --git a/src/ClawdDotNet.Desktop/Views/MainWindow.axaml b/src/ClawdDotNet.Desktop/Views/MainWindow.axaml index 6576eb9..8ed1080 100644 --- a/src/ClawdDotNet.Desktop/Views/MainWindow.axaml +++ b/src/ClawdDotNet.Desktop/Views/MainWindow.axaml @@ -33,7 +33,7 @@ @@ -190,7 +190,7 @@ - + @@ -200,7 +200,7 @@ - + @@ -16,8 +16,8 @@ - @@ -30,39 +30,39 @@ - + - + - + + Content="{Binding L[desktop.settings.autostart]}" /> + Text="{Binding L[desktop.settings.autostartHint]}" /> - + + Text="{Binding L[desktop.settings.logDirHint]}" /> - + - + @@ -72,21 +72,33 @@ - + - + + + + + + + + + + + + - + - + @@ -96,26 +108,26 @@ - + - + - + - + - + @@ -123,14 +135,14 @@ - + - - diff --git a/src/ClawdDotNet.Desktop/Views/TasksPageView.axaml b/src/ClawdDotNet.Desktop/Views/TasksPageView.axaml index 0549ee0..9ae3046 100644 --- a/src/ClawdDotNet.Desktop/Views/TasksPageView.axaml +++ b/src/ClawdDotNet.Desktop/Views/TasksPageView.axaml @@ -16,13 +16,13 @@ Bildpunkt, damit zwischen ihnen nur eine Linie steht und nicht zwei. --> - - - @@ -40,7 +40,7 @@ @@ -48,7 +48,7 @@ IsEnabled="{Binding SelectedJob, Converter={x:Static ObjectConverters.IsNotNull}}"> - + @@ -56,7 +56,7 @@ IsEnabled="{Binding SelectedJob, Converter={x:Static ObjectConverters.IsNotNull}}"> - + @@ -64,7 +64,7 @@ IsEnabled="{Binding SelectedJob, Converter={x:Static ObjectConverters.IsNotNull}}"> - + @@ -74,27 +74,27 @@ IsEnabled="{Binding SelectedJob, Converter={x:Static ObjectConverters.IsNotNull}}"> - + - - - - - - - + + + + + + + @@ -131,7 +131,7 @@ @@ -139,7 +139,7 @@ IsEnabled="{Binding SelectedService, Converter={x:Static ObjectConverters.IsNotNull}}"> - + @@ -152,25 +152,25 @@ IsEnabled="{Binding SelectedService, Converter={x:Static ObjectConverters.IsNotNull}}"> - + - - - - - + + + + + @@ -206,19 +206,19 @@ - - - - - - + + + + + + diff --git a/src/ClawdDotNet.Desktop/Views/TextEditorWindow.axaml b/src/ClawdDotNet.Desktop/Views/TextEditorWindow.axaml index 0087c13..c97b157 100644 --- a/src/ClawdDotNet.Desktop/Views/TextEditorWindow.axaml +++ b/src/ClawdDotNet.Desktop/Views/TextEditorWindow.axaml @@ -17,12 +17,12 @@ + Text="Edit Text" Margin="0,0,0,12" TextWrapping="Wrap" /> - @@ -46,7 +46,7 @@ - + @@ -78,15 +78,15 @@ - + - - - - - + + + + + diff --git a/src/ClawdDotNet.Desktop/Views/ToolSettingsView.axaml b/src/ClawdDotNet.Desktop/Views/ToolSettingsView.axaml index 89b8a97..f05bd54 100644 --- a/src/ClawdDotNet.Desktop/Views/ToolSettingsView.axaml +++ b/src/ClawdDotNet.Desktop/Views/ToolSettingsView.axaml @@ -23,25 +23,25 @@ - + - + - + - + - + @@ -51,36 +51,36 @@ Converter={x:Static conv:ToolMatchConverter.Instance}, ConverterParameter=Mail}"> - + - + - + - + - + - + - + @@ -90,21 +90,21 @@ - + - + - + - + @@ -113,15 +113,15 @@ - + - + - + @@ -131,20 +131,20 @@ Converter={x:Static conv:ToolMatchConverter.Instance}, ConverterParameter=DirectAPI}"> - + - + - + - + @@ -153,17 +153,17 @@ - + - + - + @@ -173,34 +173,34 @@ - + - + - + - + - + - + - + @@ -210,28 +210,28 @@ - + - + - + - + diff --git a/src/ClawdDotNet.Desktop/i18n/de.json b/src/ClawdDotNet.Desktop/i18n/de.json new file mode 100644 index 0000000..10e6a7c --- /dev/null +++ b/src/ClawdDotNet.Desktop/i18n/de.json @@ -0,0 +1,339 @@ +{ + "desktop.mainWindow.title": "ClawdDotNet", + "desktop.mainWindow.sidebarTooltip": "Seitenleiste ein-/ausklappen", + "desktop.mainWindow.switchInstance": "Instanz wechseln", + "desktop.mainWindow.instance": "INSTANZ", + "desktop.mainWindow.themeLight": "Hellmodus", + "desktop.mainWindow.themeDark": "Dunkelmodus", + "desktop.mainWindow.themeToggle": "Farbschema umschalten", + "desktop.mainWindow.openInstanceFolder": "Instanz-Ordner öffnen", + "desktop.mainWindow.instanceManager": "Instanzmanager", + "desktop.mainWindow.exit": "Beenden", + "desktop.mainWindow.instanceLabel": "Instanz: {0}", + "desktop.mainWindow.apiStatus": "API {0}", + "desktop.mainWindow.credits": "Credits: ${0:F2} von ${1:F2}", + "desktop.mainWindow.creditsNone": "Credits: —", + "desktop.mainWindow.dismiss": "Ausblenden", + + "desktop.nav.work": "Arbeiten", + "desktop.nav.analytics": "Analyse", + "desktop.nav.system": "System", + + "desktop.status.unknown": "unbekannt", + "desktop.status.design": "Entwurf", + "desktop.status.noApiKey": "kein API-Key", + "desktop.status.noUsageData": "keine Nutzungsdaten", + "desktop.status.ready": "Bereit", + "desktop.status.reachable": "OK", + "desktop.status.unreachable": "nicht erreichbar", + + "desktop.chat.agent": "Agent:", + "desktop.chat.clearHistory": "Verlauf leeren", + "desktop.chat.abort": "Abbrechen", + "desktop.chat.inputPlaceholder": "Nachricht eingeben… (Enter zum Senden, Shift+Enter für neue Zeile)", + "desktop.chat.send": "Senden", + "desktop.chat.statusReady": "Bereit", + "desktop.chat.statusSending": "Sende…", + "desktop.chat.statusDesign": "Entwurfsmodus", + "desktop.chat.noApiKey": "OpenRouter API-Key ist nicht konfiguriert. Bitte in den Einstellungen hinterlegen.", + "desktop.chat.noAgent": "Bitte einen Agenten auswählen.", + "desktop.chat.emptyMessage": "Bitte eine Nachricht eingeben.", + + "desktop.agents.addAgent": "Agent", + "desktop.agents.removeAgent": "Agent", + "desktop.agents.runNow": "Jetzt ausf.", + "desktop.agents.identity": "Identity", + "desktop.agents.soul": "Soul", + "desktop.agents.folder": "Ordner", + "desktop.agents.save": "Speichern", + "desktop.agents.thDisplayName": "ANZEIGENAME", + "desktop.agents.thModel": "MODELL", + "desktop.agents.thSummary": "KURZINFO", + "desktop.agents.kickerGeneralModel": "ALLGEMEIN & MODELL", + "desktop.agents.agentId": "Agent-ID", + "desktop.agents.displayName": "Anzeigename", + "desktop.agents.model": "Modell", + "desktop.agents.promptCaching": "Prompt Caching", + "desktop.agents.roleDescription": "Rolle / Beschreibung", + "desktop.agents.systemPrompt": "System-Prompt (zusätzliche Anweisungen)", + "desktop.agents.kickerLimits": "GRENZEN", + "desktop.agents.limitsTitle": "Schutzgrenzen (LoopGuard & Budget)", + "desktop.agents.maxSteps": "Max. Schritte pro Run", + "desktop.agents.timeoutSeconds": "Timeout (Sekunden)", + "desktop.agents.maxContextTokens": "Max. Kontext-Tokens", + "desktop.agents.maxToolResultChars": "Max. Tool-Ergebnis (Zeichen)", + "desktop.agents.dailyCost": "Tagesbudget ($ USD)", + "desktop.agents.dailyTokens": "Tagesbudget (Tokens)", + "desktop.agents.allowedToolsFor": "ZUGELASSENE WERKZEUGE FÜR {0}", + "desktop.agents.thTool": "WERKZEUG", + "desktop.agents.thDescription": "BESCHREIBUNG", + "desktop.agents.toolSettingsKicker": "WERKZEUG-EINSTELLUNGEN", + "desktop.agents.apply": "Übernehmen", + "desktop.agents.saved": "Agenten erfolgreich gespeichert.", + + "desktop.tasks.tabJobs": "Geplante Jobs", + "desktop.tasks.tabServices": "Dienste", + "desktop.tasks.tabHistory": "Verlauf", + "desktop.tasks.addJob": "Job hinzufügen", + "desktop.tasks.editJob": "Bearbeiten", + "desktop.tasks.removeJob": "Entfernen", + "desktop.tasks.toggleJob": "Aktivieren/Deaktivieren", + "desktop.tasks.runJobNow": "Jetzt ausführen", + "desktop.tasks.refresh": "Aktualisieren", + "desktop.tasks.thType": "TYP", + "desktop.tasks.thAgent": "AGENT", + "desktop.tasks.thTask": "TOOL / NACHRICHT", + "desktop.tasks.thCron": "CRON", + "desktop.tasks.thNextRun": "NÄCHSTER LAUF", + "desktop.tasks.thLastRun": "LETZTER LAUF", + "desktop.tasks.thStatus": "STATUS", + + "desktop.services.addService": "Dienst hinzufügen", + "desktop.services.removeService": "Entfernen", + "desktop.services.toggleService": "Aktivieren/Deaktivieren", + "desktop.services.thName": "DIENSTNAME", + "desktop.services.thType": "TYP", + "desktop.services.thPort": "PORT", + "desktop.services.thStatus": "STATUS", + "desktop.services.thDescription": "BESCHREIBUNG", + + "desktop.history.refresh": "Verlauf aktualisieren", + "desktop.history.thTime": "ZEITPUNKT", + "desktop.history.thJob": "JOB", + "desktop.history.thAgent": "AGENT", + "desktop.history.thDescription": "BESCHREIBUNG", + "desktop.history.thStatus": "STATUS", + "desktop.history.thDetails": "DETAILS", + + "desktop.usage.kickerTokens7d": "TOKENS (7 TAGE)", + "desktop.usage.kickerCost7d": "KOSTEN (7 TAGE)", + "desktop.usage.kickerRuns7d": "RUNS (7 TAGE)", + "desktop.usage.refresh": "Aktualisieren", + "desktop.usage.tokensPerDay": "Tokens pro Tag", + "desktop.usage.breakdownTitle": "Aufschlüsselung nach Agent & Modell", + "desktop.usage.thAgent": "AGENT", + "desktop.usage.thModel": "MODELL", + "desktop.usage.thTokens": "TOKENS", + "desktop.usage.thCost": "KOSTEN", + "desktop.usage.thRuns": "RUNS", + "desktop.usage.empty": "Noch keine Tokennutzung aufgezeichnet.", + + "desktop.agentChats.agentA": "Agent A:", + "desktop.agentChats.agentB": "Agent B:", + "desktop.agentChats.empty": "Noch keine Nachrichten zwischen diesen Agenten aufgezeichnet.", + "desktop.agentChats.needTwo": "Für einen Verlauf braucht es mindestens zwei Agenten in dieser Instanz.", + + "desktop.backup.kickerManual": "MANUELL", + "desktop.backup.titleCreate": "Sicherung erstellen", + "desktop.backup.targetFolder": "Zielordner", + "desktop.backup.targetFolderPlaceholder": "Zielordner für Sicherungen…", + "desktop.backup.browse": "Durchsuchen…", + "desktop.backup.credentials": "Zugangsdaten", + "desktop.backup.credentialsExclude": "nicht mitsichern", + "desktop.backup.credentialsProtect": "mit Passphrase schützen", + "desktop.backup.passphrase": "Passphrase eingeben", + "desktop.backup.passphraseRepeat": "Passphrase wiederholen", + "desktop.backup.scope": "Umfang", + "desktop.backup.scopeChat": "Chatverläufe", + "desktop.backup.scopeLogs": "Protokolle", + "desktop.backup.createNow": "Sicherung jetzt erstellen", + "desktop.backup.kickerAuto": "AUTOMATIK", + "desktop.backup.titleAuto": "Automatische Sicherung", + "desktop.backup.autoDaily": "täglich sichern um", + "desktop.backup.time": "Uhrzeit", + "desktop.backup.keep": "Aufbewahren", + "desktop.backup.autoHint": "Die Automatik sichert ohne Zugangsdaten — eine gespeicherte Passphrase wäre wirkungslos.", + "desktop.backup.kickerExisting": "BESTAND", + "desktop.backup.titleExisting": "Vorhandene Sicherungen", + "desktop.backup.refresh": "Aktualisieren", + "desktop.backup.openFolder": "Ordner öffnen", + "desktop.backup.delete": "Löschen", + "desktop.backup.restore": "Wiederherstellen…", + "desktop.backup.thFilename": "DATEINAME", + "desktop.backup.thCreatedAt": "ERSTELLT AM", + "desktop.backup.thInstance": "INSTANZ", + "desktop.backup.thSize": "GRÖSSE", + "desktop.backup.thSecrets": "ZUGANGSDATEN", + + "desktop.logs.module": "Modul:", + "desktop.logs.level": "Stufe:", + "desktop.logs.refresh": "Aktualisieren", + "desktop.logs.clear": "Leeren", + "desktop.logs.openFolder": "Ordner öffnen", + "desktop.logs.follow": "Mitlaufen", + + "desktop.settings.discard": "Verwerfen", + "desktop.settings.save": "Speichern", + "desktop.settings.instance": "INSTANZ", + "desktop.settings.instanceName": "Instanz-Name", + "desktop.settings.apiKey": "OpenRouter API-Key", + "desktop.settings.autostart": "Autostart durch Manager", + "desktop.settings.autostartHint": "Der Instanzmanager startet diese Instanz, wenn er hochkommt, und wieder, wenn sie abstürzt. Nach mehreren Neustarts in kurzer Folge setzt er aus und meldet es.", + "desktop.settings.logDir": "Protokoll-Verzeichnis", + "desktop.settings.logDirHint": "Gilt nur für diese Instanz. Leer heißt: Unterordner Logs im Instanzverzeichnis.", + "desktop.settings.dailyCost": "Tagesbudget ($ USD)", + "desktop.settings.dailyTokens": "Tagesbudget (Tokens)", + "desktop.settings.general": "ALLGEMEIN", + "desktop.settings.instancesDir": "Instanzen-Verzeichnis", + "desktop.settings.minLogLevel": "Minimaler Log-Level", + "desktop.settings.openRouterUrl": "OpenRouter Base-URL", + "desktop.settings.language": "Sprache", + "desktop.settings.dcAndLicense": "DEPLOYMENTCENTER & LIZENZ", + "desktop.settings.dcUrl": "Server-URL", + "desktop.settings.dcToken": "Master-Token", + "desktop.settings.dcEnv": "Umgebung", + "desktop.settings.updateChannel": "Update-Kanal", + "desktop.settings.licenseKey": "Lizenzschlüssel", + "desktop.settings.errorReporting": "Fehler automatisch an Deploymentcenter melden", + "desktop.settings.updateCheck": "Beim Start automatisch auf Updates prüfen", + "desktop.settings.saved": "Einstellungen und Instanz-Konfiguration gespeichert.", + "desktop.settings.savedLogRestart": "Gespeichert. Das Protokoll-Verzeichnis gilt ab dem nächsten Start.", + + "desktop.info.kickerApp": "ANWENDUNG", + "desktop.info.build": "Build", + "desktop.info.version": "Version", + "desktop.info.buildSummary": "Build-Zusammenfassung", + "desktop.info.kickerInstance": "INSTANZ", + "desktop.info.currentInstance": "Aktuelle Instanz", + "desktop.info.openFolder": "Instanz-Ordner öffnen", + "desktop.info.name": "Name", + "desktop.info.id": "ID", + "desktop.info.path": "Pfad", + "desktop.info.logDir": "Log-Ordner", + "desktop.info.openRouter": "OpenRouter", + "desktop.info.kickerModules": "MODULE", + "desktop.info.assemblies": "Baugruppen", + "desktop.info.thModule": "MODUL", + "desktop.info.thBuild": "BUILD", + "desktop.info.thBuildDate": "BUILD-DATUM", + "desktop.info.thChanges": "ÄNDERUNGEN", + + "desktop.dialog.addAgent": "Neuen Agenten anlegen", + "desktop.dialog.agentId": "Agent-ID (eindeutiger Schlüssel):", + "desktop.dialog.agentDisplayName": "Anzeigename:", + "desktop.dialog.defaultModel": "Standard-Modell:", + "desktop.dialog.roleDescription": "Beschreibung / Rolle:", + "desktop.dialog.addJobTitle": "Job hinzufügen / bearbeiten", + "desktop.dialog.jobType": "Job-Typ:", + "desktop.dialog.agentWakeup": "Agent Wakeup (Periodische Ausführung)", + "desktop.dialog.toolJob": "Tool Job (Werkzeug-spezifischer Job)", + "desktop.dialog.jobTypeId": "Job-Typ-ID / Name:", + "desktop.dialog.taskMessage": "Aufgabenbeschreibung / Nachricht:", + "desktop.dialog.cronExpression": "Cron-Ausdruck:", + "desktop.dialog.cronHint": "Syntax: Min Std Tag Mon Wochentag (z.B. '*/15 * * * *' alle 15 Minuten)", + "desktop.dialog.runOnStart": "Beim Anwendungsstart einmalig sofort ausführen", + "desktop.dialog.addServiceTitle": "Dienst (Service) hinzufügen", + "desktop.dialog.serviceName": "Name des Dienstes:", + "desktop.dialog.serviceType": "Typ:", + "desktop.dialog.servicePort": "Port:", + "desktop.dialog.serviceDesc": "Beschreibung:", + "desktop.dialog.restoreTitle": "Sicherung wiederherstellen", + "desktop.dialog.backupInfo": "Sicherungsinformationen", + "desktop.dialog.backupInstance": "Instanz: {0}", + "desktop.dialog.backupCreated": "Erstellt: {0:g}", + "desktop.dialog.backupFiles": "Dateien: {0}", + "desktop.dialog.backupSecrets": "Zugangsdaten: {0}", + "desktop.dialog.targetDir": "Zielordner:", + "desktop.dialog.chooseFolder": "Ordner wählen", + "desktop.dialog.restoreHint": "Hinweis: Es wird empfohlen, in einen neuen Ordner wiederherzustellen, um Konflikte mit laufenden Prozessen zu vermeiden.", + "desktop.dialog.passphrase": "Passphrase:", + "desktop.dialog.passphraseProtected": "Diese Sicherung enthält geschützte Zugangsdaten.", + "desktop.dialog.overwriteExisting": "Vorhandene Dateien im Zielordner überschreiben", + "desktop.dialog.licenseTitle": "ClawdDotNet – Lizenz", + "desktop.dialog.licenseKey": "Lizenzschlüssel", + "desktop.dialog.hardwareId": "Hardware-ID (für den Support):", + "desktop.dialog.verify": "Prüfen", + "desktop.dialog.textEditorTitle": "Text bearbeiten", + + "desktop.instancePicker.titleSwitch": "Instanz wechseln", + "desktop.instancePicker.titleSelect": "Instanz wählen", + "desktop.instancePicker.hintSwitch": "Jede Instanz läuft unabhängig mit eigenen Agenten und Daten. Wechseln startet ClawdDotNet mit der gewählten Instanz neu.", + "desktop.instancePicker.hintSelect": "Jede Instanz läuft unabhängig mit eigenen Agenten und Daten.", + "desktop.instancePicker.confirmSwitch": "Wechseln", + "desktop.instancePicker.confirmStart": "Starten", + "desktop.instancePicker.newInstancePlaceholder": "Name der neuen Instanz", + "desktop.instancePicker.create": "Anlegen", + "desktop.instancePicker.active": "Aktiv", + "desktop.instancePicker.dirLabel": "Verzeichnis: {0}", + "desktop.instancePicker.agentsCount": "{0} Agenten · API-Key {1}", + "desktop.instancePicker.apiKeyConfigured": "konfiguriert", + "desktop.instancePicker.apiKeyMissing": "fehlt", + "desktop.instancePicker.tooltipConfigured": "API-Key hinterlegt", + "desktop.instancePicker.tooltipMissing": "Kein API-Key — Agenten bleiben deaktiviert", + "desktop.instancePicker.nameRequired": "Bitte einen Instanznamen eingeben.", + "desktop.instancePicker.createFailed": "Instanz konnte nicht angelegt werden: {0}", + + "desktop.startMode.title": "ClawdDotNet – Startmodus wählen", + "desktop.startMode.question": "Wie möchtest du ClawdDotNet starten?", + "desktop.startMode.managerBtn": "Instanzmanager starten (Empfohlen)", + "desktop.startMode.standaloneBtn": "Als Standalone-Instanz starten", + "desktop.startMode.explanation": "• Instanzmanager (Empfohlen):\n Startet den zentralen Hintergrund-Manager und öffnet die Verwaltung im Browser. Ermöglicht den stabilen Dauerbetrieb mehrerer Instanzen parallel ohne Absturzrisiko.\n\n• Standalone-Modus:\n Startet genau eine Instanz direkt als einzelnes Desktop-Fenster.", + + "desktop.standaloneSwitch.title": "ClawdDotNet – Instanzwechsel (Standalone)", + "desktop.standaloneSwitch.warning": "Achtung: Standalone-Modus", + "desktop.standaloneSwitch.text": "Diese Instanz läuft eigenständig ohne Instanzmanager.\n\nWenn du die Instanz hier wechselst, wird diese Instanz vollständig beendet und alle laufenden Agenten und Hintergrunddienste werden gestoppt.\n\nEmpfehlung: Starte stattdessen den Instanzmanager, um mehrere Instanzen gleichzeitig und unabhängig voneinander zu betreiben.", + "desktop.standaloneSwitch.switchAnyway": "Instanz trotzdem wechseln", + + "desktop.toolSettings.noSettings": "Dieses Werkzeug benötigt keine weiteren Einstellungen.", + "desktop.toolSettings.host": "Host", + "desktop.toolSettings.port": "Port", + "desktop.toolSettings.username": "Benutzername", + "desktop.toolSettings.password": "Passwort", + "desktop.toolSettings.rootPath": "Root-Pfad", + "desktop.toolSettings.imapHost": "IMAP-Host", + "desktop.toolSettings.imapPort": "IMAP-Port", + "desktop.toolSettings.smtpHost": "SMTP-Host", + "desktop.toolSettings.smtpPort": "SMTP-Port", + "desktop.toolSettings.allowedRecipients": "Erlaubte Empfänger", + "desktop.toolSettings.type": "Typ", + "desktop.toolSettings.connectionString": "Connection-String", + "desktop.toolSettings.accessLevel": "Zugriffsebene", + "desktop.toolSettings.allowedTables": "Erlaubte Tabellen", + "desktop.toolSettings.botToken": "Bot-Token", + "desktop.toolSettings.defaultChatId": "Standard Chat-ID", + "desktop.toolSettings.allowedChatIds": "Erlaubte Chat-IDs", + "desktop.toolSettings.defaultProvider": "Standard-Provider", + "desktop.toolSettings.cacheTtl": "Cache TTL (Sekunden)", + "desktop.toolSettings.twelveDataKey": "Twelve Data Key", + "desktop.toolSettings.alphaVantageKey": "Alpha Vantage Key", + "desktop.toolSettings.allowedDomains": "Erlaubte Domains", + "desktop.toolSettings.maxResponseKb": "Max. Antwortgröße (KB)", + "desktop.toolSettings.userAgent": "User Agent", + "desktop.toolSettings.xApiKey": "X (Twitter) API-Key", + "desktop.toolSettings.xAccounts": "X Accounts", + "desktop.toolSettings.subreddits": "Subreddits", + "desktop.toolSettings.redditPostLimit": "Reddit Post Limit", + "desktop.toolSettings.openRouterKey": "OpenRouter Key", + "desktop.toolSettings.sttModel": "STT-Modell", + "desktop.toolSettings.youtubeChannels": "YouTube-Kanäle", + "desktop.toolSettings.fileRwPersonalExt": "Erlaubte Dateiendungen (persönlich)", + "desktop.toolSettings.fileRwSharedAccess": "Zugriffsebene (geteilt)", + "desktop.toolSettings.fileRwSharedExt": "Erlaubte Dateiendungen (geteilt)", + "desktop.toolSettings.fileRwProtectedPaths": "Geschützte Pfade", + + "nav.chat": "Chat", + "nav.agents": "Agenten", + "nav.tasks": "Aufgaben", + "nav.usage": "Tokennutzung", + "nav.agentChats": "Agenten-Chats", + "nav.backup": "Sicherung", + "nav.logs": "Protokoll", + "nav.settings": "Einstellungen", + "nav.info": "Info", + + "common.all": "Alle", + "common.save": "Speichern", + "common.cancel": "Abbrechen", + "common.delete": "Löschen", + "common.edit": "Bearbeiten", + "common.refresh": "Aktualisieren", + "common.close": "Schließen", + "common.discard": "Verwerfen", + "common.ready": "Bereit", + "common.loading": "Lade…", + "common.create": "Anlegen", + "common.add": "Hinzufügen", + "common.apply": "Übernehmen", + "common.ok": "OK", + "common.exit": "Beenden" +} diff --git a/src/ClawdDotNet.Desktop/i18n/en.json b/src/ClawdDotNet.Desktop/i18n/en.json new file mode 100644 index 0000000..a24da00 --- /dev/null +++ b/src/ClawdDotNet.Desktop/i18n/en.json @@ -0,0 +1,339 @@ +{ + "desktop.mainWindow.title": "ClawdDotNet", + "desktop.mainWindow.sidebarTooltip": "Toggle sidebar", + "desktop.mainWindow.switchInstance": "Switch instance", + "desktop.mainWindow.instance": "INSTANCE", + "desktop.mainWindow.themeLight": "Light Mode", + "desktop.mainWindow.themeDark": "Dark Mode", + "desktop.mainWindow.themeToggle": "Toggle theme", + "desktop.mainWindow.openInstanceFolder": "Open Instance Folder", + "desktop.mainWindow.instanceManager": "Instance Manager", + "desktop.mainWindow.exit": "Exit", + "desktop.mainWindow.instanceLabel": "Instance: {0}", + "desktop.mainWindow.apiStatus": "API {0}", + "desktop.mainWindow.credits": "Credits: ${0:F2} of ${1:F2}", + "desktop.mainWindow.creditsNone": "Credits: —", + "desktop.mainWindow.dismiss": "Dismiss", + + "desktop.nav.work": "Work", + "desktop.nav.analytics": "Analytics", + "desktop.nav.system": "System", + + "desktop.status.unknown": "unknown", + "desktop.status.design": "Design", + "desktop.status.noApiKey": "no API key", + "desktop.status.noUsageData": "no usage data", + "desktop.status.ready": "Ready", + "desktop.status.reachable": "OK", + "desktop.status.unreachable": "unreachable", + + "desktop.chat.agent": "Agent:", + "desktop.chat.clearHistory": "Clear History", + "desktop.chat.abort": "Abort", + "desktop.chat.inputPlaceholder": "Type message… (Enter to send, Shift+Enter for new line)", + "desktop.chat.send": "Send", + "desktop.chat.statusReady": "Ready", + "desktop.chat.statusSending": "Sending…", + "desktop.chat.statusDesign": "Design Mode", + "desktop.chat.noApiKey": "OpenRouter API key is not configured. Please enter the key in Settings.", + "desktop.chat.noAgent": "Please select an agent.", + "desktop.chat.emptyMessage": "Please enter a message.", + + "desktop.agents.addAgent": "Agent", + "desktop.agents.removeAgent": "Agent", + "desktop.agents.runNow": "Run Now", + "desktop.agents.identity": "Identity", + "desktop.agents.soul": "Soul", + "desktop.agents.folder": "Folder", + "desktop.agents.save": "Save", + "desktop.agents.thDisplayName": "DISPLAY NAME", + "desktop.agents.thModel": "MODEL", + "desktop.agents.thSummary": "SUMMARY", + "desktop.agents.kickerGeneralModel": "GENERAL & MODEL", + "desktop.agents.agentId": "Agent ID", + "desktop.agents.displayName": "Display Name", + "desktop.agents.model": "Model", + "desktop.agents.promptCaching": "Prompt Caching", + "desktop.agents.roleDescription": "Role / Description", + "desktop.agents.systemPrompt": "System Prompt (additional instructions)", + "desktop.agents.kickerLimits": "LIMITS", + "desktop.agents.limitsTitle": "Guardrails (LoopGuard & Budget)", + "desktop.agents.maxSteps": "Max Steps per Run", + "desktop.agents.timeoutSeconds": "Timeout (seconds)", + "desktop.agents.maxContextTokens": "Max Context Tokens", + "desktop.agents.maxToolResultChars": "Max Tool Result (chars)", + "desktop.agents.dailyCost": "Daily Budget ($ USD)", + "desktop.agents.dailyTokens": "Daily Budget (Tokens)", + "desktop.agents.allowedToolsFor": "ALLOWED TOOLS FOR {0}", + "desktop.agents.thTool": "TOOL", + "desktop.agents.thDescription": "DESCRIPTION", + "desktop.agents.toolSettingsKicker": "TOOL SETTINGS", + "desktop.agents.apply": "Apply", + "desktop.agents.saved": "Agents saved successfully.", + + "desktop.tasks.tabJobs": "Scheduled Jobs", + "desktop.tasks.tabServices": "Services", + "desktop.tasks.tabHistory": "History", + "desktop.tasks.addJob": "Add Job", + "desktop.tasks.editJob": "Edit", + "desktop.tasks.removeJob": "Remove", + "desktop.tasks.toggleJob": "Enable/Disable", + "desktop.tasks.runJobNow": "Run Now", + "desktop.tasks.refresh": "Refresh", + "desktop.tasks.thType": "TYPE", + "desktop.tasks.thAgent": "AGENT", + "desktop.tasks.thTask": "TOOL / MESSAGE", + "desktop.tasks.thCron": "CRON", + "desktop.tasks.thNextRun": "NEXT RUN", + "desktop.tasks.thLastRun": "LAST RUN", + "desktop.tasks.thStatus": "STATUS", + + "desktop.services.addService": "Add Service", + "desktop.services.removeService": "Remove", + "desktop.services.toggleService": "Enable/Disable", + "desktop.services.thName": "SERVICE NAME", + "desktop.services.thType": "TYPE", + "desktop.services.thPort": "PORT", + "desktop.services.thStatus": "STATUS", + "desktop.services.thDescription": "DESCRIPTION", + + "desktop.history.refresh": "Refresh History", + "desktop.history.thTime": "TIMESTAMP", + "desktop.history.thJob": "JOB", + "desktop.history.thAgent": "AGENT", + "desktop.history.thDescription": "DESCRIPTION", + "desktop.history.thStatus": "STATUS", + "desktop.history.thDetails": "DETAILS", + + "desktop.usage.kickerTokens7d": "TOKENS (7 DAYS)", + "desktop.usage.kickerCost7d": "COST (7 DAYS)", + "desktop.usage.kickerRuns7d": "RUNS (7 DAYS)", + "desktop.usage.refresh": "Refresh", + "desktop.usage.tokensPerDay": "Tokens per Day", + "desktop.usage.breakdownTitle": "Breakdown by Agent & Model", + "desktop.usage.thAgent": "AGENT", + "desktop.usage.thModel": "MODEL", + "desktop.usage.thTokens": "TOKENS", + "desktop.usage.thCost": "COST", + "desktop.usage.thRuns": "RUNS", + "desktop.usage.empty": "No token usage recorded yet.", + + "desktop.agentChats.agentA": "Agent A:", + "desktop.agentChats.agentB": "Agent B:", + "desktop.agentChats.empty": "No messages between these agents yet.", + "desktop.agentChats.needTwo": "At least two agents are required in this instance to view a conversation history.", + + "desktop.backup.kickerManual": "MANUAL", + "desktop.backup.titleCreate": "Create Backup", + "desktop.backup.targetFolder": "Target Folder", + "desktop.backup.targetFolderPlaceholder": "Target folder for backups…", + "desktop.backup.browse": "Browse…", + "desktop.backup.credentials": "Credentials", + "desktop.backup.credentialsExclude": "do not include", + "desktop.backup.credentialsProtect": "protect with passphrase", + "desktop.backup.passphrase": "Enter passphrase", + "desktop.backup.passphraseRepeat": "Repeat passphrase", + "desktop.backup.scope": "Scope", + "desktop.backup.scopeChat": "Chat history", + "desktop.backup.scopeLogs": "Logs", + "desktop.backup.createNow": "Create Backup Now", + "desktop.backup.kickerAuto": "AUTOMATIC", + "desktop.backup.titleAuto": "Automatic Backup", + "desktop.backup.autoDaily": "daily backup at", + "desktop.backup.time": "Time", + "desktop.backup.keep": "Keep Count", + "desktop.backup.autoHint": "Automatic backup excludes credentials — a saved passphrase would be ineffective.", + "desktop.backup.kickerExisting": "EXISTING", + "desktop.backup.titleExisting": "Existing Backups", + "desktop.backup.refresh": "Refresh", + "desktop.backup.openFolder": "Open Folder", + "desktop.backup.delete": "Delete", + "desktop.backup.restore": "Restore…", + "desktop.backup.thFilename": "FILENAME", + "desktop.backup.thCreatedAt": "CREATED AT", + "desktop.backup.thInstance": "INSTANCE", + "desktop.backup.thSize": "SIZE", + "desktop.backup.thSecrets": "CREDENTIALS", + + "desktop.logs.module": "Module:", + "desktop.logs.level": "Level:", + "desktop.logs.refresh": "Refresh", + "desktop.logs.clear": "Clear", + "desktop.logs.openFolder": "Open Folder", + "desktop.logs.follow": "Follow Tail", + + "desktop.settings.discard": "Discard", + "desktop.settings.save": "Save", + "desktop.settings.instance": "INSTANCE", + "desktop.settings.instanceName": "Instance Name", + "desktop.settings.apiKey": "OpenRouter API Key", + "desktop.settings.autostart": "Autostart by Manager", + "desktop.settings.autostartHint": "The instance manager starts this instance on launch and restarts it if it crashes.", + "desktop.settings.logDir": "Log Directory", + "desktop.settings.logDirHint": "Applies only to this instance. Empty means: Logs subfolder in instance directory.", + "desktop.settings.dailyCost": "Daily Budget ($ USD)", + "desktop.settings.dailyTokens": "Daily Budget (Tokens)", + "desktop.settings.general": "GENERAL", + "desktop.settings.instancesDir": "Instances Directory", + "desktop.settings.minLogLevel": "Minimum Log Level", + "desktop.settings.openRouterUrl": "OpenRouter Base URL", + "desktop.settings.language": "Language", + "desktop.settings.dcAndLicense": "DEPLOYMENTCENTER & LICENSE", + "desktop.settings.dcUrl": "Server URL", + "desktop.settings.dcToken": "Master Token", + "desktop.settings.dcEnv": "Environment", + "desktop.settings.updateChannel": "Update Channel", + "desktop.settings.licenseKey": "License Key", + "desktop.settings.errorReporting": "Automatically report errors to Deploymentcenter", + "desktop.settings.updateCheck": "Check for updates automatically on startup", + "desktop.settings.saved": "Settings and instance configuration saved successfully.", + "desktop.settings.savedLogRestart": "Saved. The log directory will take effect upon next restart.", + + "desktop.info.kickerApp": "APPLICATION", + "desktop.info.build": "Build", + "desktop.info.version": "Version", + "desktop.info.buildSummary": "Build Summary", + "desktop.info.kickerInstance": "INSTANCE", + "desktop.info.currentInstance": "Current Instance", + "desktop.info.openFolder": "Open Instance Folder", + "desktop.info.name": "Name", + "desktop.info.id": "ID", + "desktop.info.path": "Path", + "desktop.info.logDir": "Log Directory", + "desktop.info.openRouter": "OpenRouter", + "desktop.info.kickerModules": "MODULES", + "desktop.info.assemblies": "Assemblies", + "desktop.info.thModule": "MODULE", + "desktop.info.thBuild": "BUILD", + "desktop.info.thBuildDate": "BUILD DATE", + "desktop.info.thChanges": "CHANGES", + + "desktop.dialog.addAgent": "Create New Agent", + "desktop.dialog.agentId": "Agent ID (unique key):", + "desktop.dialog.agentDisplayName": "Display Name:", + "desktop.dialog.defaultModel": "Default Model:", + "desktop.dialog.roleDescription": "Description / Role:", + "desktop.dialog.addJobTitle": "Add / Edit Job", + "desktop.dialog.jobType": "Job Type:", + "desktop.dialog.agentWakeup": "Agent Wakeup (Periodic Execution)", + "desktop.dialog.toolJob": "Tool Job (Tool-specific Job)", + "desktop.dialog.jobTypeId": "Job Type ID / Name:", + "desktop.dialog.taskMessage": "Task Description / Message:", + "desktop.dialog.cronExpression": "Cron Expression:", + "desktop.dialog.cronHint": "Syntax: Min Hour Day Month Weekday (e.g. '*/15 * * * *' every 15 minutes)", + "desktop.dialog.runOnStart": "Run once immediately on application startup", + "desktop.dialog.addServiceTitle": "Add Service", + "desktop.dialog.serviceName": "Service Name:", + "desktop.dialog.serviceType": "Type:", + "desktop.dialog.servicePort": "Port:", + "desktop.dialog.serviceDesc": "Description:", + "desktop.dialog.restoreTitle": "Restore Backup", + "desktop.dialog.backupInfo": "Backup Information", + "desktop.dialog.backupInstance": "Instance: {0}", + "desktop.dialog.backupCreated": "Created: {0:g}", + "desktop.dialog.backupFiles": "Files: {0}", + "desktop.dialog.backupSecrets": "Credentials: {0}", + "desktop.dialog.targetDir": "Target Directory:", + "desktop.dialog.chooseFolder": "Choose Folder", + "desktop.dialog.restoreHint": "Note: It is recommended to restore into a new folder to avoid conflicts with running processes.", + "desktop.dialog.passphrase": "Passphrase:", + "desktop.dialog.passphraseProtected": "This backup contains protected credentials.", + "desktop.dialog.overwriteExisting": "Overwrite existing files in target folder", + "desktop.dialog.licenseTitle": "ClawdDotNet – License", + "desktop.dialog.licenseKey": "License Key", + "desktop.dialog.hardwareId": "Hardware ID (for support):", + "desktop.dialog.verify": "Verify", + "desktop.dialog.textEditorTitle": "Edit Text", + + "desktop.instancePicker.titleSwitch": "Switch Instance", + "desktop.instancePicker.titleSelect": "Choose Instance", + "desktop.instancePicker.hintSwitch": "Each instance runs independently with its own agents and data. Switching restarts ClawdDotNet with the selected instance.", + "desktop.instancePicker.hintSelect": "Each instance runs independently with its own agents and data.", + "desktop.instancePicker.confirmSwitch": "Switch", + "desktop.instancePicker.confirmStart": "Start", + "desktop.instancePicker.newInstancePlaceholder": "Name of new instance", + "desktop.instancePicker.create": "Create", + "desktop.instancePicker.active": "Active", + "desktop.instancePicker.dirLabel": "Directory: {0}", + "desktop.instancePicker.agentsCount": "{0} Agents · API Key {1}", + "desktop.instancePicker.apiKeyConfigured": "configured", + "desktop.instancePicker.apiKeyMissing": "missing", + "desktop.instancePicker.tooltipConfigured": "API key configured", + "desktop.instancePicker.tooltipMissing": "No API key — agents remain inactive", + "desktop.instancePicker.nameRequired": "Please enter an instance name.", + "desktop.instancePicker.createFailed": "Instance could not be created: {0}", + + "desktop.startMode.title": "ClawdDotNet – Select Start Mode", + "desktop.startMode.question": "How would you like to start ClawdDotNet?", + "desktop.startMode.managerBtn": "Launch Instance Manager (Recommended)", + "desktop.startMode.standaloneBtn": "Start as Standalone Instance", + "desktop.startMode.explanation": "• Instance Manager (Recommended):\n Starts the central background manager and opens management in your browser. Enables stable parallel operation of multiple instances without crash risks.\n\n• Standalone Mode:\n Starts exactly one instance directly as a single desktop window.", + + "desktop.standaloneSwitch.title": "ClawdDotNet – Instance Switch (Standalone)", + "desktop.standaloneSwitch.warning": "Attention: Standalone Mode", + "desktop.standaloneSwitch.text": "This instance runs standalone without the Instance Manager.\n\nIf you switch instances here, this instance will be fully stopped and all running agents and background services will end.\n\nRecommendation: Launch the Instance Manager instead to run multiple instances concurrently and independently.", + "desktop.standaloneSwitch.switchAnyway": "Switch Instance Anyway", + + "desktop.toolSettings.noSettings": "This tool does not require additional settings.", + "desktop.toolSettings.host": "Host", + "desktop.toolSettings.port": "Port", + "desktop.toolSettings.username": "Username", + "desktop.toolSettings.password": "Password", + "desktop.toolSettings.rootPath": "Root Path", + "desktop.toolSettings.imapHost": "IMAP Host", + "desktop.toolSettings.imapPort": "IMAP Port", + "desktop.toolSettings.smtpHost": "SMTP Host", + "desktop.toolSettings.smtpPort": "SMTP Port", + "desktop.toolSettings.allowedRecipients": "Allowed Recipients", + "desktop.toolSettings.type": "Type", + "desktop.toolSettings.connectionString": "Connection String", + "desktop.toolSettings.accessLevel": "Access Level", + "desktop.toolSettings.allowedTables": "Allowed Tables", + "desktop.toolSettings.botToken": "Bot Token", + "desktop.toolSettings.defaultChatId": "Default Chat ID", + "desktop.toolSettings.allowedChatIds": "Allowed Chat IDs", + "desktop.toolSettings.defaultProvider": "Default Provider", + "desktop.toolSettings.cacheTtl": "Cache TTL (seconds)", + "desktop.toolSettings.twelveDataKey": "Twelve Data Key", + "desktop.toolSettings.alphaVantageKey": "Alpha Vantage Key", + "desktop.toolSettings.allowedDomains": "Allowed Domains", + "desktop.toolSettings.maxResponseKb": "Max Response Size (KB)", + "desktop.toolSettings.userAgent": "User Agent", + "desktop.toolSettings.xApiKey": "X (Twitter) API Key", + "desktop.toolSettings.xAccounts": "X Accounts", + "desktop.toolSettings.subreddits": "Subreddits", + "desktop.toolSettings.redditPostLimit": "Reddit Post Limit", + "desktop.toolSettings.openRouterKey": "OpenRouter Key", + "desktop.toolSettings.sttModel": "STT Model", + "desktop.toolSettings.youtubeChannels": "YouTube Channels", + "desktop.toolSettings.fileRwPersonalExt": "Allowed File Extensions (personal)", + "desktop.toolSettings.fileRwSharedAccess": "Access Level (shared)", + "desktop.toolSettings.fileRwSharedExt": "Allowed File Extensions (shared)", + "desktop.toolSettings.fileRwProtectedPaths": "Protected Paths", + + "nav.chat": "Chat", + "nav.agents": "Agents", + "nav.tasks": "Tasks", + "nav.usage": "Token Usage", + "nav.agentChats": "Agent Chats", + "nav.backup": "Backup", + "nav.logs": "Logs", + "nav.settings": "Settings", + "nav.info": "Info", + + "common.all": "All", + "common.save": "Save", + "common.cancel": "Cancel", + "common.delete": "Delete", + "common.edit": "Edit", + "common.refresh": "Refresh", + "common.close": "Close", + "common.discard": "Discard", + "common.ready": "Ready", + "common.loading": "Loading…", + "common.create": "Create", + "common.add": "Add", + "common.apply": "Apply", + "common.ok": "OK", + "common.exit": "Exit" +} diff --git a/src/ClawdDotNet.Manager.Web/wwwroot/app.js b/src/ClawdDotNet.Manager.Web/wwwroot/app.js index 6595cf7..5f0f203 100644 --- a/src/ClawdDotNet.Manager.Web/wwwroot/app.js +++ b/src/ClawdDotNet.Manager.Web/wwwroot/app.js @@ -30,7 +30,7 @@ import { rendereInstanzen, stoppePolling as stoppeInstanzenPolling } from "./vie import { rendereEinstellungen } from "./views/settings.js"; import { rendereSicherung } from "./views/backup.js"; import { rendereAgentenChats, trenneAgentenChatsStream } from "./views/agent-chats.js"; -import { loadLanguage, translateDocument } from "./modules/i18n.js"; +import { loadLanguage, translateDocument, setLanguage, currentLanguage, t } from "./modules/i18n.js"; import { renderUsageRuns } from "./views/usage-runs.js"; import { renderUsageTimeline } from "./views/usage-timeline.js"; import { renderRunDetail } from "./views/run-detail.js"; @@ -112,6 +112,18 @@ document.querySelectorAll(".nav-link").forEach((link) => { }); }); +// Sprache umschalten +const spracheSchalter = document.getElementById("sprache-schalter"); +spracheSchalter?.addEventListener("change", async (e) => { + await setLanguage(e.target.value); +}); + +window.addEventListener("clawd:language-changed", () => { + if (spracheSchalter) spracheSchalter.value = currentLanguage(); + const hash = window.location.hash || "#/chat"; + navigiere(hash); +}); + // Farbschema umschalten themeSchalter?.addEventListener("click", () => { wechsleTheme(); @@ -145,10 +157,10 @@ beiVerbindungsStatus((istOffen) => { if (!verbindungPunkt) return; if (istOffen) { verbindungPunkt.classList.add("offen"); - verbindungPunkt.title = "Ereignisstrom: verbunden"; + verbindungPunkt.title = t("header.eventStreamConnected"); } else { verbindungPunkt.classList.remove("offen"); - verbindungPunkt.title = "Ereignisstrom: getrennt"; + verbindungPunkt.title = t("header.eventStreamDisconnected"); } }); @@ -190,7 +202,7 @@ function aktualisiereInstanzPlakette() { kopfInstanzStatus.replaceWith(neuePlakette); kopfInstanzStatus = neuePlakette; } else { - const text = laufende.length > 1 ? `${laufende.length} laufen` : "Keine aktiv"; + const text = laufende.length > 1 ? t("header.multipleRunning", { count: laufende.length }) : t("header.noneActive"); const art = laufende.length > 1 ? "warnung" : "neutral"; const neuePlakette = erstellePlakette(text, art); neuePlakette.id = "kopf-instanz-status"; @@ -203,10 +215,10 @@ function aktualisiereInstanzPlakette() { if (aktive && aktive.zustand !== "Running") { startBtn.hidden = false; startBtn.disabled = false; - startBtn.textContent = "Starten"; + startBtn.textContent = t("header.startInstance"); startBtn.onclick = async () => { startBtn.disabled = true; - startBtn.textContent = "Startet…"; + startBtn.textContent = t("header.starting"); try { const res = await api(`/verwaltung/instanzen/${encodeURIComponent(aktive.id)}/start`, { method: "POST" @@ -223,7 +235,7 @@ function aktualisiereInstanzPlakette() { zeigeMeldung(`Start fehlgeschlagen: ${err.message}`, "gefahr"); } startBtn.disabled = false; - startBtn.textContent = "Starten"; + startBtn.textContent = t("header.startInstance"); } }; } else { @@ -266,7 +278,7 @@ async function aktualisiereInstanzenListe(vorhandene = null) { kopfInstanzSelect.replaceChildren(); const placeholderOpt = erstelleElement("option", { value: "", - text: bekannteInstanzen.length === 0 ? "Keine Instanzen" : "— Instanz wählen —" + text: bekannteInstanzen.length === 0 ? t("header.noInstances") : t("header.selectInstancePlaceholder") }); kopfInstanzSelect.append(placeholderOpt); @@ -304,9 +316,9 @@ async function aktualisiereKopfStatus() { } catch (err) { if (kopfOpenRouter) { if (err.instanzUnerreichbar) { - kopfOpenRouter.textContent = "Instanz offline (502)"; + kopfOpenRouter.textContent = t("header.instanceOffline"); } else if (err.instanzKonflikt) { - kopfOpenRouter.textContent = "Instanz wählen (409)"; + kopfOpenRouter.textContent = t("header.selectInstancePrompt"); } else { kopfOpenRouter.textContent = ""; } @@ -366,6 +378,7 @@ async function starteHauptAnwendung(instanzen) { // #661: language packs before the first render await loadLanguage(); translateDocument(); + if (spracheSchalter) spracheSchalter.value = currentLanguage(); // Router initialisieren initialisiereRouter(); diff --git a/src/ClawdDotNet.Manager.Web/wwwroot/i18n/de.json b/src/ClawdDotNet.Manager.Web/wwwroot/i18n/de.json index 2d711ff..681e8dc 100644 --- a/src/ClawdDotNet.Manager.Web/wwwroot/i18n/de.json +++ b/src/ClawdDotNet.Manager.Web/wwwroot/i18n/de.json @@ -6,6 +6,496 @@ "common.loading": "Lade…", "common.previous": "Zurück", "common.next": "Weiter", + "common.save": "Speichern", + "common.cancel": "Abbrechen", + "common.delete": "Löschen", + "common.edit": "Bearbeiten", + "common.refresh": "Aktualisieren", + "common.close": "Schließen", + "common.discard": "Verwerfen", + "common.apply": "Anwenden", + "common.reset": "Zurücksetzen", + "common.search": "Suche", + "common.actions": "Aktionen", + "common.status": "Status", + "common.name": "Name", + "common.description": "Beschreibung", + "common.ready": "Bereit", + "common.none": "Keine", + "common.unknown": "Unbekannt", + "common.all": "Alle", + "common.details": "Details", + "common.language": "Sprache", + "common.logout": "Abmelden", + "common.confirm": "Bestätigen", + "common.yes": "Ja", + "common.no": "Nein", + + "status.running": "Läuft", + "status.stopped": "Gestoppt", + "status.silent": "Verstummt", + "status.crashed": "Abgestürzt", + "status.unknown": "Unbekannt", + "status.ready": "Bereit", + "status.busy": "Beschäftigt", + "status.paused": "Pausiert", + "status.done": "Erledigt", + "status.failed": "Fehlgeschlagen", + "status.blocked": "Blockiert", + "status.inReview": "Im Review", + "status.canceled": "Verworfen", + "status.archived": "Archiviert", + + "header.instanceLabel": "Instanz:", + "header.noInstance": "(Keine Instanz)", + "header.switchInstance": "Aktive Instanz wechseln", + "header.startInstance": "Starten", + "header.startInstanceTooltip": "Ausgewählte Instanz jetzt starten", + "header.toggleTheme": "Farbschema umschalten", + "header.eventStreamConnected": "Ereignisstrom: verbunden", + "header.eventStreamDisconnected": "Ereignisstrom: getrennt", + "header.multipleRunning": "{count} laufen", + "header.noneActive": "Keine aktiv", + "header.starting": "Startet…", + "header.noInstances": "Keine Instanzen", + "header.selectInstancePlaceholder": "— Instanz wählen —", + "header.instanceOffline": "Instanz offline (502)", + "header.selectInstancePrompt": "Instanz wählen (409)", + + "nav.toggleSidebar": "Seitenleiste ein-/ausklappen", + "nav.group.work": "Arbeiten", + "nav.chat": "Chat", + "nav.agents": "Agenten", + "nav.tasks": "Aufgaben", + "nav.services": "Dienste", + "nav.group.analytics": "Analyse", + "nav.usage": "Tokennutzung", + "nav.agentChats": "Agenten-Chats", + "nav.group.system": "System", + "nav.instances": "Instanzen", + "nav.staging": "Freigaben", + "nav.logs": "Protokoll", + "nav.backup": "Sicherung", + "nav.settings": "Einstellungen", + "nav.info": "Info", + + "login.prompt": "Bitte gib das Zugangstoken des Instanzmanagers ein (webUiToken in manager.json).", + "login.tokenPlaceholder": "Zugangstoken (mind. 20 Zeichen)", + "login.submit": "Anmelden", + "login.invalidToken": "Token ungültig oder kürzer als 20 Zeichen.", + "login.error": "Anmeldefehler: {error}", + + "chat.agentLabel": "Agent: ", + "chat.noAgents": "Keine Agenten vorhanden", + "chat.wake": "Wecken", + "chat.wakeTooltip": "Agenten manuell ausführen / wecken", + "chat.clearHistory": "Verlauf leeren", + "chat.clearHistoryTooltip": "Chatverlauf dieses Agenten leeren", + "chat.cancel": "Abbrechen", + "chat.cancelTooltip": "Laufenden Lauf abbrechen", + "chat.placeholder": "Nachricht an den Agenten eingeben (Enter zum Senden, Umschalt+Enter für neue Zeile)…", + "chat.send": "Senden", + "chat.sendTooltip": "Nachricht senden", + "chat.loadingHistory": "Lade Verlauf…", + "chat.emptyHistory": "Noch keine Nachrichten im Verlauf vorhanden.", + "chat.historyLoadError": "Verlauf konnte nicht geladen werden: {error}", + "chat.sendError": "Nachricht konnte nicht gesendet werden: {error}", + "chat.cancelConfirm": "Lauf wurde abgebrochen.", + "chat.cancelError": "Abbruch fehlgeschlagen: {error}", + "chat.wakeSuccess": "Agent wurde manuell gestartet.", + "chat.wakeError": "Starten fehlgeschlagen: {error}", + "chat.clearConfirmPrompt": "Soll der gesamte Chatverlauf dieses Agenten unwiderruflich gelöscht werden?", + "chat.cleared": "Chatverlauf geleert.", + "chat.clearError": "Leeren fehlgeschlagen: {error}", + "chat.role.user": "Benutzer", + "chat.role.system": "System", + "chat.role.tool": "Werkzeug", + + "agents.title": "Agenten", + "agents.newAgent": "Neuer Agent", + "agents.loading": "Lade Agenten…", + "agents.empty": "In dieser Instanz sind noch keine Agenten eingerichtet.", + "agents.createFirst": "Ersten Agenten anlegen", + "agents.id": "Kennung (ID)", + "agents.model": "Modell", + "agents.toolsActive": "{count} Werkzeuge aktiv", + "agents.details": "Agent-Details", + "agents.selectPrompt": "Wähle links einen Agenten aus.", + "agents.chat": "Chatten", + "agents.chatTooltip": "Mit diesem Agenten chatten", + "agents.delete": "Löschen", + "agents.deleteTooltip": "Diesen Agenten löschen", + "agents.name": "Name", + "agents.description": "Beschreibung", + "agents.availableTools": "Verfügbare Werkzeuge", + "agents.noTools": "Keine Werkzeuge für diesen Agenten konfiguriert.", + "agents.configuration": "Konfiguration", + "agents.configTooltip": "Werkzeug-Einstellungen einsehen", + "agents.toolToggled": "Werkzeug '{tool}' {status}.", + "agents.enabled": "aktiviert", + "agents.disabled": "deaktiviert", + "agents.saveFailed": "Änderung fehlgeschlagen: {error}", + "agents.newAgentTitle": "Neuen Agenten anlegen", + "agents.idLabel": "Agent-ID (eindeutiger Schlüssel):", + "agents.idPlaceholder": "z.B. scout", + "agents.nameLabel": "Anzeigename:", + "agents.namePlaceholder": "z.B. Scout Agent", + "agents.modelLabel": "Modell (OpenRouter):", + "agents.descLabel": "Beschreibung:", + "agents.descPlaceholder": "Optionale Beschreibung des Agenten…", + "agents.createButton": "Agent anlegen", + "agents.createdSuccess": "Agent '{name}' wurde erfolgreich angelegt.", + "agents.createFailed": "Fehler beim Anlegen: {error}", + "agents.deleteConfirm": "Soll der Agent '{name}' ({id}) wirklich gelöscht werden?", + "agents.deleted": "Agent '{name}' wurde gelöscht.", + "agents.deleteFailed": "Löschen fehlgeschlagen: {error}", + "agents.settingsTitle": "Einstellungen: {tool} ({agent})", + "agents.loadingSettings": "Lade Konfiguration…", + "agents.secretHint": "Geheime Werte (Passwörter, Schlüssel) werden vom Server maskiert ausgegeben.", + "agents.noSettings": "Keine individuellen Einstellungen hinterlegt.", + "agents.loadFailed": "Laden fehlgeschlagen: {error}", + "agents.rights": "Rechte", + "agents.noOperations": "Dieses Werkzeug kennt keine einzelnen Operationen. Es gilt die Standardregel.", + "agents.configuredHint": "Erlaubte Operationen laufen ohne Rückfrage, alle anderen werden einzeln zur Freigabe vorgelegt, verbotene gar nicht erst.", + "agents.notConfiguredHint": "Noch keine Rechte gesetzt: Es gilt die Vorgabe des Werkzeugs. Mit dem Speichern werden die Einstellungen unten verbindlich.", + "agents.operation": "Operation", + "agents.setting": "Einstellung", + "agents.effect": "Wirkung", + "agents.allowed": "erlaubt", + "agents.singleApproval": "einzeln freigeben", + "agents.forbidden": "verboten", + "agents.alwaysApprovalReason": "Änderungen an Agenten brauchen immer eine Freigabe.", + "agents.saveRights": "Rechte speichern", + "agents.rightsSaved": "Rechte gespeichert.", + "agents.effectAuto": "läuft", + "agents.effectApprove": "einzeln freigeben", + "agents.effectDeny": "gesperrt", + "agents.standingApproval": "Dauerfreigabe #{id}", + + "tasks.title": "Aufgaben", + "tasks.newTask": "Neue Aufgabe", + "tasks.showCompleted": "Abgeschlossene anzeigen", + "tasks.empty": "Keine Aufgaben vorhanden", + "tasks.createTitle": "Aufgabe erstellen", + "tasks.agent": "Agent", + "tasks.taskMessage": "Aufgabenbeschreibung / Nachricht", + "tasks.schedule": "Zeitplan", + "tasks.scheduleType": "Planungsart", + "tasks.scheduleNone": "Keine (Nur manuell)", + "tasks.scheduleCron": "Cron-Ausdruck", + "tasks.scheduleInterval": "Intervall", + "tasks.scheduleAt": "Fester Zeitpunkt", + "tasks.runNow": "Jetzt ausführen", + "tasks.pause": "Pausieren", + "tasks.resume": "Fortsetzen", + "tasks.delete": "Löschen", + "tasks.saved": "Aufgabe gespeichert.", + "tasks.deleted": "Aufgabe gelöscht.", + "tasks.runTriggered": "Lauf ausgelöst.", + + "services.title": "Dienste", + "services.newService": "Neuer Dienst", + "services.empty": "Keine Hintergrunddienste konfiguriert", + "services.serviceName": "Dienstname", + "services.serviceType": "Typ", + "services.port": "Port", + "services.active": "Aktiv", + "services.start": "Starten", + "services.stop": "Beenden", + "services.restart": "Neustarten", + + "backup.title": "Datensicherung & Wiederherstellung", + "backup.refresh": "Aktualisieren", + "backup.refreshTooltip": "Sicherungen neu laden", + "backup.newBackup": "Neue Sicherung", + "backup.newBackupTooltip": "Neue Sicherung erstellen", + "backup.noInstance": "Keine Instanz ausgewählt", + "backup.selectInstancePrompt": "Bitte wähle oben im Kopfbereich eine Instanz aus, um deren Sicherungen zu verwalten.", + "backup.loading": "Lade Sicherungen…", + "backup.backupDir": "Sicherungsverzeichnis", + "backup.backupDirHint": "Sicherungen werden als Ganzes mit dem Lizenzschlüssel des Instanzmanagers verschlüsselt und enthalten die Zugangsdaten. Auf einer anderen Maschine lassen sie sich mit demselben Lizenzschlüssel zurückspielen.", + "backup.existingBackups": "Vorhandene Sicherungen ({count})", + "backup.noBackups": "Für diese Instanz gibt es noch keine Sicherung. Lege über die Schaltfläche oben eine an.", + "backup.restore": "Wiederherstellen", + "backup.restoreTooltip": "Diese Sicherung wiederherstellen", + "backup.deleteTooltip": "Sicherungsdatei unwiderruflich löschen", + "backup.archiveFile": "Archivdatei", + "backup.createdAt": "Erstellt am", + "backup.size": "Größe", + "backup.protection": "Schutz", + "backup.files": "Dateien", + "backup.actions": "Aktionen", + "backup.fileCount": "{count} Dateien", + "backup.foreignLicense": "Fremde Lizenz", + "backup.unreadable": "Unlesbar", + "backup.licenseProtection": "Lizenz ({count} Zugangsdaten)", + "backup.passphraseProtection": "Passphrase ({count})", + "backup.unencryptedProtection": "Unverschlüsselt, ohne Zugangsdaten", + "backup.dialogTitle": "Neue Sicherung erstellen", + "backup.dialogHint": "Die Sicherung wird mit dem Lizenzschlüssel des Instanzmanagers verschlüsselt und enthält die Zugangsdaten. Ist ein FTP-Upload eingerichtet, wird sie anschließend hochgeladen.", + "backup.includeChats": "Chatverläufe sichern", + "backup.includeLogs": "Protokolldateien (Logs) einschließen", + "backup.createButton": "Sicherung erstellen", + "backup.creating": "Erstelle Sicherung…", + "backup.created": "Sicherung erstellt.", + "backup.createFailed": "Fehler beim Sichern: {error}", + "backup.restoreTitle": "Sicherung wiederherstellen: {file}", + "backup.overwriteWarningTitle": "Achtung: Überschreiben bestehender Daten", + "backup.overwriteWarningText": "Die Wiederherstellung überschreibt die aktuellen Konfigurationen und Daten der Instanz mit dem Stand vom {date}.", + "backup.instanceStopNotice": "Die Instanz wird dafür geordnet angehalten und danach wieder gestartet.", + "backup.passphraseLabel": "Passphrase für Zugangsdaten", + "backup.passphrasePlaceholder": "Passphrase der Sicherung…", + "backup.passphraseRequired": "Bitte gib die Passphrase an.", + "backup.restoreNow": "Jetzt wiederherstellen", + "backup.restoring": "Stelle wieder her…", + "backup.restoreSuccess": "Wiederherstellung abgeschlossen.", + "backup.restoreFailed": "Wiederherstellung fehlgeschlagen: {error}", + "backup.foreignLicenseTitle": "Lizenzschlüssel der Quelle erforderlich", + "backup.sourceLicenseLabel": "Lizenzschlüssel der Quelle", + "backup.sourceLicenseHint": "Groß- und Kleinschreibung sowie Leerzeichen spielen keine Rolle.", + "backup.sourceLicensePlaceholder": "Lizenzschlüssel der Installation, von der die Sicherung stammt…", + "backup.restoreWithKey": "Mit diesem Schlüssel wiederherstellen", + "backup.keyMismatch": "Dieser Lizenzschlüssel passt nicht zu der Sicherung.", + "backup.deleteTitle": "Sicherung löschen", + "backup.deleteConfirm": "Soll die Sicherungsdatei '{file}' wirklich unwiderruflich gelöscht werden?", + "backup.deleted": "Sicherung gelöscht.", + "backup.deleteFailed": "Löschen fehlgeschlagen: {error}", + "backup.ftpTitle": "FTP-Upload der Sicherungen", + "backup.ftpLoading": "Lade FTP-Einstellungen…", + "backup.ftpHint": "Die Sicherungen sind verschlüsselt — wer den Server liest, liest nichts Brauchbares. Das Kennwort des Servers geht ohne TLS aber im Klartext über die Leitung.", + "backup.ftpAutoUpload": "Fertige Sicherungen automatisch hochladen", + "backup.ftpServer": "Server", + "backup.ftpPort": "Port", + "backup.ftpUser": "Benutzer", + "backup.ftpPassword": "Kennwort", + "backup.ftpPasswordSaved": "(gespeichert — leer lassen für unverändert)", + "backup.ftpDirectory": "Zielordner", + "backup.ftpDirectoryHint": "Darunter bekommt jede Instanz einen eigenen Ordner.", + "backup.ftpKeepCount": "Sicherungen je Instanz behalten", + "backup.ftpKeepCountHint": "Ältere werden auf dem Server gelöscht.", + "backup.ftpRequireTls": "Nur mit TLS verbinden", + "backup.ftpTest": "Verbindung testen", + "backup.ftpTestFailed": "Test fehlgeschlagen: {error}", + "backup.ftpSaved": "Gespeichert.", + "backup.ftpSaveFailed": "Speichern fehlgeschlagen: {error}", + + "logs.title": "Protokoll", + "logs.connecting": "Verbinde…", + "logs.allInstances": "Alle Instanzen", + "logs.allModules": "Alle Module", + "logs.clear": "Leeren", + "logs.pause": "Pausieren", + "logs.resume": "Fortsetzen", + "logs.paused": "Pausiert", + "logs.autoScroll": "Autoscroll", + "logs.empty": "Noch keine Protokolleinträge vorhanden.", + + "settings.title": "Einstellungen & Systemarchitektur", + "settings.intro": "Übersicht der Konfiguration nach dem Instanzmanager-Konzept: Strikte Trennung zwischen globaler Manager-Ebene (Installation) und isolierter Instanz-Ebene.", + "settings.managerLevel": "Instanzmanager", + "settings.managerBadge": "Manager-Ebene · Global", + "settings.managerIntro": "Gilt für alle Instanzen und die Weboberfläche. Gespeichert in manager.json im Anwendungs-Hauptverzeichnis.", + "settings.webUiAndNet": "Weboberfläche & Netzwerk", + "settings.configFile": "Konfigurationsdatei", + "settings.webUiPort": "WebUI-Port", + "settings.webUiPortDefault": "7000 (Vorgabe, webUiPort)", + "settings.networkBinding": "Netzwerk-Bindung", + "settings.networkBindingValue": "loopback (127.0.0.1, webUiBinding)", + "settings.accessProtection": "Zugangsschutz", + "settings.tokenActive": "Token aktiv (≥ 20 Zeichen)", + "settings.licenseAndUpdates": "Lizenz & Updates", + "settings.licenseScope": "Lizenz-Geltung", + "settings.licenseScopeValue": "Installationsebene (E-M3: Beliebig viele Instanzen)", + "settings.licenseKey": "Lizenzschlüssel", + "settings.licenseEncrypted": "●●●●-●●●●-●●●●-●●●● (verschlüsselt)", + "settings.updateChannel": "Update-Kanal", + "settings.updateChannelValue": "prod (Offizielle Release-Versionen)", + "settings.updateCheck": "Update-Prüfung", + "settings.updateCheckValue": "Zentral auf Manager-Ebene", + "settings.dcAndOversight": "Deploymentcenter & Aufsicht", + "settings.dcMasterToken": "Master-Token", + "settings.dcMasterTokenValue": "●●●●●●●● (erzeugt Sub-Token je Instanz)", + "settings.desktopWindows": "Desktop-Fenster", + "settings.desktopWindowsValue": "Aktiv (jede Instanz hat ein eigenes Fenster)", + "settings.crashProtection": "Absturz-Schutz", + "settings.crashProtectionValue": "Max. 5 Neustarts / 10 Minuten", + "settings.instanceSectionTitle": "Aktive Instanz: {name}", + "settings.instanceSectionTitleNone": "Instanzbezogene Einstellungen", + "settings.instanceBadge": "Instanz-Ebene · Isoliert", + "settings.noInstanceActive": "Keine Instanz aktiv", + "settings.instanceIntro": "Gilt ausschließlich für die aktuell ausgewählte Instanz. Alle Pfade, Protokolle, Agenten und Budgets sind strikt von anderen Instanzen isoliert.", + "settings.noInstanceSelected": "Keine Instanz ausgewählt", + "settings.noInstancePrompt": "Bitte wähle oben im Kopfbereich eine aktive Instanz aus oder wechsle in den Bereich 'Instanzen', um eine Instanz zu starten oder auszuwählen.", + "settings.toInstancesOverview": "Zur Instanzen-Übersicht", + "settings.identityAndProcess": "Identität & Prozess", + "settings.instanceName": "Instanzname", + "settings.instanceId": "Instanz-ID", + "settings.autostartByManager": "Autostart durch Manager", + "settings.autostartActive": "Aktiv (Am Laufen halten)", + "settings.autostartInactive": "Inaktiv (Manueller Start)", + "settings.communication": "Kommunikation", + "settings.communicationValue": "HTTP über Named Pipe (Windows) / Socket (Linux)", + "settings.isolatedPaths": "Isolierte Verzeichnisse", + "settings.logsPath": "Protokolle (Logs)", + "settings.backupsPath": "Datensicherungen", + "settings.stateDbPath": "Zustandsdatenbank", + "settings.stateDbValue": "Instances/{name}/state.db (exklusiv)", + "settings.workspacePath": "Arbeitsbereich (Shared)", + "settings.openRouterCard": "Modellanbindung (OpenRouter)", + "settings.openRouterStatus": "OpenRouter-Status", + "settings.connected": "Verbunden", + "settings.offline": "Offline", + "settings.statusMessage": "Statusmeldung", + "settings.credits": "Guthaben / Credits", + "settings.apiKey": "API-Schlüssel", + "settings.apiKeyMasked": "In InstanceSettings.json dieser Instanz maskiert hinterlegt", + "settings.openRouterNotRunning": "Instanz läuft nicht. Daten werden nach dem Start geladen.", + "settings.openRouterNoKey": "Kein Schlüssel hinterlegt oder Dienst nicht erreichbar.", + "settings.runtimeAndSubsystems": "Laufzeit & Teilsysteme", + "settings.agentEngine": "Agent Engine", + "settings.scheduler": "Scanner / Scheduler", + "settings.stagingService": "Freigabestelle (Staging)", + "settings.dcSubToken": "Deploymentcenter Sub-Token", + "settings.subsystemsLoading": "Laufzeitdaten werden abgefragt…", + "settings.subsystemsStopped": "Instanz ist beendet — Teilsysteme ruhen.", + "settings.save": "Einstellungen speichern", + "settings.discard": "Verwerfen", + "settings.saved": "Einstellungen erfolgreich gespeichert.", + "settings.saveError": "Fehler beim Speichern der Einstellungen: {error}", + + "info.title": "System & Status", + "info.noInstanceSelected": "Keine Instanz ausgewählt", + "info.noInstanceHint": "Wähle oben im Kopfbereich eine aktive Instanz aus oder wechsle in den Bereich 'Instanzen', um eine Instanz zu starten.", + "info.loading": "Lade Systemdaten…", + "info.instanceAndVersion": "Instanz & Version", + "info.instanceName": "Instanzname", + "info.instanceId": "Instanz-ID", + "info.version": "Version", + "info.build": "Build", + "info.environment": "Umgebung", + "info.dataDirectory": "Datenverzeichnis", + "info.subsystems": "Teilsysteme", + "info.openRouterStatus": "OpenRouter & Guthaben", + + "instances.title": "Instanzverwaltung", + "instances.refresh": "Aktualisieren", + "instances.newInstance": "Neue Instanz", + "instances.activeBadge": "Aktiv im WebUI", + "instances.selectActive": "Als aktiv wählen", + "instances.start": "Starten", + "instances.stop": "Beenden", + "instances.bringToFront": "Fenster nach vorn", + "instances.createTitle": "Neue Instanz anlegen", + "instances.instanceOfflineError": "Die Instanz ist nicht erreichbar.", + "instances.noInstanceActiveError": "Keine Instanz aktiv. Unter 'Instanzen' starten oder auswählen.", + "instances.instanceNameLabel": "Instanz-Name", + "instances.instanceNamePlaceholder": "z.B. production-1", + "instances.dailyBudgetUsd": "Tagesbudget ($ USD)", + "instances.dailyBudgetTokens": "Tagesbudget (Tokens)", + "instances.autostart": "Autostart durch Manager", + "instances.created": "Instanz erfolgreich angelegt.", + "instances.createError": "Fehler beim Anlegen der Instanz: {error}", + + "staging.title": "Freigabestelle", + "staging.refresh": "Aktualisieren", + "staging.loading": "Lade Freigaben…", + "staging.empty": "Keine offenen Freigaben", + "staging.emptyHint": "Aktuell warten keine Werkzeug-Aufrufe auf eine manuelle Bestätigung.", + "staging.approve": "Freigeben", + "staging.reject": "Ablehnen", + "staging.callId": "Aufruf-ID", + "staging.agent": "Agent", + "staging.tool": "Werkzeug", + "staging.time": "Zeitpunkt", + "staging.frozenArgs": "Eingefrorene Argumente:", + "staging.suggestHint": "Diese Operation von {agent} wurde in den letzten 14 Tagen schon {approvals}-mal freigegeben. Soll {tool}.{action} für diesen Agenten künftig ohne Rückfrage laufen?", + "staging.allowSimilar": "Gleichartiges künftig erlauben: {agent} darf {tool}.{action} ab jetzt ohne Rückfrage (widerrufbar unten unter „Dauerfreigaben“).", + "staging.notActive": "Freigabestelle nicht aktiv", + "staging.notActiveHint": "In dieser Instanz ist keine Freigabestelle eingerichtet.", + "staging.approved": "Aufruf freigegeben{similar}: {message}", + "staging.similarSuffix": " und künftig erlaubt", + "staging.executedSuccess": "Erfolgreich ausgeführt", + "staging.approveFailed": "Freigabe fehlgeschlagen: {error}", + "staging.standingTitle": "Dauerfreigaben", + "staging.standingEmpty": "Keine. Beim Freigeben lässt sich „Gleichartiges künftig erlauben“ wählen; dauerhafte Rechte stehen sonst in der Werkzeugkonfiguration des Agenten.", + "staging.granted": "Erteilt", + "staging.occasion": "Anlass", + "staging.revoke": "Widerrufen", + "staging.revokeConfirm": "Dauerfreigabe widerrufen? {agent} muss {tool}.{action} dann wieder einzeln vorlegen.", + "staging.revoked": "Widerrufen.", + "staging.revokeFailed": "Widerruf fehlgeschlagen: {error}", + "staging.rejectTitle": "Werkzeug-Aufruf ablehnen", + "staging.rejectReasonLabel": "Begründung (optional):", + "staging.rejectReasonPlaceholder": "Begründung für die Ablehnung an den Agenten…", + "staging.rejectButton": "Aufruf ablehnen", + "staging.rejected": "Aufruf wurde abgelehnt.", + "staging.rejectFailed": "Ablehnen fehlgeschlagen: {error}", + + "agentChats.title": "Agenten-Kommunikation (A2A)", + "agentChats.allAgents": "Alle Agenten", + "agentChats.filterTooltip": "Nach beteiligtem Agenten filtern (Absender oder Empfänger)", + "agentChats.fromTooltip": "Nachrichten ab diesem Tag", + "agentChats.toTooltip": "Nachrichten bis einschließlich diesem Tag", + "agentChats.searchPlaceholder": "Volltext…", + "agentChats.searchTooltip": "Suche im Nachrichtentext und in den Agentennamen", + "agentChats.refresh": "Aktualisieren", + "agentChats.refreshTooltip": "Kommunikation neu laden", + "agentChats.from": "von", + "agentChats.to": "bis", + "agentChats.noInstance": "Keine Instanz ausgewählt", + "agentChats.selectInstancePrompt": "Bitte wähle oben im Kopfbereich eine Instanz aus, um deren Agenten-Kommunikation anzuzeigen.", + "agentChats.infoTitle": "Interne Kommunikation", + "agentChats.infoHint": "Hier wird der direkte Nachrichtenaustausch zwischen Agenten protokolliert, wenn Agenten über das Werkzeug AgentComm (Aktion send_message) miteinander sprechen.", + "agentChats.loading": "Lade Kommunikationsverlauf…", + "agentChats.matchCount": "{filtered} von {total} Nachrichten", + "agentChats.totalCount": "{total} Nachrichten", + "agentChats.empty": "Keine Nachrichten", + "agentChats.emptyFilter": "Für diese Filter liegt keine interne Kommunikation vor.", + "agentChats.emptyInstance": "In dieser Instanz wurde bisher keine interne Agent-zu-Agent-Kommunikation protokolliert.", + "agentChats.replyTo": "➔ Antwort an", + "agentChats.arrow": "➔", + "agentChats.loadFailed": "Kommunikation konnte nicht geladen werden: {error}", + + "usage.title": "Token-Verbrauch & Analyse", + "usage.loading": "Lade Verbrauchsdaten…", + "usage.empty": "Keine Daten", + "usage.emptyHint": "Noch keine Token-Verbrauchsdaten aufgezeichnet.", + "usage.totalCost": "Gesamtkosten ({runs} Läufe)", + "usage.totalTokens": "Tokens gesamt", + "usage.inputTokens": "Eingabe-Tokens (Prompt)", + "usage.outputTokens": "Ausgabe-Tokens (Completion)", + "usage.unknownCostRuns": "{count} Lauf/Läufe ohne bekannten Preis — deren Kosten sind in der Summe nicht enthalten.", + "usage.tokensByAgent": "Tokens nach Agent", + "usage.tokensByModel": "Tokens nach Modell", + "usage.recentRuns": "Letzte Agenten-Läufe", + "usage.time": "Zeitpunkt", + "usage.agent": "Agent", + "usage.model": "Modell", + "usage.status": "Status", + "usage.prompt": "Prompt", + "usage.completion": "Completion", + "usage.total": "Gesamt", + "usage.cost": "Kosten", + "usage.unknownPrice": "(Preis unbekannt)", + "usage.notAvailable": "Verbrauchsdaten nicht verfügbar", + "usage.forecastTitle": "Kostenprognose", + "usage.forecastDay": "Prognose je Tag", + "usage.forecastMonth": "Prognose je Monat (30 Tage)", + "usage.tasksDay": "davon Aufgaben je Tag", + "usage.otherDay": "sonstige Läufe je Tag (Ø 7 Tage)", + "usage.budgetWarning": "{agent}: Die geplanten Aufgaben kosten laut Prognose {cost} am Tag, das Tagesbudget erlaubt {budget}. Die späten Läufe eines Tages werden abgelehnt.", + "usage.withoutBasis": "{count} Aufgabe(n) ohne Grundlage (kein Lauf, kein Modellpreis) – nicht in der Summe enthalten.", + "usage.oneOffOpen": "Offene einmalige Aufgaben kosten zusätzlich etwa {cost}.", + "usage.basisHint": "Grundlage je Aufgabe: Mittelwert ihrer letzten Läufe, sonst der des Agenten, sonst eine Schätzung aus Promptlänge und Listenpreis (eher zu hoch).", + "usage.task": "Aufgabe", + "usage.runsDay": "Läufe/Tag", + "usage.perRun": "je Lauf", + "usage.basis": "Grundlage", + "usage.perDay": "je Tag", + "usage.basisRuns": "Ø letzte {n} Läufe", + "usage.basisAgent": "Ø des Agenten ({n} Läufe)", + "usage.basisEstimate": "Schätzung – noch kein Lauf", + "usage.basisUnknown": "keine Grundlage", "filter.range": "Zeitraum", "filter.range.24h": "Letzte 24 Stunden", @@ -42,15 +532,15 @@ "runs.total.runs": "Läufe", "runs.total.prompt": "Prompt-Tokens", "runs.total.completion": "Antwort-Tokens", - "runs.total.cached": "Aus dem Cache", + "runs.total.cached": "Aus Cache", "runs.total.cost": "Kosten", - "runs.total.notRun": "Abgewiesen (Budget, Herunterfahren)", - "runs.unknownCost": "{n} Lauf/Läufe ohne bekannten Preis sind mit 0 $ gezählt.", + "runs.total.notRun": "Abgelehnt (Budget, Shutdown)", + "runs.unknownCost": "{n} Lauf/Läufe ohne bekannten Preis sind mit 0 $ gerechnet.", "runs.empty.title": "Keine Läufe", - "runs.empty.text": "Kein Lauf passt zu diesem Filter.", + "runs.empty.text": "Kein Lauf passt auf diesen Filter.", "runs.list": "Läufe", "runs.page": "{from}–{to} von {total}", - "runs.col.time": "Zeit", + "runs.col.time": "Zeitpunkt", "runs.col.agent": "Agent", "runs.col.model": "Modell", "runs.col.event": "Ereignis", @@ -66,33 +556,33 @@ "detail.notFound": "Lauf nicht gefunden", "detail.runId": "Lauf-ID", "detail.tokens": "Tokens", - "detail.tokensValue": "{prompt} Prompt · {completion} Antwort · {cached} aus dem Cache", + "detail.tokensValue": "{prompt} Prompt · {completion} Antwort · {cached} aus Cache", "detail.result": "Ergebnis", - "detail.receipt": "Beleg", + "detail.receipt": "Quittung", "detail.transcript": "Prompt und Antwort", - "detail.transcript.disabled": "Für diesen Agenten ist der Prompt-Verlauf abgeschaltet (Aufbewahrung: nie).", + "detail.transcript.disabled": "Prompt-Verlauf ist für diesen Agenten abgeschaltet (Aufbewahrung: nie).", "detail.transcript.unavailable": "Diese Instanz zeichnet keinen Prompt-Verlauf auf.", - "detail.transcript.missing": "Kein Transkript zu diesem Lauf – er lief vor Beginn der Aufzeichnung oder ist über seine Aufbewahrungsfrist hinaus.", - "detail.transcript.truncated": "Dieser Lauf hat die Größengrenze des Prompt-Verlaufs überschritten; spätere Einträge sind gekürzt.", + "detail.transcript.missing": "Kein Verlauf für diesen Lauf — er lief vor Beginn der Aufzeichnung oder ist älter als die Aufbewahrungsfrist.", + "detail.transcript.truncated": "Dieser Lauf hat die Größengrenze des Verlaufs überschritten; spätere Einträge sind gekürzt.", "detail.transcript.size": "{n} Einträge, {chars} Zeichen", - "detail.systemPrompt": "Systemprompt ({n} Zeichen)", - "detail.entry.user": "Auftrag / Nachricht", - "detail.entry.engine": "Nachfrage der Engine", + "detail.systemPrompt": "System-Prompt ({n} Zeichen)", + "detail.entry.user": "Aufgabe / Nachricht", + "detail.entry.engine": "Nachfasslauf der Engine", "detail.entry.event": "Ereignis", "detail.entry.assistant": "Modellschritt {n}", "detail.entry.usage": "{prompt} rein · {completion} raus", "detail.entry.tool_result": "Ergebnis: {tool}", - "detail.entry.call": "Ruft {tool} auf", + "detail.entry.call": "Ruft {tool}", "detail.entry.clipped": "gekürzt", "detail.tool": "Werkzeug", "detail.arguments": "Argumente", "detail.toolCalls": "Werkzeugaufrufe", - "detail.toolCalls.hint": "Aus dem Audit-Log – auch ohne Transkript vorhanden.", + "detail.toolCalls.hint": "Aus dem Audit-Log — auch ohne Verlauf vorhanden.", "detail.approvals": "Zur Freigabe vorgelegt", "timeline.title": "Tokennutzung › Zeitverlauf", "timeline.chart": "Zeitverlauf", - "timeline.metric": "Maß", + "timeline.metric": "Messwert", "timeline.metric.cost": "Kosten", "timeline.metric.tokens": "Tokens", "timeline.metric.runs": "Läufe", @@ -106,21 +596,21 @@ "timeline.tooltipRuns": "{n} Lauf/Läufe", "timeline.event.budget_exceeded": "Budget erschöpft", "timeline.event.model_changed": "Modell gewechselt", - "timeline.hint": "Ein Klick auf einen Balken zeigt dessen Läufe.", + "timeline.hint": "Klick auf einen Balken zeigt dessen Läufe.", "history.title": "Prompt-Verlauf", - "history.intro": "Prompt und Antwort jedes Laufs, durchsuchbar über das ganze Gespräch samt Werkzeugaufrufen und Ergebnissen.", + "history.intro": "Prompt und Antwort jedes Laufs, durchsuchbar über die gesamte Unterhaltung samt Werkzeugaufrufen und Ergebnissen.", "history.unavailable": "Prompt-Verlauf nicht verfügbar", "history.empty.title": "Nichts aufgezeichnet", - "history.empty.text": "Keine Transkripte in diesem Zeitraum. Die Aufzeichnung beginnt mit Version 0.1.12 und folgt der Aufbewahrung unten.", - "history.empty.search": "Kein Transkript enthält diesen Text.", - "history.list": "Transkripte ({n})", + "history.empty.text": "Keine Verläufe in diesem Zeitraum. Die Aufzeichnung beginnt mit Version 0.1.12 und folgt den unten stehenden Fristen.", + "history.empty.search": "Kein Verlauf enthält diesen Text.", + "history.list": "Verläufe ({n})", "history.prompt": "Prompt", "history.response": "Antwort", "history.truncated": "Größengrenze erreicht ({chars} Zeichen gespeichert).", "history.settings.title": "Aufbewahrung", - "history.settings.intro": "Wie lange Prompts und Antworten aufbewahrt werden. Die Vorgabe gilt für jeden Agenten ohne eigene Einstellung; „nie“ löscht auch, was vorher aufgezeichnet wurde.", + "history.settings.intro": "Wie lange Prompts und Antworten aufgehoben werden. Die Vorgabe gilt für jeden Agenten ohne eigene Angabe; „nie“ löscht auch, was vorher aufgezeichnet war.", "history.settings.default": "Vorgabe für alle Agenten:", "history.settings.agent": "Agent", "history.settings.retention": "Aufbewahrung", @@ -129,13 +619,65 @@ "history.settings.mode.inherit": "Vorgabe", "history.settings.mode.never": "Nie aufzeichnen", "history.settings.mode.days": "Tage", - "history.settings.mode.forever": "Unbegrenzt", + "history.settings.mode.forever": "Für immer aufheben", "history.settings.days": "Tage", "history.settings.daysUnit": "Tage", "history.settings.effective.never": "wird nicht aufgezeichnet", - "history.settings.effective.forever": "unbegrenzt", - "history.settings.effective.days": "{n} Tage", + "history.settings.effective.forever": "bleibt für immer", + "history.settings.effective.days": "bleibt {n} Tage", "history.settings.save": "Aufbewahrung speichern", "history.settings.saved": "Aufbewahrung gespeichert.", - "history.settings.failed": "Speichern fehlgeschlagen: {error}" + "history.settings.failed": "Speichern fehlgeschlagen: {error}", + + "desktop.mainWindow.title": "ClawdDotNet", + "desktop.mainWindow.sidebarTooltip": "Seitenleiste ein-/ausklappen", + "desktop.mainWindow.switchInstance": "Instanz wechseln", + "desktop.mainWindow.instance": "INSTANZ", + "desktop.mainWindow.themeLight": "Hellmodus", + "desktop.mainWindow.themeDark": "Dunkelmodus", + "desktop.mainWindow.openInstanceFolder": "Instanz-Ordner öffnen", + "desktop.mainWindow.instanceManager": "Instanzmanager", + "desktop.mainWindow.exit": "Beenden", + "desktop.nav.work": "Arbeiten", + "desktop.nav.analytics": "Analyse", + "desktop.nav.system": "System", + "desktop.status.unknown": "unbekannt", + "desktop.status.design": "Entwurf", + "desktop.status.noApiKey": "kein API-Key", + "desktop.status.noUsageData": "keine Nutzungsdaten", + "desktop.settings.discard": "Verwerfen", + "desktop.settings.save": "Speichern", + "desktop.settings.instance": "INSTANZ", + "desktop.settings.instanceName": "Instanz-Name", + "desktop.settings.apiKey": "OpenRouter API-Key", + "desktop.settings.autostart": "Autostart durch Manager", + "desktop.settings.autostartHint": "Der Instanzmanager startet diese Instanz, wenn er hochkommt, und wieder, wenn sie abstürzt.", + "desktop.settings.logDir": "Protokoll-Verzeichnis", + "desktop.settings.logDirHint": "Gilt nur für diese Instanz. Leer heißt: Unterordner Logs im Instanzverzeichnis.", + "desktop.settings.dailyCost": "Tagesbudget ($ USD)", + "desktop.settings.dailyTokens": "Tagesbudget (Tokens)", + "desktop.settings.general": "ALLGEMEIN", + "desktop.settings.instancesDir": "Instanzen-Verzeichnis", + "desktop.settings.minLogLevel": "Minimaler Log-Level", + "desktop.settings.openRouterUrl": "OpenRouter Base-URL", + "desktop.settings.language": "Sprache", + "desktop.settings.dcAndLicense": "DEPLOYMENTCENTER & LIZENZ", + "desktop.settings.dcUrl": "Server-URL", + "desktop.settings.dcToken": "Master-Token", + "desktop.settings.dcEnv": "Umgebung", + "desktop.settings.errorReporting": "Fehler automatisch melden", + "desktop.settings.updateCheck": "Beim Start auf Updates prüfen", + "desktop.settings.updateChannel": "Update-Kanal", + "desktop.settings.licenseKey": "Lizenzschlüssel", + "desktop.chat.inputPlaceholder": "Nachricht eingeben (Enter zum Senden, Shift+Enter für neue Zeile)...", + "desktop.chat.send": "Senden", + "desktop.chat.statusReady": "Bereit", + "desktop.chat.statusSending": "Sende…", + "desktop.chat.statusDesign": "Entwurfsmodus", + "desktop.dialog.addAgent": "Agent hinzufügen", + "desktop.dialog.addJob": "Job hinzufügen", + "desktop.dialog.addService": "Dienst hinzufügen", + "desktop.dialog.restoreBackup": "Sicherung wiederherstellen", + "desktop.dialog.license": "Lizenz-Aktivierung", + "desktop.dialog.instancePicker": "Instanz auswählen" } diff --git a/src/ClawdDotNet.Manager.Web/wwwroot/i18n/en.json b/src/ClawdDotNet.Manager.Web/wwwroot/i18n/en.json index 4264f1b..e5044c2 100644 --- a/src/ClawdDotNet.Manager.Web/wwwroot/i18n/en.json +++ b/src/ClawdDotNet.Manager.Web/wwwroot/i18n/en.json @@ -6,6 +6,496 @@ "common.loading": "Loading…", "common.previous": "Previous", "common.next": "Next", + "common.save": "Save", + "common.cancel": "Cancel", + "common.delete": "Delete", + "common.edit": "Edit", + "common.refresh": "Refresh", + "common.close": "Close", + "common.discard": "Discard", + "common.apply": "Apply", + "common.reset": "Reset", + "common.search": "Search", + "common.actions": "Actions", + "common.status": "Status", + "common.name": "Name", + "common.description": "Description", + "common.ready": "Ready", + "common.none": "None", + "common.unknown": "Unknown", + "common.all": "All", + "common.details": "Details", + "common.language": "Language", + "common.logout": "Log Out", + "common.confirm": "Confirm", + "common.yes": "Yes", + "common.no": "No", + + "status.running": "Running", + "status.stopped": "Stopped", + "status.silent": "Silent", + "status.crashed": "Crashed", + "status.unknown": "Unknown", + "status.ready": "Ready", + "status.busy": "Busy", + "status.paused": "Paused", + "status.done": "Done", + "status.failed": "Failed", + "status.blocked": "Blocked", + "status.inReview": "In Review", + "status.canceled": "Canceled", + "status.archived": "Archived", + + "header.instanceLabel": "Instance:", + "header.noInstance": "(No instance)", + "header.switchInstance": "Switch active instance", + "header.startInstance": "Start", + "header.startInstanceTooltip": "Start selected instance now", + "header.toggleTheme": "Toggle color theme", + "header.eventStreamConnected": "Event stream: connected", + "header.eventStreamDisconnected": "Event stream: disconnected", + "header.multipleRunning": "{count} running", + "header.noneActive": "None active", + "header.starting": "Starting…", + "header.noInstances": "No instances", + "header.selectInstancePlaceholder": "— Select instance —", + "header.instanceOffline": "Instance offline (502)", + "header.selectInstancePrompt": "Select instance (409)", + + "nav.toggleSidebar": "Toggle sidebar", + "nav.group.work": "Work", + "nav.chat": "Chat", + "nav.agents": "Agents", + "nav.tasks": "Tasks", + "nav.services": "Services", + "nav.group.analytics": "Analytics", + "nav.usage": "Token Usage", + "nav.agentChats": "Agent Chats", + "nav.group.system": "System", + "nav.instances": "Instances", + "nav.staging": "Approvals", + "nav.logs": "Logs", + "nav.backup": "Backup", + "nav.settings": "Settings", + "nav.info": "Info", + + "login.prompt": "Please enter the instance manager access token (webUiToken in manager.json).", + "login.tokenPlaceholder": "Access token (at least 20 characters)", + "login.submit": "Sign In", + "login.invalidToken": "Token is invalid or shorter than 20 characters.", + "login.error": "Sign in error: {error}", + + "chat.agentLabel": "Agent: ", + "chat.noAgents": "No agents available", + "chat.wake": "Wake", + "chat.wakeTooltip": "Manually trigger / wake agent", + "chat.clearHistory": "Clear history", + "chat.clearHistoryTooltip": "Clear chat history for this agent", + "chat.cancel": "Cancel", + "chat.cancelTooltip": "Cancel active run", + "chat.placeholder": "Type message to agent (Enter to send, Shift+Enter for new line)…", + "chat.send": "Send", + "chat.sendTooltip": "Send message", + "chat.loadingHistory": "Loading history…", + "chat.emptyHistory": "No messages in history yet.", + "chat.historyLoadError": "Failed to load history: {error}", + "chat.sendError": "Failed to send message: {error}", + "chat.cancelConfirm": "Run was canceled.", + "chat.cancelError": "Cancellation failed: {error}", + "chat.wakeSuccess": "Agent started manually.", + "chat.wakeError": "Failed to start agent: {error}", + "chat.clearConfirmPrompt": "Permanently clear the entire chat history for this agent?", + "chat.cleared": "Chat history cleared.", + "chat.clearError": "Failed to clear history: {error}", + "chat.role.user": "User", + "chat.role.system": "System", + "chat.role.tool": "Tool", + + "agents.title": "Agents", + "agents.newAgent": "New Agent", + "agents.loading": "Loading agents…", + "agents.empty": "No agents configured in this instance yet.", + "agents.createFirst": "Create first agent", + "agents.id": "ID", + "agents.model": "Model", + "agents.toolsActive": "{count} tools active", + "agents.details": "Agent Details", + "agents.selectPrompt": "Select an agent on the left.", + "agents.chat": "Chat", + "agents.chatTooltip": "Chat with this agent", + "agents.delete": "Delete", + "agents.deleteTooltip": "Delete this agent", + "agents.name": "Name", + "agents.description": "Description", + "agents.availableTools": "Available Tools", + "agents.noTools": "No tools configured for this agent.", + "agents.configuration": "Configuration", + "agents.configTooltip": "View tool settings", + "agents.toolToggled": "Tool '{tool}' {status}.", + "agents.enabled": "enabled", + "agents.disabled": "disabled", + "agents.saveFailed": "Modification failed: {error}", + "agents.newAgentTitle": "Create New Agent", + "agents.idLabel": "Agent ID (unique key):", + "agents.idPlaceholder": "e.g. scout", + "agents.nameLabel": "Display Name:", + "agents.namePlaceholder": "e.g. Scout Agent", + "agents.modelLabel": "Model (OpenRouter):", + "agents.descLabel": "Description:", + "agents.descPlaceholder": "Optional description of the agent…", + "agents.createButton": "Create Agent", + "agents.createdSuccess": "Agent '{name}' created successfully.", + "agents.createFailed": "Failed to create agent: {error}", + "agents.deleteConfirm": "Really delete agent '{name}' ({id})?", + "agents.deleted": "Agent '{name}' deleted.", + "agents.deleteFailed": "Deletion failed: {error}", + "agents.settingsTitle": "Settings: {tool} ({agent})", + "agents.loadingSettings": "Loading configuration…", + "agents.secretHint": "Secret values (passwords, keys) are masked by the server.", + "agents.noSettings": "No custom settings configured.", + "agents.loadFailed": "Loading failed: {error}", + "agents.rights": "Permissions", + "agents.noOperations": "This tool has no individual operations. Default rule applies.", + "agents.configuredHint": "Allowed operations run without confirmation, others require single approval, forbidden are denied immediately.", + "agents.notConfiguredHint": "No permissions configured: Tool defaults apply. Saving below will make these settings binding.", + "agents.operation": "Operation", + "agents.setting": "Setting", + "agents.effect": "Effect", + "agents.allowed": "allowed", + "agents.singleApproval": "single approval", + "agents.forbidden": "forbidden", + "agents.alwaysApprovalReason": "Modifications to agents always require approval.", + "agents.saveRights": "Save Permissions", + "agents.rightsSaved": "Permissions saved.", + "agents.effectAuto": "runs", + "agents.effectApprove": "single approval", + "agents.effectDeny": "denied", + "agents.standingApproval": "Standing approval #{id}", + + "tasks.title": "Tasks", + "tasks.newTask": "New Task", + "tasks.showCompleted": "Show completed", + "tasks.empty": "No tasks found", + "tasks.createTitle": "Create Task", + "tasks.agent": "Agent", + "tasks.taskMessage": "Task description / instruction", + "tasks.schedule": "Schedule", + "tasks.scheduleType": "Schedule Type", + "tasks.scheduleNone": "None (Manual run only)", + "tasks.scheduleCron": "Cron Expression", + "tasks.scheduleInterval": "Interval", + "tasks.scheduleAt": "Specific time", + "tasks.runNow": "Run Now", + "tasks.pause": "Pause", + "tasks.resume": "Resume", + "tasks.delete": "Delete", + "tasks.saved": "Task saved.", + "tasks.deleted": "Task deleted.", + "tasks.runTriggered": "Run triggered.", + + "services.title": "Services", + "services.newService": "New Service", + "services.empty": "No background services configured", + "services.serviceName": "Service Name", + "services.serviceType": "Type", + "services.port": "Port", + "services.active": "Active", + "services.start": "Start", + "services.stop": "Stop", + "services.restart": "Restart", + + "backup.title": "Backup & Restore", + "backup.refresh": "Refresh", + "backup.refreshTooltip": "Reload backups", + "backup.newBackup": "New Backup", + "backup.newBackupTooltip": "Create new backup", + "backup.noInstance": "No instance selected", + "backup.selectInstancePrompt": "Please select an instance in the header above to manage its backups.", + "backup.loading": "Loading backups…", + "backup.backupDir": "Backup Directory", + "backup.backupDirHint": "Backups are fully encrypted with the instance manager's license key and contain credentials. They can be restored on another machine using the same license key.", + "backup.existingBackups": "Existing Backups ({count})", + "backup.noBackups": "No backups exist for this instance yet. Create one using the button above.", + "backup.restore": "Restore", + "backup.restoreTooltip": "Restore this backup", + "backup.deleteTooltip": "Permanently delete backup file", + "backup.archiveFile": "Archive File", + "backup.createdAt": "Created At", + "backup.size": "Size", + "backup.protection": "Protection", + "backup.files": "Files", + "backup.actions": "Actions", + "backup.fileCount": "{count} files", + "backup.foreignLicense": "Foreign License", + "backup.unreadable": "Unreadable", + "backup.licenseProtection": "License ({count} credentials)", + "backup.passphraseProtection": "Passphrase ({count})", + "backup.unencryptedProtection": "Unencrypted, no credentials", + "backup.dialogTitle": "Create New Backup", + "backup.dialogHint": "The backup will be encrypted with the instance manager's license key and contains credentials. If FTP upload is configured, it will be uploaded afterwards.", + "backup.includeChats": "Backup chat history", + "backup.includeLogs": "Include log files", + "backup.createButton": "Create Backup", + "backup.creating": "Creating backup…", + "backup.created": "Backup created.", + "backup.createFailed": "Backup failed: {error}", + "backup.restoreTitle": "Restore Backup: {file}", + "backup.overwriteWarningTitle": "Caution: Overwriting Existing Data", + "backup.overwriteWarningText": "Restoring will overwrite the instance's current configuration and data with the state from {date}.", + "backup.instanceStopNotice": "The instance will be gracefully stopped and restarted afterwards.", + "backup.passphraseLabel": "Passphrase for credentials", + "backup.passphrasePlaceholder": "Backup passphrase…", + "backup.passphraseRequired": "Please enter the passphrase.", + "backup.restoreNow": "Restore Now", + "backup.restoring": "Restoring…", + "backup.restoreSuccess": "Restore completed.", + "backup.restoreFailed": "Restore failed: {error}", + "backup.foreignLicenseTitle": "Source License Key Required", + "backup.sourceLicenseLabel": "Source License Key", + "backup.sourceLicenseHint": "Case and spaces do not matter.", + "backup.sourceLicensePlaceholder": "License key of the installation where the backup originated…", + "backup.restoreWithKey": "Restore with this Key", + "backup.keyMismatch": "This license key does not match the backup.", + "backup.deleteTitle": "Delete Backup", + "backup.deleteConfirm": "Really permanently delete backup file '{file}'?", + "backup.deleted": "Backup deleted.", + "backup.deleteFailed": "Deletion failed: {error}", + "backup.ftpTitle": "Backup FTP Upload", + "backup.ftpLoading": "Loading FTP settings…", + "backup.ftpHint": "Backups are encrypted — anyone reading the server sees nothing usable. However, without TLS, the server password is sent in plain text.", + "backup.ftpAutoUpload": "Automatically upload completed backups", + "backup.ftpServer": "Server", + "backup.ftpPort": "Port", + "backup.ftpUser": "Username", + "backup.ftpPassword": "Password", + "backup.ftpPasswordSaved": "(saved — leave empty to keep unchanged)", + "backup.ftpDirectory": "Target Directory", + "backup.ftpDirectoryHint": "Each instance gets its own subfolder.", + "backup.ftpKeepCount": "Backups to keep per instance", + "backup.ftpKeepCountHint": "Older backups on the server will be deleted.", + "backup.ftpRequireTls": "Connect with TLS only", + "backup.ftpTest": "Test Connection", + "backup.ftpTestFailed": "Test failed: {error}", + "backup.ftpSaved": "Saved.", + "backup.ftpSaveFailed": "Save failed: {error}", + + "logs.title": "Logs", + "logs.connecting": "Connecting…", + "logs.allInstances": "All Instances", + "logs.allModules": "All Modules", + "logs.clear": "Clear", + "logs.pause": "Pause", + "logs.resume": "Resume", + "logs.paused": "Paused", + "logs.autoScroll": "Autoscroll", + "logs.empty": "No log messages yet.", + + "settings.title": "Settings & System Architecture", + "settings.intro": "Configuration overview following the instance manager concept: Strict separation between global manager level (installation) and isolated instance level.", + "settings.managerLevel": "Instance Manager", + "settings.managerBadge": "Manager Level · Global", + "settings.managerIntro": "Applies to all instances and the WebUI. Stored in manager.json in the application root directory.", + "settings.webUiAndNet": "WebUI & Network", + "settings.configFile": "Configuration File", + "settings.webUiPort": "WebUI Port", + "settings.webUiPortDefault": "7000 (default, webUiPort)", + "settings.networkBinding": "Network Binding", + "settings.networkBindingValue": "loopback (127.0.0.1, webUiBinding)", + "settings.accessProtection": "Access Protection", + "settings.tokenActive": "Token active (≥ 20 characters)", + "settings.licenseAndUpdates": "License & Updates", + "settings.licenseScope": "License Scope", + "settings.licenseScopeValue": "Installation level (E-M3: Unlimited instances)", + "settings.licenseKey": "License Key", + "settings.licenseEncrypted": "●●●●-●●●●-●●●●-●●●● (encrypted)", + "settings.updateChannel": "Update Channel", + "settings.updateChannelValue": "prod (Official release versions)", + "settings.updateCheck": "Update Check", + "settings.updateCheckValue": "Central at manager level", + "settings.dcAndOversight": "Deploymentcenter & Oversight", + "settings.dcMasterToken": "Master Token", + "settings.dcMasterTokenValue": "●●●●●●●● (generates sub-tokens per instance)", + "settings.desktopWindows": "Desktop Windows", + "settings.desktopWindowsValue": "Active (each instance has its own window)", + "settings.crashProtection": "Crash Protection", + "settings.crashProtectionValue": "Max 5 restarts / 10 minutes", + "settings.instanceSectionTitle": "Active Instance: {name}", + "settings.instanceSectionTitleNone": "Instance-Related Settings", + "settings.instanceBadge": "Instance Level · Isolated", + "settings.noInstanceActive": "No instance active", + "settings.instanceIntro": "Applies exclusively to the currently selected instance. All paths, logs, agents and budgets are strictly isolated from other instances.", + "settings.noInstanceSelected": "No instance selected", + "settings.noInstancePrompt": "Please select an active instance in the header above or switch to 'Instances' to start or select an instance.", + "settings.toInstancesOverview": "To Instances Overview", + "settings.identityAndProcess": "Identity & Process", + "settings.instanceName": "Instance Name", + "settings.instanceId": "Instance ID", + "settings.autostartByManager": "Autostart by Manager", + "settings.autostartActive": "Active (keep running)", + "settings.autostartInactive": "Inactive (manual start)", + "settings.communication": "Communication", + "settings.communicationValue": "HTTP over Named Pipe (Windows) / Socket (Linux)", + "settings.isolatedPaths": "Isolated Directories", + "settings.logsPath": "Logs", + "settings.backupsPath": "Backups", + "settings.stateDbPath": "State Database", + "settings.stateDbValue": "Instances/{name}/state.db (exclusive)", + "settings.workspacePath": "Shared Workspace", + "settings.openRouterCard": "Model Integration (OpenRouter)", + "settings.openRouterStatus": "OpenRouter Status", + "settings.connected": "Connected", + "settings.offline": "Offline", + "settings.statusMessage": "Status Message", + "settings.credits": "Credits", + "settings.apiKey": "API Key", + "settings.apiKeyMasked": "Masked in InstanceSettings.json of this instance", + "settings.openRouterNotRunning": "Instance is not running. Data will be loaded after start.", + "settings.openRouterNoKey": "No key configured or service unreachable.", + "settings.runtimeAndSubsystems": "Runtime & Subsystems", + "settings.agentEngine": "Agent Engine", + "settings.scheduler": "Scanner / Scheduler", + "settings.stagingService": "Approvals (Staging)", + "settings.dcSubToken": "Deploymentcenter Sub-Token", + "settings.subsystemsLoading": "Querying runtime data…", + "settings.subsystemsStopped": "Instance stopped — subsystems idle.", + "settings.save": "Save Settings", + "settings.discard": "Discard Changes", + "settings.saved": "Settings saved successfully.", + "settings.saveError": "Failed to save settings: {error}", + + "info.title": "System & Status", + "info.noInstanceSelected": "No instance selected", + "info.noInstanceHint": "Select an active instance in the header above or switch to 'Instances' to start one.", + "info.loading": "Loading system data…", + "info.instanceAndVersion": "Instance & Version", + "info.instanceName": "Instance Name", + "info.instanceId": "Instance ID", + "info.version": "Version", + "info.build": "Build", + "info.environment": "Environment", + "info.dataDirectory": "Data Directory", + "info.subsystems": "Subsystems", + "info.openRouterStatus": "OpenRouter & Credits", + + "instances.title": "Instance Management", + "instances.refresh": "Refresh", + "instances.newInstance": "New Instance", + "instances.activeBadge": "Active in WebUI", + "instances.selectActive": "Select as active", + "instances.start": "Start", + "instances.stop": "Stop", + "instances.bringToFront": "Bring window to front", + "instances.createTitle": "Create New Instance", + "instances.instanceOfflineError": "The instance is not reachable.", + "instances.noInstanceActiveError": "No instance is active. Start or select one under 'Instances'.", + "instances.instanceNameLabel": "Instance Name", + "instances.instanceNamePlaceholder": "e.g. production-1", + "instances.dailyBudgetUsd": "Daily Budget ($ USD)", + "instances.dailyBudgetTokens": "Daily Budget (Tokens)", + "instances.autostart": "Autostart with Manager", + "instances.created": "Instance created.", + "instances.createError": "Failed to create instance: {error}", + + "staging.title": "Approvals", + "staging.refresh": "Refresh", + "staging.loading": "Loading approvals…", + "staging.empty": "No open approvals", + "staging.emptyHint": "Currently no tool calls are waiting for manual approval.", + "staging.approve": "Approve", + "staging.reject": "Reject", + "staging.callId": "Call ID", + "staging.agent": "Agent", + "staging.tool": "Tool", + "staging.time": "Timestamp", + "staging.frozenArgs": "Frozen Arguments:", + "staging.suggestHint": "This operation by {agent} was approved {approvals} times in the last 14 days. Should {tool}.{action} run without confirmation for this agent in the future?", + "staging.allowSimilar": "Allow similar in future: {agent} may run {tool}.{action} without confirmation from now on (revocable below under 'Standing Approvals').", + "staging.notActive": "Staging not active", + "staging.notActiveHint": "No staging service configured in this instance.", + "staging.approved": "Call approved{similar}: {message}", + "staging.similarSuffix": " and allowed for the future", + "staging.executedSuccess": "Executed successfully", + "staging.approveFailed": "Approval failed: {error}", + "staging.standingTitle": "Standing Approvals", + "staging.standingEmpty": "None. When approving, 'Allow similar in future' can be selected; permanent permissions are in the agent's tool config.", + "staging.granted": "Granted", + "staging.occasion": "Occasion", + "staging.revoke": "Revoke", + "staging.revokeConfirm": "Revoke standing approval? {agent} will have to submit {tool}.{action} individually again.", + "staging.revoked": "Revoked.", + "staging.revokeFailed": "Revocation failed: {error}", + "staging.rejectTitle": "Reject Tool Call", + "staging.rejectReasonLabel": "Reason (optional):", + "staging.rejectReasonPlaceholder": "Reason for rejection to the agent…", + "staging.rejectButton": "Reject Call", + "staging.rejected": "Call was rejected.", + "staging.rejectFailed": "Rejection failed: {error}", + + "agentChats.title": "Agent Communication (A2A)", + "agentChats.allAgents": "All Agents", + "agentChats.filterTooltip": "Filter by participating agent (sender or receiver)", + "agentChats.fromTooltip": "Messages from this day", + "agentChats.toTooltip": "Messages up to and including this day", + "agentChats.searchPlaceholder": "Fulltext…", + "agentChats.searchTooltip": "Search message text and agent names", + "agentChats.refresh": "Refresh", + "agentChats.refreshTooltip": "Reload communication", + "agentChats.from": "from", + "agentChats.to": "to", + "agentChats.noInstance": "No instance selected", + "agentChats.selectInstancePrompt": "Please select an instance in the header above to view its agent communication.", + "agentChats.infoTitle": "Internal Communication", + "agentChats.infoHint": "Direct message exchanges between agents are logged here when agents communicate via the AgentComm tool (action send_message).", + "agentChats.loading": "Loading communication history…", + "agentChats.matchCount": "{filtered} of {total} messages", + "agentChats.totalCount": "{total} messages", + "agentChats.empty": "No Messages", + "agentChats.emptyFilter": "No internal communication matches these filters.", + "agentChats.emptyInstance": "No internal agent-to-agent communication has been logged in this instance yet.", + "agentChats.replyTo": "➔ Reply to", + "agentChats.arrow": "➔", + "agentChats.loadFailed": "Failed to load communication: {error}", + + "usage.title": "Token Usage & Analysis", + "usage.loading": "Loading usage data…", + "usage.empty": "No Data", + "usage.emptyHint": "No token usage data recorded yet.", + "usage.totalCost": "Total Cost ({runs} runs)", + "usage.totalTokens": "Total Tokens", + "usage.inputTokens": "Input Tokens (Prompt)", + "usage.outputTokens": "Output Tokens (Completion)", + "usage.unknownCostRuns": "{count} run(s) without known price — their costs are not included in the total.", + "usage.tokensByAgent": "Tokens by Agent", + "usage.tokensByModel": "Tokens by Model", + "usage.recentRuns": "Recent Agent Runs", + "usage.time": "Timestamp", + "usage.agent": "Agent", + "usage.model": "Model", + "usage.status": "Status", + "usage.prompt": "Prompt", + "usage.completion": "Completion", + "usage.total": "Total", + "usage.cost": "Cost", + "usage.unknownPrice": "(price unknown)", + "usage.notAvailable": "Usage data not available", + "usage.forecastTitle": "Cost Forecast", + "usage.forecastDay": "Forecast per day", + "usage.forecastMonth": "Forecast per month (30 days)", + "usage.tasksDay": "thereof tasks per day", + "usage.otherDay": "other runs per day (Ø 7 days)", + "usage.budgetWarning": "{agent}: Planned tasks are forecast to cost {cost} per day, daily budget allows {budget}. Later runs of a day will be rejected.", + "usage.withoutBasis": "{count} task(s) without basis (no run, no model price) — not included in total.", + "usage.oneOffOpen": "Open one-off tasks cost approximately an additional {cost}.", + "usage.basisHint": "Basis per task: average of its recent runs, otherwise agent's average, otherwise an estimate from prompt length and list price (tends to be high).", + "usage.task": "Task", + "usage.runsDay": "Runs/Day", + "usage.perRun": "per Run", + "usage.basis": "Basis", + "usage.perDay": "per Day", + "usage.basisRuns": "Ø last {n} runs", + "usage.basisAgent": "Ø of agent ({n} runs)", + "usage.basisEstimate": "Estimate – no runs yet", + "usage.basisUnknown": "no basis", "filter.range": "Period", "filter.range.24h": "Last 24 hours", @@ -137,5 +627,57 @@ "history.settings.effective.days": "kept {n} days", "history.settings.save": "Save retention", "history.settings.saved": "Retention saved.", - "history.settings.failed": "Saving failed: {error}" + "history.settings.failed": "Saving failed: {error}", + + "desktop.mainWindow.title": "ClawdDotNet", + "desktop.mainWindow.sidebarTooltip": "Toggle sidebar", + "desktop.mainWindow.switchInstance": "Switch instance", + "desktop.mainWindow.instance": "INSTANCE", + "desktop.mainWindow.themeLight": "Light Mode", + "desktop.mainWindow.themeDark": "Dark Mode", + "desktop.mainWindow.openInstanceFolder": "Open Instance Folder", + "desktop.mainWindow.instanceManager": "Instance Manager", + "desktop.mainWindow.exit": "Exit", + "desktop.nav.work": "Work", + "desktop.nav.analytics": "Analytics", + "desktop.nav.system": "System", + "desktop.status.unknown": "unknown", + "desktop.status.design": "Design", + "desktop.status.noApiKey": "No API Key", + "desktop.status.noUsageData": "No usage data", + "desktop.settings.discard": "Discard", + "desktop.settings.save": "Save", + "desktop.settings.instance": "INSTANCE", + "desktop.settings.instanceName": "Instance Name", + "desktop.settings.apiKey": "OpenRouter API Key", + "desktop.settings.autostart": "Autostart by Manager", + "desktop.settings.autostartHint": "The instance manager starts this instance on launch and restarts it if it crashes.", + "desktop.settings.logDir": "Log Directory", + "desktop.settings.logDirHint": "Applies only to this instance. Empty means: Logs subfolder in instance directory.", + "desktop.settings.dailyCost": "Daily Budget ($ USD)", + "desktop.settings.dailyTokens": "Daily Budget (Tokens)", + "desktop.settings.general": "GENERAL", + "desktop.settings.instancesDir": "Instances Directory", + "desktop.settings.minLogLevel": "Minimum Log Level", + "desktop.settings.openRouterUrl": "OpenRouter Base URL", + "desktop.settings.language": "Language", + "desktop.settings.dcAndLicense": "DEPLOYMENTCENTER & LICENSE", + "desktop.settings.dcUrl": "Server URL", + "desktop.settings.dcToken": "Master Token", + "desktop.settings.dcEnv": "Environment", + "desktop.settings.errorReporting": "Automatically report errors", + "desktop.settings.updateCheck": "Check for updates on startup", + "desktop.settings.updateChannel": "Update Channel", + "desktop.settings.licenseKey": "License Key", + "desktop.chat.inputPlaceholder": "Type message... (Enter to send, Shift+Enter for new line)", + "desktop.chat.send": "Send", + "desktop.chat.statusReady": "Ready", + "desktop.chat.statusSending": "Sending…", + "desktop.chat.statusDesign": "Design Mode", + "desktop.dialog.addAgent": "Add Agent", + "desktop.dialog.addJob": "Add Job", + "desktop.dialog.addService": "Add Service", + "desktop.dialog.restoreBackup": "Restore Backup", + "desktop.dialog.license": "License Activation", + "desktop.dialog.instancePicker": "Choose Instance" } diff --git a/src/ClawdDotNet.Manager.Web/wwwroot/index.html b/src/ClawdDotNet.Manager.Web/wwwroot/index.html index e9e52a8..0f99b2b 100644 --- a/src/ClawdDotNet.Manager.Web/wwwroot/index.html +++ b/src/ClawdDotNet.Manager.Web/wwwroot/index.html @@ -1,5 +1,5 @@ - + @@ -14,12 +14,12 @@

ClawdDotNet

-

- Bitte gib das Zugangstoken des Instanzmanagers ein (webUiToken in manager.json). +

+ Please enter the instance manager access token (webUiToken in manager.json).

- - + +
@@ -33,7 +33,7 @@