From 350a89f364a2aa75ce297cf35f5658ec739f5d6e Mon Sep 17 00:00:00 2001 From: mika kuns Date: Wed, 3 Jun 2026 12:43:30 +0200 Subject: [PATCH] feat(i18n): localize ViewModel-built strings via ambient Loc accessor Co-Authored-By: Claude Sonnet 4.6 --- src/ClaudeDo.App/Program.cs | 1 + src/ClaudeDo.Localization/locales/en.json | 21 ++++++ src/ClaudeDo.Ui/Localization/Loc.cs | 43 +++++++++++ .../Islands/DetailsIslandViewModel.cs | 73 ++++++++++++++----- .../Islands/ListsIslandViewModel.cs | 16 ++-- .../ViewModels/Islands/TaskRowViewModel.cs | 29 ++++++-- .../Islands/TasksIslandViewModel.cs | 14 +++- .../ViewModels/IslandsShellViewModel.cs | 9 ++- .../ViewModels/Modals/DiffModalViewModel.cs | 7 +- .../Modals/ListSettingsModalViewModel.cs | 5 +- .../ViewModels/Modals/MergeModalViewModel.cs | 17 +++-- .../Settings/FilesSettingsTabViewModel.cs | 13 ++-- .../Settings/WorktreesSettingsTabViewModel.cs | 9 ++- .../Modals/SettingsModalViewModel.cs | 5 +- .../Modals/WeeklyReportModalViewModel.cs | 7 +- .../Modals/WorktreesOverviewModalViewModel.cs | 15 ++-- .../Planning/ConflictResolutionViewModel.cs | 5 +- .../Planning/PlanningDiffViewModel.cs | 5 +- .../Views/Islands/DetailsIslandView.axaml | 4 +- .../Views/Islands/ListsIslandView.axaml | 8 +- .../Planning/ConflictResolutionView.axaml | 4 +- .../ViewModels/PlanningDiffViewModelTests.cs | 12 +++ .../UiVm/TaskRowViewModelPlanningTests.cs | 12 +++ 23 files changed, 250 insertions(+), 84 deletions(-) create mode 100644 src/ClaudeDo.Ui/Localization/Loc.cs diff --git a/src/ClaudeDo.App/Program.cs b/src/ClaudeDo.App/Program.cs index 1b85046..ef1f321 100644 --- a/src/ClaudeDo.App/Program.cs +++ b/src/ClaudeDo.App/Program.cs @@ -86,6 +86,7 @@ sealed class Program fallback: "en"); var localizer = new Localizer(localeStore, initialLang); TrExtension.Localizer = localizer; + ClaudeDo.Ui.Localization.Loc.Current = localizer; sc.AddSingleton(localizer); sc.AddDbContextFactory(opt => opt.UseSqlite($"Data Source={dbPath}")); diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json index 28acb73..b35a594 100644 --- a/src/ClaudeDo.Localization/locales/en.json +++ b/src/ClaudeDo.Localization/locales/en.json @@ -302,5 +302,26 @@ "updateNow": "Update now", "dismiss": "Dismiss" } + }, + "vm": { + "connection": { "online": "Online", "connecting": "Connecting…", "offline": "Offline" }, + "shell": { "restartingWorker": "Restarting worker…" }, + "agentStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "done": "Done", "failed": "Failed", "cancelled": "Cancelled" }, + "taskStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "waitingForReview": "Waiting for Review", "done": "Done", "failed": "Failed", "cancelled": "Cancelled" }, + "planningBadge": { "active": "PLANNING", "finalized": "PLANNED" }, + "taskRow": { "createdPrefix": "Created {0}", "stepsText": "{0}/{1} steps" }, + "tasksIsland": { "completedHeader": "COMPLETED", "completedHeaderCount": "COMPLETED · {0}" }, + "diff": { "loadFailed": "Failed to load diff: {0}", "noChanges": "No changes to show." }, + "planningDiff": { "hubError": "Could not build combined preview (hub error).", "conflict": "Cannot build combined preview: subtask {0} conflicts with an earlier subtask ({1} files)." }, + "merge": { "commitMessage": "Merge task: {0}", "workerOfflineBranches": "Worker offline — cannot list branches.", "loadBranchesFailed": "Failed to load branches: {0}", "merged": "Merged.", "conflict": "Merge conflict — target branch restored. Resolve manually or via Continue, then retry.", "blocked": "Blocked: {0}", "unknownStatus": "Unknown status: {0}", "mergeFailed": "Merge failed: {0}" }, + "conflictResolution": { "vsCodeError": "Could not launch VS Code: {0}. Paths are listed above — copy them manually.", "subtaskPrefix": "Conflicts in subtask: {0}", "targetPrefix": "Merging into: {0}" }, + "settingsModal": { "workerOffline": "Worker offline — settings read-only.", "saveFailed": "Save failed: {0}" }, + "weeklyReport": { "invalidRange": "Invalid date range.", "generating": "Generating report…", "error": "Error: {0}" }, + "filesTab": { "workerOffline": "Worker offline.", "noneBundled": "No default agents bundled.", "allPresent": "All default agents already present.", "restored": "Restored {0} default agent(s).", "restoreFailed": "Restore failed: {0}", "openFailed": "Open failed: {0}" }, + "worktreesTab": { "workerOffline": "Worker offline.", "removed": "Removed {0} worktree(s).", "blocked": "Cannot force-remove: {0} task(s) still running. Cancel them first.", "removedFrom": "Removed {0} worktree(s) from {1} task(s)." }, + "worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "list", "cleanupFailed": "Cleanup failed.", "removed": "Removed {0} worktree(s).", "discardFailed": "Failed to discard worktree.", "keepFailed": "Failed to keep worktree.", "cannotForceRunning": "Cannot force-remove a running task.", "forceRemoveFailed": "Force remove failed." }, + "listSettings": { "untitled": "Untitled" }, + "details": { "effectiveIfInherited": "Effective if inherited: {0}" }, + "lists": { "localSuffix": "{0} / local", "smartMyDay": "My Day", "smartImportant": "Important", "smartPlanned": "Planned", "virtualQueue": "Queue", "virtualRunning": "Running", "virtualReview": "Review", "newList": "New list" } } } diff --git a/src/ClaudeDo.Ui/Localization/Loc.cs b/src/ClaudeDo.Ui/Localization/Loc.cs new file mode 100644 index 0000000..9570ffd --- /dev/null +++ b/src/ClaudeDo.Ui/Localization/Loc.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using ClaudeDo.Localization; + +namespace ClaudeDo.Ui.Localization; + +/// Ambient access to the active localizer for code-built (ViewModel) strings. +/// Set once at startup. Defaults to a key-echo localizer so unit tests that +/// construct ViewModels without startup wiring do not crash. +public static class Loc +{ + private static ILocalizer _current = new KeyEchoLocalizer(); + + public static ILocalizer Current + { + get => _current; + set + { + if (_current is not null) _current.LanguageChanged -= OnInnerChanged; + _current = value; + _current.LanguageChanged += OnInnerChanged; + OnInnerChanged(value, EventArgs.Empty); + } + } + + public static event EventHandler? LanguageChanged; + + private static void OnInnerChanged(object? sender, EventArgs e) => + LanguageChanged?.Invoke(sender, e); + + public static string T(string key) => Current[key]; + public static string T(string key, params object[] args) => Current.Get(key, args); + + private sealed class KeyEchoLocalizer : ILocalizer + { + public string this[string key] => key; + public string Get(string key, params object[] args) => key; + public string CurrentCode => "en"; + public IReadOnlyList AvailableLanguages => Array.Empty(); + public void SetLanguage(string code) { } + public event EventHandler? LanguageChanged { add { } remove { } } + } +} diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs index 93c0f92..2d2a537 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs @@ -6,6 +6,7 @@ using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Ui.Helpers; +using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services.Interfaces; using ClaudeDo.Ui.ViewModels.Modals; @@ -99,13 +100,14 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase [NotifyCanExecuteChangedFor(nameof(DequeueCommand))] [NotifyCanExecuteChangedFor(nameof(ResetAndRetryCommand))] [NotifyCanExecuteChangedFor(nameof(ContinueCommand))] - private string _agentStatusLabel = "Idle"; - public bool IsIdle => AgentStatusLabel == "Idle"; - public bool IsQueued => AgentStatusLabel == "Queued"; - public bool IsRunning => AgentStatusLabel == "Running"; - public bool IsDone => AgentStatusLabel == "Done"; - public bool IsFailed => AgentStatusLabel == "Failed"; - public bool IsCancelled => AgentStatusLabel == "Cancelled"; + private string _agentState = "idle"; + public string AgentStatusLabel => Loc.T($"vm.agentStatus.{AgentState}"); + public bool IsIdle => AgentState == "idle"; + public bool IsQueued => AgentState == "queued"; + public bool IsRunning => AgentState == "running"; + public bool IsDone => AgentState == "done"; + public bool IsFailed => AgentState == "failed"; + public bool IsCancelled => AgentState == "cancelled"; // Recovery actions: Continue (resume session) for Failed/Cancelled. public bool ShowContinue => IsFailed || IsCancelled; @@ -116,8 +118,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase [NotifyCanExecuteChangedFor(nameof(ContinueCommand))] private string? _latestRunSessionId; - partial void OnAgentStatusLabelChanged(string value) + partial void OnAgentStateChanged(string value) { + OnPropertyChanged(nameof(AgentStatusLabel)); OnPropertyChanged(nameof(IsIdle)); OnPropertyChanged(nameof(IsQueued)); OnPropertyChanged(nameof(IsRunning)); @@ -127,6 +130,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase OnPropertyChanged(nameof(ShowContinue)); OnPropertyChanged(nameof(ShowResetAndRetry)); OnPropertyChanged(nameof(IsAgentSectionEnabled)); + OnPropertyChanged(nameof(EffectiveModelLabel)); + OnPropertyChanged(nameof(EffectiveAgentLabel)); } [ObservableProperty] private string? _model; @@ -139,6 +144,12 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase [ObservableProperty] private string _effectiveSystemPromptHint = ""; [ObservableProperty] private string _effectiveAgentHint = ""; + public string EffectiveModelLabel => Loc.T("vm.details.effectiveIfInherited", EffectiveModelHint); + public string EffectiveAgentLabel => Loc.T("vm.details.effectiveIfInherited", EffectiveAgentHint); + + partial void OnEffectiveModelHintChanged(string value) => OnPropertyChanged(nameof(EffectiveModelLabel)); + partial void OnEffectiveAgentHintChanged(string value) => OnPropertyChanged(nameof(EffectiveAgentLabel)); + public System.Collections.ObjectModel.ObservableCollection TaskModelOptions { get; } = new( new[] { ModelRegistry.TaskInheritSentinel }.Concat(ModelRegistry.Aliases)); @@ -223,6 +234,26 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase // Set by the view so DeleteTaskCommand can show an error message public Func? ShowErrorAsync { get; set; } + private static string StatusToStateKey(ClaudeDo.Data.Models.TaskStatus status) => status switch + { + ClaudeDo.Data.Models.TaskStatus.Queued => "queued", + ClaudeDo.Data.Models.TaskStatus.Running => "running", + ClaudeDo.Data.Models.TaskStatus.WaitingForReview => "running", + ClaudeDo.Data.Models.TaskStatus.Done => "done", + ClaudeDo.Data.Models.TaskStatus.Failed => "failed", + ClaudeDo.Data.Models.TaskStatus.Cancelled => "cancelled", + _ => "idle", + }; + + private static string FinishedStatusToStateKey(string status) => status switch + { + "done" => "done", + "failed" => "failed", + "cancelled" => "cancelled", + "waiting_for_review" => "running", + _ => status.ToLowerInvariant(), + }; + private async System.Threading.Tasks.Task RefreshStatusAsync(string taskId) { try @@ -233,7 +264,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase .FirstOrDefaultAsync(t => t.Id == taskId); if (entity is null || Task?.Id != taskId) return; - AgentStatusLabel = entity.Status.ToString(); + AgentState = StatusToStateKey(entity.Status); } catch { } } @@ -245,6 +276,12 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase _services = services; _notesApi = notesApi; Notes = new NotesEditorViewModel(_notesApi); + Loc.LanguageChanged += (_, _) => + { + OnPropertyChanged(nameof(AgentStatusLabel)); + OnPropertyChanged(nameof(EffectiveModelLabel)); + OnPropertyChanged(nameof(EffectiveAgentLabel)); + }; // Subscribe once; filter by current task id inside the handler _worker.TaskMessageEvent += OnTaskMessage; @@ -264,7 +301,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase // If the task row's live status changes (e.g. TaskStarted/Finished), mirror it. _worker.TaskStartedEvent += (slot, taskId, startedAt) => { - if (Task?.Id == taskId) AgentStatusLabel = "Running"; + if (Task?.Id == taskId) AgentState = "running"; }; _worker.TaskFinishedEvent += (slot, taskId, status, finishedAt) => { @@ -275,7 +312,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase Kind = LogKind.Done, Text = $"── {status.ToUpperInvariant()} · {finishedAt.ToLocalTime():HH:mm:ss} ──", }); - AgentStatusLabel = status; + AgentState = FinishedStatusToStateKey(status); // Re-query to pick up worktree created during the run. _ = RefreshWorktreeAsync(taskId); }; @@ -473,7 +510,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase WorktreePath = null; WorktreeStateLabel = null; BranchLine = null; - AgentStatusLabel = "Idle"; + AgentState = "idle"; LatestRunSessionId = null; _suppressAgentSave = true; try @@ -519,7 +556,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase WorktreeBaseCommit = entity.Worktree?.BaseCommit; WorktreeStateLabel = entity.Worktree?.State.ToString(); BranchLine = entity.Worktree is { } w ? $"{w.BranchName} \u2190 main" : null; - AgentStatusLabel = entity.Status.ToString(); + AgentState = StatusToStateKey(entity.Status); await LoadAgentSettingsAsync(entity, ct); ct.ThrowIfCancellationRequested(); @@ -730,7 +767,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase WorktreeBaseCommit = entity.Worktree?.BaseCommit; WorktreeStateLabel = entity.Worktree?.State.ToString(); BranchLine = entity.Worktree is { } w ? $"{w.BranchName} ← main" : null; - AgentStatusLabel = entity.Status.ToString(); + AgentState = StatusToStateKey(entity.Status); if (Task is { } row && entity.Worktree?.DiffStat is { } stat) row.DiffStat = stat; } @@ -808,7 +845,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase ? ClaudeDo.Data.Models.TaskStatus.Done : ClaudeDo.Data.Models.TaskStatus.Idle; Task.Status = entity.Status; - AgentStatusLabel = entity.Status.ToString(); + AgentState = StatusToStateKey(entity.Status); await repo.UpdateAsync(entity); } @@ -922,7 +959,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase try { await _worker.SetTaskStatusAsync(Task.Id, ClaudeDo.Data.Models.TaskStatus.Queued); - AgentStatusLabel = "Queued"; + AgentState = "queued"; } catch { /* offline */ } } @@ -938,7 +975,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase try { await _worker.SetTaskStatusAsync(Task.Id, ClaudeDo.Data.Models.TaskStatus.Idle); - AgentStatusLabel = "Idle"; + AgentState = "idle"; } catch { /* offline */ } } @@ -973,7 +1010,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase try { await _worker.SetTaskStatusAsync(Task.Id, ClaudeDo.Data.Models.TaskStatus.Queued); - AgentStatusLabel = "Queued"; + AgentState = "queued"; } catch { /* offline */ } } diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs index f5ac544..cf9b11b 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs @@ -6,6 +6,7 @@ using ClaudeDo.Data.Filtering; using ClaudeDo.Data.Models; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; using ClaudeDo.Data.Repositories; +using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels.Modals; using Microsoft.EntityFrameworkCore; @@ -137,6 +138,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase public string UserName { get; } = Environment.UserName; public string MachineName { get; } = Environment.MachineName; + public string MachineNameLocal => Loc.T("vm.lists.localSuffix", MachineName); public string UserInitials { get; } public ListsIslandViewModel(IDbContextFactory dbFactory, IServiceProvider? services = null, WorkerClient? worker = null) @@ -170,12 +172,12 @@ public sealed partial class ListsIslandViewModel : ViewModelBase var smart = new[] { - new ListNavItemViewModel { Id = "smart:my-day", Name = "My Day", Kind = ListKind.Smart, IconKey = "Sun" }, - new ListNavItemViewModel { Id = "smart:important", Name = "Important", Kind = ListKind.Smart, IconKey = "Star" }, - new ListNavItemViewModel { Id = "smart:planned", Name = "Planned", Kind = ListKind.Smart, IconKey = "Calendar" }, - new ListNavItemViewModel { Id = "virtual:queued", Name = "Queue", Kind = ListKind.Virtual, IconKey = "Inbox" }, - new ListNavItemViewModel { Id = "virtual:running", Name = "Running", Kind = ListKind.Virtual, IconKey = "Activity" }, - new ListNavItemViewModel { Id = "virtual:review", Name = "Review", Kind = ListKind.Virtual, IconKey = "Eye" }, + new ListNavItemViewModel { Id = "smart:my-day", Name = Loc.T("vm.lists.smartMyDay"), Kind = ListKind.Smart, IconKey = "Sun" }, + new ListNavItemViewModel { Id = "smart:important", Name = Loc.T("vm.lists.smartImportant"), Kind = ListKind.Smart, IconKey = "Star" }, + new ListNavItemViewModel { Id = "smart:planned", Name = Loc.T("vm.lists.smartPlanned"), Kind = ListKind.Smart, IconKey = "Calendar" }, + new ListNavItemViewModel { Id = "virtual:queued", Name = Loc.T("vm.lists.virtualQueue"), Kind = ListKind.Virtual, IconKey = "Inbox" }, + new ListNavItemViewModel { Id = "virtual:running", Name = Loc.T("vm.lists.virtualRunning"), Kind = ListKind.Virtual, IconKey = "Activity" }, + new ListNavItemViewModel { Id = "virtual:review", Name = Loc.T("vm.lists.virtualReview"), Kind = ListKind.Virtual, IconKey = "Eye" }, }; foreach (var s in smart) { Items.Add(s); SmartLists.Add(s); } @@ -242,7 +244,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase var entity = new ListEntity { Id = Guid.NewGuid().ToString("N"), - Name = "New list", + Name = Loc.T("vm.lists.newList"), DefaultCommitType = CommitTypeRegistry.DefaultType, CreatedAt = DateTime.UtcNow, }; diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs index f9ae5a4..fde68a3 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs @@ -1,5 +1,6 @@ using CommunityToolkit.Mvvm.ComponentModel; using ClaudeDo.Data.Models; +using ClaudeDo.Ui.Localization; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Ui.ViewModels.Islands; @@ -31,7 +32,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase [ObservableProperty] private bool _parentFinalized; public DateTime CreatedAt { get; init; } - public string CreatedAtFormatted => CreatedAt == default ? "—" : $"Created {CreatedAt:MMM d}"; + public string CreatedAtFormatted => CreatedAt == default ? "—" : Loc.T("vm.taskRow.createdPrefix", CreatedAt.ToString("MMM d")); public int StepsCount { get; init; } public int StepsCompleted { get; init; } @@ -50,8 +51,8 @@ public sealed partial class TaskRowViewModel : ViewModelBase public string? PlanningBadge => PlanningPhase switch { - PlanningPhase.Active => "PLANNING", - PlanningPhase.Finalized => "PLANNED", + PlanningPhase.Active => Loc.T("vm.planningBadge.active"), + PlanningPhase.Finalized => Loc.T("vm.planningBadge.finalized"), _ => null, }; @@ -77,9 +78,19 @@ public sealed partial class TaskRowViewModel : ViewModelBase public string DiffAdditionsText => $"+{DiffAdditions}"; public string DiffDeletionsText => $"−{DiffDeletions}"; - public string StepsText => $"{StepsCompleted}/{StepsCount} steps"; + public string StepsText => Loc.T("vm.taskRow.stepsText", StepsCompleted, StepsCount); - public string StatusLabel => Status == TaskStatus.WaitingForReview ? "Waiting for Review" : Status.ToString(); + public string StatusLabel => Status switch + { + TaskStatus.Idle => Loc.T("vm.taskStatus.idle"), + TaskStatus.Queued => Loc.T("vm.taskStatus.queued"), + TaskStatus.Running => Loc.T("vm.taskStatus.running"), + TaskStatus.WaitingForReview => Loc.T("vm.taskStatus.waitingForReview"), + TaskStatus.Done => Loc.T("vm.taskStatus.done"), + TaskStatus.Failed => Loc.T("vm.taskStatus.failed"), + TaskStatus.Cancelled => Loc.T("vm.taskStatus.cancelled"), + _ => Status.ToString(), + }; public string StatusChipClass => (Status, IsBlocked: !string.IsNullOrEmpty(BlockedByTaskId)) switch { @@ -164,6 +175,14 @@ public sealed partial class TaskRowViewModel : ViewModelBase partial void OnDiffAdditionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffAdditionsText)); } partial void OnDiffDeletionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffDeletionsText)); } + public void RefreshLocalized() + { + OnPropertyChanged(nameof(StatusLabel)); + OnPropertyChanged(nameof(PlanningBadge)); + OnPropertyChanged(nameof(CreatedAtFormatted)); + OnPropertyChanged(nameof(StepsText)); + } + public static TaskRowViewModel FromEntity(TaskEntity t) { var row = new TaskRowViewModel { Id = t.Id, CreatedAt = t.CreatedAt }; diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs index c613298..ac2bdd6 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs @@ -6,6 +6,7 @@ using ClaudeDo.Data; using ClaudeDo.Data.Filtering; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; +using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels.Modals; using Microsoft.EntityFrameworkCore; @@ -52,7 +53,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase [ObservableProperty] private bool _hasOpen; [ObservableProperty] private bool _hasCompleted; [ObservableProperty] private bool _showOpenLabel; - [ObservableProperty] private string _completedHeader = "COMPLETED"; + [ObservableProperty] private string _completedHeader = ""; [ObservableProperty] private bool _showNotesRow; public Func? ShowUnfinishedPlanningModal { get; set; } @@ -61,6 +62,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase { _dbFactory = dbFactory; _worker = worker; + CompletedHeader = Loc.T("vm.tasksIsland.completedHeader"); if (_worker is not null) { _worker.TaskUpdatedEvent += OnWorkerTaskUpdated; @@ -68,6 +70,14 @@ public sealed partial class TasksIslandViewModel : ViewModelBase _worker.ListUpdatedEvent += OnWorkerListUpdated; _worker.ConnectionRestoredEvent += () => LoadForList(_currentList); } + Loc.LanguageChanged += (_, _) => RefreshLocalizedText(); + } + + private void RefreshLocalizedText() + { + CompletedHeader = Loc.T("vm.tasksIsland.completedHeader"); + foreach (var row in Items) row.RefreshLocalized(); + foreach (var row in CompletedItems) row.RefreshLocalized(); } private async void OnWorkerListUpdated(string listId) @@ -340,7 +350,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase HasOpen = OpenItems.Count > 0; HasCompleted = CompletedItems.Count > 0; ShowOpenLabel = HasOpen && HasOverdue; - CompletedHeader = $"COMPLETED · {CompletedItems.Count}"; + CompletedHeader = Loc.T("vm.tasksIsland.completedHeaderCount", CompletedItems.Count); } private void UpdateSubtitle() diff --git a/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs b/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs index ffc68c6..28a6411 100644 --- a/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs @@ -6,6 +6,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using ClaudeDo.Data; using ClaudeDo.Data.Models; +using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels.Islands; using ClaudeDo.Ui.ViewModels.Modals; @@ -23,9 +24,9 @@ public sealed partial class IslandsShellViewModel : ViewModelBase public UpdateCheckService UpdateCheck => _updateCheck; public string ConnectionText => - Worker?.IsConnected == true ? "Online" - : Worker?.IsReconnecting == true ? "Connecting…" - : "Offline"; + Worker?.IsConnected == true ? Loc.T("vm.connection.online") + : Worker?.IsReconnecting == true ? Loc.T("vm.connection.connecting") + : Loc.T("vm.connection.offline"); public bool IsOffline => Worker?.IsConnected != true && Worker?.IsReconnecting != true; @@ -358,7 +359,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase [RelayCommand] private async Task RestartWorkerAsync() { - RestartWorkerStatus = "Restarting worker…"; + RestartWorkerStatus = Loc.T("vm.shell.restartingWorker"); try { await Task.Run(RestartWorkerService); diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs index 6476da5..13ad168 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using ClaudeDo.Data.Git; +using ClaudeDo.Ui.Localization; namespace ClaudeDo.Ui.ViewModels.Modals; @@ -91,13 +92,13 @@ public sealed partial class DiffModalViewModel : ViewModelBase } catch (Exception ex) { - StatusMessage = $"Failed to load diff: {ex.Message}"; + StatusMessage = Loc.T("vm.diff.loadFailed", ex.Message); return; } if (string.IsNullOrWhiteSpace(raw)) { - StatusMessage = "No changes to show."; + StatusMessage = Loc.T("vm.diff.noChanges"); return; } @@ -169,7 +170,7 @@ public sealed partial class DiffModalViewModel : ViewModelBase } SelectedFile = Files.Count > 0 ? Files[0] : null; - if (Files.Count == 0) StatusMessage = "No changes to show."; + if (Files.Count == 0) StatusMessage = Loc.T("vm.diff.noChanges"); } private static void ParseHunkHeader(string header, out int oldStart, out int newStart) diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs index 9f7a635..7ca80ba 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.ObjectModel; using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; +using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -80,7 +81,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase await _worker.UpdateListAsync(new UpdateListDto( ListId, - string.IsNullOrWhiteSpace(Name) ? "Untitled" : Name, + string.IsNullOrWhiteSpace(Name) ? Loc.T("vm.listSettings.untitled") : Name, string.IsNullOrWhiteSpace(WorkingDir) ? null : WorkingDir, DefaultCommitType)); @@ -93,7 +94,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase [RelayCommand] private async Task DeleteAsync() { - var displayName = string.IsNullOrWhiteSpace(Name) ? "Untitled" : Name; + var displayName = string.IsNullOrWhiteSpace(Name) ? Loc.T("vm.listSettings.untitled") : Name; if (ConfirmAsync is not null) { var ok = await ConfirmAsync($"Delete list \"{displayName}\" and all its tasks? This cannot be undone."); diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/MergeModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/MergeModalViewModel.cs index 9b7d454..68ae36a 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/MergeModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/MergeModalViewModel.cs @@ -1,4 +1,5 @@ using System.Collections.ObjectModel; +using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -36,7 +37,7 @@ public sealed partial class MergeModalViewModel : ViewModelBase { TaskId = taskId; TaskTitle = taskTitle; - CommitMessage = $"Merge task: {taskTitle}"; + CommitMessage = Loc.T("vm.merge.commitMessage", taskTitle); IsBusy = true; try @@ -45,7 +46,7 @@ public sealed partial class MergeModalViewModel : ViewModelBase Branches.Clear(); if (targets is null) { - ErrorMessage = "Worker offline — cannot list branches."; + ErrorMessage = Loc.T("vm.merge.workerOfflineBranches"); return; } foreach (var b in targets.LocalBranches) Branches.Add(b); @@ -55,7 +56,7 @@ public sealed partial class MergeModalViewModel : ViewModelBase } catch (Exception ex) { - ErrorMessage = $"Failed to load branches: {ex.Message}"; + ErrorMessage = Loc.T("vm.merge.loadBranchesFailed", ex.Message); } finally { IsBusy = false; } } @@ -81,7 +82,7 @@ public sealed partial class MergeModalViewModel : ViewModelBase case "merged": SuccessMessage = result.ErrorMessage is not null ? $"Merged with warning: {result.ErrorMessage}" - : "Merged."; + : Loc.T("vm.merge.merged"); // Auto-close after a short delay. _ = Task.Run(async () => { @@ -92,19 +93,19 @@ public sealed partial class MergeModalViewModel : ViewModelBase case "conflict": HasConflict = true; ConflictFiles = result.ConflictFiles; - ErrorMessage = "Merge conflict — target branch restored. Resolve manually or via Continue, then retry."; + ErrorMessage = Loc.T("vm.merge.conflict"); break; case "blocked": - ErrorMessage = $"Blocked: {result.ErrorMessage}"; + ErrorMessage = Loc.T("vm.merge.blocked", result.ErrorMessage ?? ""); break; default: - ErrorMessage = $"Unknown status: {result.Status}"; + ErrorMessage = Loc.T("vm.merge.unknownStatus", result.Status); break; } } catch (Exception ex) { - ErrorMessage = $"Merge failed: {ex.Message}"; + ErrorMessage = Loc.T("vm.merge.mergeFailed", ex.Message); } finally { diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs index a674624..bd7a778 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using ClaudeDo.Data; +using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -26,13 +27,13 @@ public sealed partial class FilesSettingsTabViewModel : ViewModelBase try { var r = await _worker.RestoreDefaultAgentsAsync(); - if (r is null) StatusMessage = "Worker offline."; - else if (r.Copied == 0 && r.Skipped == 0) StatusMessage = "No default agents bundled."; - else if (r.Copied == 0) StatusMessage = "All default agents already present."; - else StatusMessage = $"Restored {r.Copied} default agent(s)."; + if (r is null) StatusMessage = Loc.T("vm.filesTab.workerOffline"); + else if (r.Copied == 0 && r.Skipped == 0) StatusMessage = Loc.T("vm.filesTab.noneBundled"); + else if (r.Copied == 0) StatusMessage = Loc.T("vm.filesTab.allPresent"); + else StatusMessage = Loc.T("vm.filesTab.restored", r.Copied); await _worker.RefreshAgentsAsync(); } - catch (Exception ex) { StatusMessage = $"Restore failed: {ex.Message}"; } + catch (Exception ex) { StatusMessage = Loc.T("vm.filesTab.restoreFailed", ex.Message); } finally { IsBusy = false; } } @@ -46,6 +47,6 @@ public sealed partial class FilesSettingsTabViewModel : ViewModelBase var path = PromptFiles.PathFor(kind); Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); } - catch (Exception ex) { StatusMessage = $"Open failed: {ex.Message}"; } + catch (Exception ex) { StatusMessage = Loc.T("vm.filesTab.openFailed", ex.Message); } } } diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/WorktreesSettingsTabViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/WorktreesSettingsTabViewModel.cs index 21f7f52..277d345 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/WorktreesSettingsTabViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/WorktreesSettingsTabViewModel.cs @@ -1,4 +1,5 @@ using System.IO; +using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -43,7 +44,7 @@ public sealed partial class WorktreesSettingsTabViewModel : ViewModelBase try { var r = await _worker.CleanupFinishedWorktreesAsync(); - StatusMessage = r is null ? "Worker offline." : $"Removed {r.Removed} worktree(s)."; + StatusMessage = r is null ? Loc.T("vm.worktreesTab.workerOffline") : Loc.T("vm.worktreesTab.removed", r.Removed); } finally { IsBusy = false; } } @@ -58,9 +59,9 @@ public sealed partial class WorktreesSettingsTabViewModel : ViewModelBase try { var r = await _worker.ResetAllWorktreesAsync(); - if (r is null) StatusMessage = "Worker offline."; - else if (r.Blocked) StatusMessage = $"Cannot force-remove: {r.RunningTasks} task(s) still running. Cancel them first."; - else StatusMessage = $"Removed {r.Removed} worktree(s) from {r.TasksAffected} task(s)."; + if (r is null) StatusMessage = Loc.T("vm.worktreesTab.workerOffline"); + else if (r.Blocked) StatusMessage = Loc.T("vm.worktreesTab.blocked", r.RunningTasks); + else StatusMessage = Loc.T("vm.worktreesTab.removedFrom", r.Removed, r.TasksAffected); } finally { IsBusy = false; } } diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs index 13f28eb..c4b4c5f 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs @@ -1,6 +1,7 @@ using System.Linq; using ClaudeDo.Data; using ClaudeDo.Localization; +using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels.Modals.Settings; using CommunityToolkit.Mvvm.ComponentModel; @@ -60,7 +61,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase System.Text.Json.JsonSerializer.Deserialize>(dto.ReportExcludedPaths) ?? new()); General.StandupWeekday = dto.StandupWeekday is >= 0 and <= 6 ? dto.StandupWeekday : (int)DayOfWeek.Wednesday; } - else StatusMessage = "Worker offline — settings read-only."; + else StatusMessage = Loc.T("vm.settingsModal.workerOffline"); await Prime.LoadAsync(); } @@ -95,7 +96,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase await Prime.SaveAsync(); CloseAction?.Invoke(); } - catch (Exception ex) { StatusMessage = $"Save failed: {ex.Message}"; } + catch (Exception ex) { StatusMessage = Loc.T("vm.settingsModal.saveFailed", ex.Message); } finally { IsBusy = false; } } diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/WeeklyReportModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/WeeklyReportModalViewModel.cs index a82424e..35f6379 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/WeeklyReportModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/WeeklyReportModalViewModel.cs @@ -1,3 +1,4 @@ +using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -72,16 +73,16 @@ public sealed partial class WeeklyReportModalViewModel : ViewModelBase [RelayCommand(CanExecute = nameof(CanGenerate))] private async Task Generate() { - if (!RangeValid) { StatusMessage = "Invalid date range."; return; } + if (!RangeValid) { StatusMessage = Loc.T("vm.weeklyReport.invalidRange"); return; } IsBusy = true; - StatusMessage = "Generating report…"; + StatusMessage = Loc.T("vm.weeklyReport.generating"); try { ReportMarkdown = await _worker.GenerateWeekReportAsync( DateOnly.FromDateTime(StartDate!.Value), DateOnly.FromDateTime(EndDate!.Value)); StatusMessage = ""; } - catch (Exception ex) { StatusMessage = $"Error: {ex.Message}"; } + catch (Exception ex) { StatusMessage = Loc.T("vm.weeklyReport.error", ex.Message); } finally { IsBusy = false; } } } diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs index 5b3b87d..f55ac3b 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs @@ -4,6 +4,7 @@ using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Input.Platform; using ClaudeDo.Data.Models; +using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -86,7 +87,9 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase { ListIdFilter = listId; IsGlobal = listId is null; - Title = listId is null ? "Worktrees" : $"Worktrees — {listName ?? "list"}"; + Title = listId is null + ? Loc.T("vm.worktreesOverview.titleAll") + : Loc.T("vm.worktreesOverview.titleList", listName ?? Loc.T("vm.worktreesOverview.listFallback")); } public async Task LoadAsync(CancellationToken ct = default) @@ -138,7 +141,7 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase try { var result = await _worker.CleanupFinishedWorktreesAsync(ListIdFilter); - StatusMessage = result is null ? "Cleanup failed." : $"Removed {result.Removed} worktree(s)."; + StatusMessage = result is null ? Loc.T("vm.worktreesOverview.cleanupFailed") : Loc.T("vm.worktreesOverview.removed", result.Removed); await LoadAsync(); } finally { IsBusy = false; } @@ -190,7 +193,7 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase if (row is null || row.State != WorktreeState.Active) return; var (ok, err) = await _worker.SetWorktreeStateAsync(row.TaskId, WorktreeState.Discarded); if (ok) row.State = WorktreeState.Discarded; - else StatusMessage = err ?? "Failed to discard worktree."; + else StatusMessage = err ?? Loc.T("vm.worktreesOverview.discardFailed"); } [RelayCommand] @@ -199,20 +202,20 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase if (row is null || row.State != WorktreeState.Active) return; var (ok, err) = await _worker.SetWorktreeStateAsync(row.TaskId, WorktreeState.Kept); if (ok) row.State = WorktreeState.Kept; - else StatusMessage = err ?? "Failed to keep worktree."; + else StatusMessage = err ?? Loc.T("vm.worktreesOverview.keepFailed"); } [RelayCommand] private async Task ForceRemove(WorktreeOverviewRowViewModel? row) { if (row is null) return; - if (row.IsRunning) { StatusMessage = "Cannot force-remove a running task."; return; } + if (row.IsRunning) { StatusMessage = Loc.T("vm.worktreesOverview.cannotForceRunning"); return; } if (ConfirmAction is not null && !await ConfirmAction($"Force remove worktree for '{row.TaskTitle}'? This deletes the directory and branch.")) return; var result = await _worker.ForceRemoveWorktreeAsync(row.TaskId); if (result is null || !result.Removed) { - StatusMessage = result?.Reason ?? "Force remove failed."; + StatusMessage = result?.Reason ?? Loc.T("vm.worktreesOverview.forceRemoveFailed"); return; } if (IsGlobal) diff --git a/src/ClaudeDo.Ui/ViewModels/Planning/ConflictResolutionViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Planning/ConflictResolutionViewModel.cs index 18d1e54..7b3023b 100644 --- a/src/ClaudeDo.Ui/ViewModels/Planning/ConflictResolutionViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Planning/ConflictResolutionViewModel.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; namespace ClaudeDo.Ui.ViewModels.Planning; @@ -14,6 +15,8 @@ public sealed partial class ConflictResolutionViewModel : ObservableObject public string SubtaskTitle { get; } public string TargetBranch { get; } public IReadOnlyList ConflictedFiles { get; } + public string SubtaskLabel => Loc.T("vm.conflictResolution.subtaskPrefix", SubtaskTitle); + public string TargetLabel => Loc.T("vm.conflictResolution.targetPrefix", TargetBranch); [ObservableProperty] private string? _vsCodeError; [ObservableProperty] private string? _actionError; @@ -53,7 +56,7 @@ public sealed partial class ConflictResolutionViewModel : ObservableObject } catch (Exception ex) { - VsCodeError = $"Could not launch VS Code: {ex.Message}. Paths are listed above — copy them manually."; + VsCodeError = Loc.T("vm.conflictResolution.vsCodeError", ex.Message); } } diff --git a/src/ClaudeDo.Ui/ViewModels/Planning/PlanningDiffViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Planning/PlanningDiffViewModel.cs index 485ceed..2306b46 100644 --- a/src/ClaudeDo.Ui/ViewModels/Planning/PlanningDiffViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Planning/PlanningDiffViewModel.cs @@ -1,6 +1,7 @@ using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; namespace ClaudeDo.Ui.ViewModels.Planning; @@ -59,7 +60,7 @@ public sealed partial class PlanningDiffViewModel : ObservableObject if (result is null) { DisplayedDiff = ""; - CombinedWarning = "Could not build combined preview (hub error)."; + CombinedWarning = Loc.T("vm.planningDiff.hubError"); } else if (result.Success) { @@ -69,7 +70,7 @@ public sealed partial class PlanningDiffViewModel : ObservableObject else { var files = result.ConflictedFiles?.Count ?? 0; - CombinedWarning = $"Cannot build combined preview: subtask {result.FirstConflictSubtaskId} conflicts with an earlier subtask ({files} files)."; + CombinedWarning = Loc.T("vm.planningDiff.conflict", result.FirstConflictSubtaskId ?? "", files); DisplayedDiff = ""; } } diff --git a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml index f41599b..67f5369 100644 --- a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml +++ b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml @@ -91,7 +91,7 @@ SelectedItem="{Binding TaskModelSelection, Mode=TwoWay}" HorizontalAlignment="Stretch"/> @@ -114,7 +114,7 @@ diff --git a/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml b/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml index 937584d..0baca34 100644 --- a/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml +++ b/src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml @@ -47,13 +47,7 @@ - - - - - - - +