feat(i18n): localize ViewModel-built strings via ambient Loc accessor

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-03 12:43:30 +02:00
parent 086c6f6c45
commit 350a89f364
23 changed files with 250 additions and 84 deletions

View File

@@ -86,6 +86,7 @@ sealed class Program
fallback: "en"); fallback: "en");
var localizer = new Localizer(localeStore, initialLang); var localizer = new Localizer(localeStore, initialLang);
TrExtension.Localizer = localizer; TrExtension.Localizer = localizer;
ClaudeDo.Ui.Localization.Loc.Current = localizer;
sc.AddSingleton<ILocalizer>(localizer); sc.AddSingleton<ILocalizer>(localizer);
sc.AddDbContextFactory<ClaudeDoDbContext>(opt => sc.AddDbContextFactory<ClaudeDoDbContext>(opt =>
opt.UseSqlite($"Data Source={dbPath}")); opt.UseSqlite($"Data Source={dbPath}"));

View File

@@ -302,5 +302,26 @@
"updateNow": "Update now", "updateNow": "Update now",
"dismiss": "Dismiss" "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" }
} }
} }

View File

@@ -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<LanguageOption> AvailableLanguages => Array.Empty<LanguageOption>();
public void SetLanguage(string code) { }
public event EventHandler? LanguageChanged { add { } remove { } }
}
}

View File

@@ -6,6 +6,7 @@ using ClaudeDo.Data;
using ClaudeDo.Data.Models; using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories; using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Helpers; using ClaudeDo.Ui.Helpers;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.Services.Interfaces; using ClaudeDo.Ui.Services.Interfaces;
using ClaudeDo.Ui.ViewModels.Modals; using ClaudeDo.Ui.ViewModels.Modals;
@@ -99,13 +100,14 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
[NotifyCanExecuteChangedFor(nameof(DequeueCommand))] [NotifyCanExecuteChangedFor(nameof(DequeueCommand))]
[NotifyCanExecuteChangedFor(nameof(ResetAndRetryCommand))] [NotifyCanExecuteChangedFor(nameof(ResetAndRetryCommand))]
[NotifyCanExecuteChangedFor(nameof(ContinueCommand))] [NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
private string _agentStatusLabel = "Idle"; private string _agentState = "idle";
public bool IsIdle => AgentStatusLabel == "Idle"; public string AgentStatusLabel => Loc.T($"vm.agentStatus.{AgentState}");
public bool IsQueued => AgentStatusLabel == "Queued"; public bool IsIdle => AgentState == "idle";
public bool IsRunning => AgentStatusLabel == "Running"; public bool IsQueued => AgentState == "queued";
public bool IsDone => AgentStatusLabel == "Done"; public bool IsRunning => AgentState == "running";
public bool IsFailed => AgentStatusLabel == "Failed"; public bool IsDone => AgentState == "done";
public bool IsCancelled => AgentStatusLabel == "Cancelled"; public bool IsFailed => AgentState == "failed";
public bool IsCancelled => AgentState == "cancelled";
// Recovery actions: Continue (resume session) for Failed/Cancelled. // Recovery actions: Continue (resume session) for Failed/Cancelled.
public bool ShowContinue => IsFailed || IsCancelled; public bool ShowContinue => IsFailed || IsCancelled;
@@ -116,8 +118,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
[NotifyCanExecuteChangedFor(nameof(ContinueCommand))] [NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
private string? _latestRunSessionId; private string? _latestRunSessionId;
partial void OnAgentStatusLabelChanged(string value) partial void OnAgentStateChanged(string value)
{ {
OnPropertyChanged(nameof(AgentStatusLabel));
OnPropertyChanged(nameof(IsIdle)); OnPropertyChanged(nameof(IsIdle));
OnPropertyChanged(nameof(IsQueued)); OnPropertyChanged(nameof(IsQueued));
OnPropertyChanged(nameof(IsRunning)); OnPropertyChanged(nameof(IsRunning));
@@ -127,6 +130,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
OnPropertyChanged(nameof(ShowContinue)); OnPropertyChanged(nameof(ShowContinue));
OnPropertyChanged(nameof(ShowResetAndRetry)); OnPropertyChanged(nameof(ShowResetAndRetry));
OnPropertyChanged(nameof(IsAgentSectionEnabled)); OnPropertyChanged(nameof(IsAgentSectionEnabled));
OnPropertyChanged(nameof(EffectiveModelLabel));
OnPropertyChanged(nameof(EffectiveAgentLabel));
} }
[ObservableProperty] private string? _model; [ObservableProperty] private string? _model;
@@ -139,6 +144,12 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
[ObservableProperty] private string _effectiveSystemPromptHint = ""; [ObservableProperty] private string _effectiveSystemPromptHint = "";
[ObservableProperty] private string _effectiveAgentHint = ""; [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<string> TaskModelOptions { get; } = new( public System.Collections.ObjectModel.ObservableCollection<string> TaskModelOptions { get; } = new(
new[] { ModelRegistry.TaskInheritSentinel }.Concat(ModelRegistry.Aliases)); 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 // Set by the view so DeleteTaskCommand can show an error message
public Func<string, System.Threading.Tasks.Task>? ShowErrorAsync { get; set; } public Func<string, System.Threading.Tasks.Task>? 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) private async System.Threading.Tasks.Task RefreshStatusAsync(string taskId)
{ {
try try
@@ -233,7 +264,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
.FirstOrDefaultAsync(t => t.Id == taskId); .FirstOrDefaultAsync(t => t.Id == taskId);
if (entity is null || Task?.Id != taskId) return; if (entity is null || Task?.Id != taskId) return;
AgentStatusLabel = entity.Status.ToString(); AgentState = StatusToStateKey(entity.Status);
} }
catch { } catch { }
} }
@@ -245,6 +276,12 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
_services = services; _services = services;
_notesApi = notesApi; _notesApi = notesApi;
Notes = new NotesEditorViewModel(_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 // Subscribe once; filter by current task id inside the handler
_worker.TaskMessageEvent += OnTaskMessage; _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. // If the task row's live status changes (e.g. TaskStarted/Finished), mirror it.
_worker.TaskStartedEvent += (slot, taskId, startedAt) => _worker.TaskStartedEvent += (slot, taskId, startedAt) =>
{ {
if (Task?.Id == taskId) AgentStatusLabel = "Running"; if (Task?.Id == taskId) AgentState = "running";
}; };
_worker.TaskFinishedEvent += (slot, taskId, status, finishedAt) => _worker.TaskFinishedEvent += (slot, taskId, status, finishedAt) =>
{ {
@@ -275,7 +312,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
Kind = LogKind.Done, Kind = LogKind.Done,
Text = $"── {status.ToUpperInvariant()} · {finishedAt.ToLocalTime():HH:mm:ss} ──", Text = $"── {status.ToUpperInvariant()} · {finishedAt.ToLocalTime():HH:mm:ss} ──",
}); });
AgentStatusLabel = status; AgentState = FinishedStatusToStateKey(status);
// Re-query to pick up worktree created during the run. // Re-query to pick up worktree created during the run.
_ = RefreshWorktreeAsync(taskId); _ = RefreshWorktreeAsync(taskId);
}; };
@@ -473,7 +510,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
WorktreePath = null; WorktreePath = null;
WorktreeStateLabel = null; WorktreeStateLabel = null;
BranchLine = null; BranchLine = null;
AgentStatusLabel = "Idle"; AgentState = "idle";
LatestRunSessionId = null; LatestRunSessionId = null;
_suppressAgentSave = true; _suppressAgentSave = true;
try try
@@ -519,7 +556,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
WorktreeBaseCommit = entity.Worktree?.BaseCommit; WorktreeBaseCommit = entity.Worktree?.BaseCommit;
WorktreeStateLabel = entity.Worktree?.State.ToString(); WorktreeStateLabel = entity.Worktree?.State.ToString();
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} \u2190 main" : null; BranchLine = entity.Worktree is { } w ? $"{w.BranchName} \u2190 main" : null;
AgentStatusLabel = entity.Status.ToString(); AgentState = StatusToStateKey(entity.Status);
await LoadAgentSettingsAsync(entity, ct); await LoadAgentSettingsAsync(entity, ct);
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
@@ -730,7 +767,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
WorktreeBaseCommit = entity.Worktree?.BaseCommit; WorktreeBaseCommit = entity.Worktree?.BaseCommit;
WorktreeStateLabel = entity.Worktree?.State.ToString(); WorktreeStateLabel = entity.Worktree?.State.ToString();
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} ← main" : null; 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) if (Task is { } row && entity.Worktree?.DiffStat is { } stat)
row.DiffStat = stat; row.DiffStat = stat;
} }
@@ -808,7 +845,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
? ClaudeDo.Data.Models.TaskStatus.Done ? ClaudeDo.Data.Models.TaskStatus.Done
: ClaudeDo.Data.Models.TaskStatus.Idle; : ClaudeDo.Data.Models.TaskStatus.Idle;
Task.Status = entity.Status; Task.Status = entity.Status;
AgentStatusLabel = entity.Status.ToString(); AgentState = StatusToStateKey(entity.Status);
await repo.UpdateAsync(entity); await repo.UpdateAsync(entity);
} }
@@ -922,7 +959,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
try try
{ {
await _worker.SetTaskStatusAsync(Task.Id, ClaudeDo.Data.Models.TaskStatus.Queued); await _worker.SetTaskStatusAsync(Task.Id, ClaudeDo.Data.Models.TaskStatus.Queued);
AgentStatusLabel = "Queued"; AgentState = "queued";
} }
catch { /* offline */ } catch { /* offline */ }
} }
@@ -938,7 +975,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
try try
{ {
await _worker.SetTaskStatusAsync(Task.Id, ClaudeDo.Data.Models.TaskStatus.Idle); await _worker.SetTaskStatusAsync(Task.Id, ClaudeDo.Data.Models.TaskStatus.Idle);
AgentStatusLabel = "Idle"; AgentState = "idle";
} }
catch { /* offline */ } catch { /* offline */ }
} }
@@ -973,7 +1010,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
try try
{ {
await _worker.SetTaskStatusAsync(Task.Id, ClaudeDo.Data.Models.TaskStatus.Queued); await _worker.SetTaskStatusAsync(Task.Id, ClaudeDo.Data.Models.TaskStatus.Queued);
AgentStatusLabel = "Queued"; AgentState = "queued";
} }
catch { /* offline */ } catch { /* offline */ }
} }

View File

@@ -6,6 +6,7 @@ using ClaudeDo.Data.Filtering;
using ClaudeDo.Data.Models; using ClaudeDo.Data.Models;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus; using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
using ClaudeDo.Data.Repositories; using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Modals; using ClaudeDo.Ui.ViewModels.Modals;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -137,6 +138,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase
public string UserName { get; } = Environment.UserName; public string UserName { get; } = Environment.UserName;
public string MachineName { get; } = Environment.MachineName; public string MachineName { get; } = Environment.MachineName;
public string MachineNameLocal => Loc.T("vm.lists.localSuffix", MachineName);
public string UserInitials { get; } public string UserInitials { get; }
public ListsIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IServiceProvider? services = null, WorkerClient? worker = null) public ListsIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IServiceProvider? services = null, WorkerClient? worker = null)
@@ -170,12 +172,12 @@ public sealed partial class ListsIslandViewModel : ViewModelBase
var smart = new[] var smart = new[]
{ {
new ListNavItemViewModel { Id = "smart:my-day", Name = "My Day", Kind = ListKind.Smart, IconKey = "Sun" }, new ListNavItemViewModel { Id = "smart:my-day", Name = Loc.T("vm.lists.smartMyDay"), Kind = ListKind.Smart, IconKey = "Sun" },
new ListNavItemViewModel { Id = "smart:important", Name = "Important", Kind = ListKind.Smart, IconKey = "Star" }, new ListNavItemViewModel { Id = "smart:important", Name = Loc.T("vm.lists.smartImportant"), Kind = ListKind.Smart, IconKey = "Star" },
new ListNavItemViewModel { Id = "smart:planned", Name = "Planned", Kind = ListKind.Smart, IconKey = "Calendar" }, new ListNavItemViewModel { Id = "smart:planned", Name = Loc.T("vm.lists.smartPlanned"), Kind = ListKind.Smart, IconKey = "Calendar" },
new ListNavItemViewModel { Id = "virtual:queued", Name = "Queue", Kind = ListKind.Virtual, IconKey = "Inbox" }, new ListNavItemViewModel { Id = "virtual:queued", Name = Loc.T("vm.lists.virtualQueue"), Kind = ListKind.Virtual, IconKey = "Inbox" },
new ListNavItemViewModel { Id = "virtual:running", Name = "Running", Kind = ListKind.Virtual, IconKey = "Activity" }, new ListNavItemViewModel { Id = "virtual:running", Name = Loc.T("vm.lists.virtualRunning"), Kind = ListKind.Virtual, IconKey = "Activity" },
new ListNavItemViewModel { Id = "virtual:review", Name = "Review", Kind = ListKind.Virtual, IconKey = "Eye" }, 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); } 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 var entity = new ListEntity
{ {
Id = Guid.NewGuid().ToString("N"), Id = Guid.NewGuid().ToString("N"),
Name = "New list", Name = Loc.T("vm.lists.newList"),
DefaultCommitType = CommitTypeRegistry.DefaultType, DefaultCommitType = CommitTypeRegistry.DefaultType,
CreatedAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow,
}; };

View File

@@ -1,5 +1,6 @@
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using ClaudeDo.Data.Models; using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Localization;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus; using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Ui.ViewModels.Islands; namespace ClaudeDo.Ui.ViewModels.Islands;
@@ -31,7 +32,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
[ObservableProperty] private bool _parentFinalized; [ObservableProperty] private bool _parentFinalized;
public DateTime CreatedAt { get; init; } 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 StepsCount { get; init; }
public int StepsCompleted { get; init; } public int StepsCompleted { get; init; }
@@ -50,8 +51,8 @@ public sealed partial class TaskRowViewModel : ViewModelBase
public string? PlanningBadge => PlanningPhase switch public string? PlanningBadge => PlanningPhase switch
{ {
PlanningPhase.Active => "PLANNING", PlanningPhase.Active => Loc.T("vm.planningBadge.active"),
PlanningPhase.Finalized => "PLANNED", PlanningPhase.Finalized => Loc.T("vm.planningBadge.finalized"),
_ => null, _ => null,
}; };
@@ -77,9 +78,19 @@ public sealed partial class TaskRowViewModel : ViewModelBase
public string DiffAdditionsText => $"+{DiffAdditions}"; public string DiffAdditionsText => $"+{DiffAdditions}";
public string DiffDeletionsText => $"{DiffDeletions}"; 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 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 OnDiffAdditionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffAdditionsText)); }
partial void OnDiffDeletionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffDeletionsText)); } 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) public static TaskRowViewModel FromEntity(TaskEntity t)
{ {
var row = new TaskRowViewModel { Id = t.Id, CreatedAt = t.CreatedAt }; var row = new TaskRowViewModel { Id = t.Id, CreatedAt = t.CreatedAt };

View File

@@ -6,6 +6,7 @@ using ClaudeDo.Data;
using ClaudeDo.Data.Filtering; using ClaudeDo.Data.Filtering;
using ClaudeDo.Data.Models; using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories; using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Modals; using ClaudeDo.Ui.ViewModels.Modals;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -52,7 +53,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
[ObservableProperty] private bool _hasOpen; [ObservableProperty] private bool _hasOpen;
[ObservableProperty] private bool _hasCompleted; [ObservableProperty] private bool _hasCompleted;
[ObservableProperty] private bool _showOpenLabel; [ObservableProperty] private bool _showOpenLabel;
[ObservableProperty] private string _completedHeader = "COMPLETED"; [ObservableProperty] private string _completedHeader = "";
[ObservableProperty] private bool _showNotesRow; [ObservableProperty] private bool _showNotesRow;
public Func<UnfinishedPlanningModalViewModel, Task>? ShowUnfinishedPlanningModal { get; set; } public Func<UnfinishedPlanningModalViewModel, Task>? ShowUnfinishedPlanningModal { get; set; }
@@ -61,6 +62,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
{ {
_dbFactory = dbFactory; _dbFactory = dbFactory;
_worker = worker; _worker = worker;
CompletedHeader = Loc.T("vm.tasksIsland.completedHeader");
if (_worker is not null) if (_worker is not null)
{ {
_worker.TaskUpdatedEvent += OnWorkerTaskUpdated; _worker.TaskUpdatedEvent += OnWorkerTaskUpdated;
@@ -68,6 +70,14 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
_worker.ListUpdatedEvent += OnWorkerListUpdated; _worker.ListUpdatedEvent += OnWorkerListUpdated;
_worker.ConnectionRestoredEvent += () => LoadForList(_currentList); _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) private async void OnWorkerListUpdated(string listId)
@@ -340,7 +350,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
HasOpen = OpenItems.Count > 0; HasOpen = OpenItems.Count > 0;
HasCompleted = CompletedItems.Count > 0; HasCompleted = CompletedItems.Count > 0;
ShowOpenLabel = HasOpen && HasOverdue; ShowOpenLabel = HasOpen && HasOverdue;
CompletedHeader = $"COMPLETED · {CompletedItems.Count}"; CompletedHeader = Loc.T("vm.tasksIsland.completedHeaderCount", CompletedItems.Count);
} }
private void UpdateSubtitle() private void UpdateSubtitle()

View File

@@ -6,6 +6,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Data; using ClaudeDo.Data;
using ClaudeDo.Data.Models; using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Islands; using ClaudeDo.Ui.ViewModels.Islands;
using ClaudeDo.Ui.ViewModels.Modals; using ClaudeDo.Ui.ViewModels.Modals;
@@ -23,9 +24,9 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
public UpdateCheckService UpdateCheck => _updateCheck; public UpdateCheckService UpdateCheck => _updateCheck;
public string ConnectionText => public string ConnectionText =>
Worker?.IsConnected == true ? "Online" Worker?.IsConnected == true ? Loc.T("vm.connection.online")
: Worker?.IsReconnecting == true ? "Connecting" : Worker?.IsReconnecting == true ? Loc.T("vm.connection.connecting")
: "Offline"; : Loc.T("vm.connection.offline");
public bool IsOffline => Worker?.IsConnected != true && Worker?.IsReconnecting != true; public bool IsOffline => Worker?.IsConnected != true && Worker?.IsReconnecting != true;
@@ -358,7 +359,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
[RelayCommand] [RelayCommand]
private async Task RestartWorkerAsync() private async Task RestartWorkerAsync()
{ {
RestartWorkerStatus = "Restarting worker"; RestartWorkerStatus = Loc.T("vm.shell.restartingWorker");
try try
{ {
await Task.Run(RestartWorkerService); await Task.Run(RestartWorkerService);

View File

@@ -2,6 +2,7 @@ using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Data.Git; using ClaudeDo.Data.Git;
using ClaudeDo.Ui.Localization;
namespace ClaudeDo.Ui.ViewModels.Modals; namespace ClaudeDo.Ui.ViewModels.Modals;
@@ -91,13 +92,13 @@ public sealed partial class DiffModalViewModel : ViewModelBase
} }
catch (Exception ex) catch (Exception ex)
{ {
StatusMessage = $"Failed to load diff: {ex.Message}"; StatusMessage = Loc.T("vm.diff.loadFailed", ex.Message);
return; return;
} }
if (string.IsNullOrWhiteSpace(raw)) if (string.IsNullOrWhiteSpace(raw))
{ {
StatusMessage = "No changes to show."; StatusMessage = Loc.T("vm.diff.noChanges");
return; return;
} }
@@ -169,7 +170,7 @@ public sealed partial class DiffModalViewModel : ViewModelBase
} }
SelectedFile = Files.Count > 0 ? Files[0] : null; 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) private static void ParseHunkHeader(string header, out int oldStart, out int newStart)

View File

@@ -2,6 +2,7 @@ using System.Collections.ObjectModel;
using ClaudeDo.Data; using ClaudeDo.Data;
using ClaudeDo.Data.Models; using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories; using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
@@ -80,7 +81,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
await _worker.UpdateListAsync(new UpdateListDto( await _worker.UpdateListAsync(new UpdateListDto(
ListId, ListId,
string.IsNullOrWhiteSpace(Name) ? "Untitled" : Name, string.IsNullOrWhiteSpace(Name) ? Loc.T("vm.listSettings.untitled") : Name,
string.IsNullOrWhiteSpace(WorkingDir) ? null : WorkingDir, string.IsNullOrWhiteSpace(WorkingDir) ? null : WorkingDir,
DefaultCommitType)); DefaultCommitType));
@@ -93,7 +94,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
[RelayCommand] [RelayCommand]
private async Task DeleteAsync() 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) if (ConfirmAsync is not null)
{ {
var ok = await ConfirmAsync($"Delete list \"{displayName}\" and all its tasks? This cannot be undone."); var ok = await ConfirmAsync($"Delete list \"{displayName}\" and all its tasks? This cannot be undone.");

View File

@@ -1,4 +1,5 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
@@ -36,7 +37,7 @@ public sealed partial class MergeModalViewModel : ViewModelBase
{ {
TaskId = taskId; TaskId = taskId;
TaskTitle = taskTitle; TaskTitle = taskTitle;
CommitMessage = $"Merge task: {taskTitle}"; CommitMessage = Loc.T("vm.merge.commitMessage", taskTitle);
IsBusy = true; IsBusy = true;
try try
@@ -45,7 +46,7 @@ public sealed partial class MergeModalViewModel : ViewModelBase
Branches.Clear(); Branches.Clear();
if (targets is null) if (targets is null)
{ {
ErrorMessage = "Worker offline — cannot list branches."; ErrorMessage = Loc.T("vm.merge.workerOfflineBranches");
return; return;
} }
foreach (var b in targets.LocalBranches) Branches.Add(b); foreach (var b in targets.LocalBranches) Branches.Add(b);
@@ -55,7 +56,7 @@ public sealed partial class MergeModalViewModel : ViewModelBase
} }
catch (Exception ex) catch (Exception ex)
{ {
ErrorMessage = $"Failed to load branches: {ex.Message}"; ErrorMessage = Loc.T("vm.merge.loadBranchesFailed", ex.Message);
} }
finally { IsBusy = false; } finally { IsBusy = false; }
} }
@@ -81,7 +82,7 @@ public sealed partial class MergeModalViewModel : ViewModelBase
case "merged": case "merged":
SuccessMessage = result.ErrorMessage is not null SuccessMessage = result.ErrorMessage is not null
? $"Merged with warning: {result.ErrorMessage}" ? $"Merged with warning: {result.ErrorMessage}"
: "Merged."; : Loc.T("vm.merge.merged");
// Auto-close after a short delay. // Auto-close after a short delay.
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
@@ -92,19 +93,19 @@ public sealed partial class MergeModalViewModel : ViewModelBase
case "conflict": case "conflict":
HasConflict = true; HasConflict = true;
ConflictFiles = result.ConflictFiles; ConflictFiles = result.ConflictFiles;
ErrorMessage = "Merge conflict — target branch restored. Resolve manually or via Continue, then retry."; ErrorMessage = Loc.T("vm.merge.conflict");
break; break;
case "blocked": case "blocked":
ErrorMessage = $"Blocked: {result.ErrorMessage}"; ErrorMessage = Loc.T("vm.merge.blocked", result.ErrorMessage ?? "");
break; break;
default: default:
ErrorMessage = $"Unknown status: {result.Status}"; ErrorMessage = Loc.T("vm.merge.unknownStatus", result.Status);
break; break;
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
ErrorMessage = $"Merge failed: {ex.Message}"; ErrorMessage = Loc.T("vm.merge.mergeFailed", ex.Message);
} }
finally finally
{ {

View File

@@ -1,5 +1,6 @@
using System.Diagnostics; using System.Diagnostics;
using ClaudeDo.Data; using ClaudeDo.Data;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
@@ -26,13 +27,13 @@ public sealed partial class FilesSettingsTabViewModel : ViewModelBase
try try
{ {
var r = await _worker.RestoreDefaultAgentsAsync(); var r = await _worker.RestoreDefaultAgentsAsync();
if (r is null) StatusMessage = "Worker offline."; if (r is null) StatusMessage = Loc.T("vm.filesTab.workerOffline");
else if (r.Copied == 0 && r.Skipped == 0) StatusMessage = "No default agents bundled."; else if (r.Copied == 0 && r.Skipped == 0) StatusMessage = Loc.T("vm.filesTab.noneBundled");
else if (r.Copied == 0) StatusMessage = "All default agents already present."; else if (r.Copied == 0) StatusMessage = Loc.T("vm.filesTab.allPresent");
else StatusMessage = $"Restored {r.Copied} default agent(s)."; else StatusMessage = Loc.T("vm.filesTab.restored", r.Copied);
await _worker.RefreshAgentsAsync(); 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; } finally { IsBusy = false; }
} }
@@ -46,6 +47,6 @@ public sealed partial class FilesSettingsTabViewModel : ViewModelBase
var path = PromptFiles.PathFor(kind); var path = PromptFiles.PathFor(kind);
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); 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); }
} }
} }

View File

@@ -1,4 +1,5 @@
using System.IO; using System.IO;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
@@ -43,7 +44,7 @@ public sealed partial class WorktreesSettingsTabViewModel : ViewModelBase
try try
{ {
var r = await _worker.CleanupFinishedWorktreesAsync(); 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; } finally { IsBusy = false; }
} }
@@ -58,9 +59,9 @@ public sealed partial class WorktreesSettingsTabViewModel : ViewModelBase
try try
{ {
var r = await _worker.ResetAllWorktreesAsync(); var r = await _worker.ResetAllWorktreesAsync();
if (r is null) StatusMessage = "Worker offline."; if (r is null) StatusMessage = Loc.T("vm.worktreesTab.workerOffline");
else if (r.Blocked) StatusMessage = $"Cannot force-remove: {r.RunningTasks} task(s) still running. Cancel them first."; else if (r.Blocked) StatusMessage = Loc.T("vm.worktreesTab.blocked", r.RunningTasks);
else StatusMessage = $"Removed {r.Removed} worktree(s) from {r.TasksAffected} task(s)."; else StatusMessage = Loc.T("vm.worktreesTab.removedFrom", r.Removed, r.TasksAffected);
} }
finally { IsBusy = false; } finally { IsBusy = false; }
} }

View File

@@ -1,6 +1,7 @@
using System.Linq; using System.Linq;
using ClaudeDo.Data; using ClaudeDo.Data;
using ClaudeDo.Localization; using ClaudeDo.Localization;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Modals.Settings; using ClaudeDo.Ui.ViewModels.Modals.Settings;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
@@ -60,7 +61,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
System.Text.Json.JsonSerializer.Deserialize<List<string>>(dto.ReportExcludedPaths) ?? new()); System.Text.Json.JsonSerializer.Deserialize<List<string>>(dto.ReportExcludedPaths) ?? new());
General.StandupWeekday = dto.StandupWeekday is >= 0 and <= 6 ? dto.StandupWeekday : (int)DayOfWeek.Wednesday; 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(); await Prime.LoadAsync();
} }
@@ -95,7 +96,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
await Prime.SaveAsync(); await Prime.SaveAsync();
CloseAction?.Invoke(); CloseAction?.Invoke();
} }
catch (Exception ex) { StatusMessage = $"Save failed: {ex.Message}"; } catch (Exception ex) { StatusMessage = Loc.T("vm.settingsModal.saveFailed", ex.Message); }
finally { IsBusy = false; } finally { IsBusy = false; }
} }

View File

@@ -1,3 +1,4 @@
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
@@ -72,16 +73,16 @@ public sealed partial class WeeklyReportModalViewModel : ViewModelBase
[RelayCommand(CanExecute = nameof(CanGenerate))] [RelayCommand(CanExecute = nameof(CanGenerate))]
private async Task Generate() private async Task Generate()
{ {
if (!RangeValid) { StatusMessage = "Invalid date range."; return; } if (!RangeValid) { StatusMessage = Loc.T("vm.weeklyReport.invalidRange"); return; }
IsBusy = true; IsBusy = true;
StatusMessage = "Generating report…"; StatusMessage = Loc.T("vm.weeklyReport.generating");
try try
{ {
ReportMarkdown = await _worker.GenerateWeekReportAsync( ReportMarkdown = await _worker.GenerateWeekReportAsync(
DateOnly.FromDateTime(StartDate!.Value), DateOnly.FromDateTime(EndDate!.Value)); DateOnly.FromDateTime(StartDate!.Value), DateOnly.FromDateTime(EndDate!.Value));
StatusMessage = ""; StatusMessage = "";
} }
catch (Exception ex) { StatusMessage = $"Error: {ex.Message}"; } catch (Exception ex) { StatusMessage = Loc.T("vm.weeklyReport.error", ex.Message); }
finally { IsBusy = false; } finally { IsBusy = false; }
} }
} }

View File

@@ -4,6 +4,7 @@ using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
using ClaudeDo.Data.Models; using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
@@ -86,7 +87,9 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
{ {
ListIdFilter = listId; ListIdFilter = listId;
IsGlobal = listId is null; 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) public async Task LoadAsync(CancellationToken ct = default)
@@ -138,7 +141,7 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
try try
{ {
var result = await _worker.CleanupFinishedWorktreesAsync(ListIdFilter); 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(); await LoadAsync();
} }
finally { IsBusy = false; } finally { IsBusy = false; }
@@ -190,7 +193,7 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
if (row is null || row.State != WorktreeState.Active) return; if (row is null || row.State != WorktreeState.Active) return;
var (ok, err) = await _worker.SetWorktreeStateAsync(row.TaskId, WorktreeState.Discarded); var (ok, err) = await _worker.SetWorktreeStateAsync(row.TaskId, WorktreeState.Discarded);
if (ok) row.State = WorktreeState.Discarded; if (ok) row.State = WorktreeState.Discarded;
else StatusMessage = err ?? "Failed to discard worktree."; else StatusMessage = err ?? Loc.T("vm.worktreesOverview.discardFailed");
} }
[RelayCommand] [RelayCommand]
@@ -199,20 +202,20 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
if (row is null || row.State != WorktreeState.Active) return; if (row is null || row.State != WorktreeState.Active) return;
var (ok, err) = await _worker.SetWorktreeStateAsync(row.TaskId, WorktreeState.Kept); var (ok, err) = await _worker.SetWorktreeStateAsync(row.TaskId, WorktreeState.Kept);
if (ok) row.State = WorktreeState.Kept; if (ok) row.State = WorktreeState.Kept;
else StatusMessage = err ?? "Failed to keep worktree."; else StatusMessage = err ?? Loc.T("vm.worktreesOverview.keepFailed");
} }
[RelayCommand] [RelayCommand]
private async Task ForceRemove(WorktreeOverviewRowViewModel? row) private async Task ForceRemove(WorktreeOverviewRowViewModel? row)
{ {
if (row is null) return; 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; 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); var result = await _worker.ForceRemoveWorktreeAsync(row.TaskId);
if (result is null || !result.Removed) if (result is null || !result.Removed)
{ {
StatusMessage = result?.Reason ?? "Force remove failed."; StatusMessage = result?.Reason ?? Loc.T("vm.worktreesOverview.forceRemoveFailed");
return; return;
} }
if (IsGlobal) if (IsGlobal)

View File

@@ -1,6 +1,7 @@
using System.Diagnostics; using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services;
namespace ClaudeDo.Ui.ViewModels.Planning; namespace ClaudeDo.Ui.ViewModels.Planning;
@@ -14,6 +15,8 @@ public sealed partial class ConflictResolutionViewModel : ObservableObject
public string SubtaskTitle { get; } public string SubtaskTitle { get; }
public string TargetBranch { get; } public string TargetBranch { get; }
public IReadOnlyList<string> ConflictedFiles { get; } public IReadOnlyList<string> 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? _vsCodeError;
[ObservableProperty] private string? _actionError; [ObservableProperty] private string? _actionError;
@@ -53,7 +56,7 @@ public sealed partial class ConflictResolutionViewModel : ObservableObject
} }
catch (Exception ex) 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);
} }
} }

View File

@@ -1,6 +1,7 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services;
namespace ClaudeDo.Ui.ViewModels.Planning; namespace ClaudeDo.Ui.ViewModels.Planning;
@@ -59,7 +60,7 @@ public sealed partial class PlanningDiffViewModel : ObservableObject
if (result is null) if (result is null)
{ {
DisplayedDiff = ""; DisplayedDiff = "";
CombinedWarning = "Could not build combined preview (hub error)."; CombinedWarning = Loc.T("vm.planningDiff.hubError");
} }
else if (result.Success) else if (result.Success)
{ {
@@ -69,7 +70,7 @@ public sealed partial class PlanningDiffViewModel : ObservableObject
else else
{ {
var files = result.ConflictedFiles?.Count ?? 0; 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 = ""; DisplayedDiff = "";
} }
} }

View File

@@ -91,7 +91,7 @@
SelectedItem="{Binding TaskModelSelection, Mode=TwoWay}" SelectedItem="{Binding TaskModelSelection, Mode=TwoWay}"
HorizontalAlignment="Stretch"/> HorizontalAlignment="Stretch"/>
<TextBlock Classes="meta" <TextBlock Classes="meta"
Text="{Binding EffectiveModelHint, StringFormat='Effective if inherited: {0}'}" Text="{Binding EffectiveModelLabel}"
Opacity="0.6"/> Opacity="0.6"/>
</StackPanel> </StackPanel>
@@ -114,7 +114,7 @@
</ComboBox.ItemTemplate> </ComboBox.ItemTemplate>
</ComboBox> </ComboBox>
<TextBlock Classes="meta" <TextBlock Classes="meta"
Text="{Binding EffectiveAgentHint, StringFormat='Effective if inherited: {0}'}" Text="{Binding EffectiveAgentLabel}"
Opacity="0.6"/> Opacity="0.6"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>

View File

@@ -47,13 +47,7 @@
<!-- Name + machine --> <!-- Name + machine -->
<StackPanel Grid.Column="1" Margin="8,0" Spacing="1" VerticalAlignment="Center"> <StackPanel Grid.Column="1" Margin="8,0" Spacing="1" VerticalAlignment="Center">
<TextBlock Classes="title" Text="{Binding UserName}"/> <TextBlock Classes="title" Text="{Binding UserName}"/>
<TextBlock Classes="meta"> <TextBlock Classes="meta" Text="{Binding MachineNameLocal}"/>
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} / local">
<Binding Path="MachineName"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel> </StackPanel>
<!-- More button --> <!-- More button -->
<Button Grid.Column="2" Classes="icon-btn" VerticalAlignment="Center" <Button Grid.Column="2" Classes="icon-btn" VerticalAlignment="Center"

View File

@@ -31,8 +31,8 @@
<!-- Content --> <!-- Content -->
<StackPanel Spacing="12" Margin="20,16" MinWidth="520"> <StackPanel Spacing="12" Margin="20,16" MinWidth="520">
<TextBlock Classes="heading" <TextBlock Classes="heading"
Text="{Binding SubtaskTitle, StringFormat='Conflicts in subtask: {0}'}"/> Text="{Binding SubtaskLabel}"/>
<TextBlock Classes="body" Text="{Binding TargetBranch, StringFormat='Merging into: {0}'}"/> <TextBlock Classes="body" Text="{Binding TargetLabel}"/>
<ItemsControl ItemsSource="{Binding ConflictedFiles}"> <ItemsControl ItemsSource="{Binding ConflictedFiles}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>

View File

@@ -1,4 +1,7 @@
using System.IO;
using ClaudeDo.Data.Models; using ClaudeDo.Data.Models;
using ClaudeDo.Localization;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Planning; using ClaudeDo.Ui.ViewModels.Planning;
@@ -6,6 +9,15 @@ namespace ClaudeDo.Ui.Tests.ViewModels;
public class PlanningDiffViewModelTests public class PlanningDiffViewModelTests
{ {
public PlanningDiffViewModelTests()
{
var dir = AppContext.BaseDirectory;
while (dir is not null && !Directory.Exists(Path.Combine(dir, "src", "ClaudeDo.Localization", "locales")))
dir = Path.GetDirectoryName(dir);
Loc.Current = new Localizer(
LocaleStore.Load(Path.Combine(dir!, "src", "ClaudeDo.Localization", "locales")), "en");
}
private sealed class FakePlanningWorker : StubWorkerClient private sealed class FakePlanningWorker : StubWorkerClient
{ {
public IReadOnlyList<SubtaskDiffDto> AggregateResult { get; set; } = Array.Empty<SubtaskDiffDto>(); public IReadOnlyList<SubtaskDiffDto> AggregateResult { get; set; } = Array.Empty<SubtaskDiffDto>();

View File

@@ -1,4 +1,7 @@
using System.IO;
using ClaudeDo.Data.Models; using ClaudeDo.Data.Models;
using ClaudeDo.Localization;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.ViewModels.Islands; using ClaudeDo.Ui.ViewModels.Islands;
using Xunit; using Xunit;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus; using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
@@ -7,6 +10,15 @@ namespace ClaudeDo.Worker.Tests.UiVm;
public class TaskRowViewModelPlanningTests public class TaskRowViewModelPlanningTests
{ {
public TaskRowViewModelPlanningTests()
{
var dir = AppContext.BaseDirectory;
while (dir is not null && !Directory.Exists(Path.Combine(dir, "src", "ClaudeDo.Localization", "locales")))
dir = Path.GetDirectoryName(dir);
Loc.Current = new Localizer(
LocaleStore.Load(Path.Combine(dir!, "src", "ClaudeDo.Localization", "locales")), "en");
}
private static TaskRowViewModel MakeRow( private static TaskRowViewModel MakeRow(
TaskStatus status, TaskStatus status,
string? parentTaskId = null, string? parentTaskId = null,