Files
ClaudeDo/src/ClaudeDo.Installer/Views/SettingsViewModel.cs
mika kuns 364a037cb3 feat(i18n): localize installer with language picker and config write-through
- Init Localizer at app startup (before self-update prompt) and assign to TrExtension.Localizer
- Register ILocalizer in DI; inject into WizardViewModel and SettingsViewModel
- WizardViewModel: SelectedLanguage ComboBox binding with OnSelectedLanguageChanged -> SetLanguage + InstallContext.Language
- WizardWindow.xaml: DockPanel wraps step chips + language ComboBox (right-aligned)
- Localize all installer XAML: WizardWindow, WelcomePage, PathsPage, ServicePage, UiSettingsPage, InstallPage, SettingsWindow, SelfUpdatePromptWindow
- Localize page Title properties and WizardViewModel.NextButtonText via TrExtension.Localizer
- Persist chosen Language in WriteConfigStep and SettingsViewModel.Save into ui.config.json
- Append installer section to en.json (nav, welcome, paths, service, uiSettings, install, settings, selfUpdate)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 12:55:08 +02:00

214 lines
6.9 KiB
C#

using System.Windows;
using ClaudeDo.Installer.Core;
using ClaudeDo.Localization;
using ClaudeDo.Releases;
using ClaudeDo.Installer.Steps;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ClaudeDo.Installer.Views;
public partial class SettingsViewModel : ObservableObject
{
private readonly InstallContext _context;
private readonly ILocalizer _localizer;
private readonly IReleaseClient _releases;
private readonly StopWorkerStep _stopService;
private readonly StartWorkerStep _startService;
private readonly RegisterAutostartStep _registerAutostart;
private readonly DownloadAndExtractStep _downloadStep;
private readonly UninstallRunner _uninstallRunner;
public IReadOnlyList<IInstallerPage> Pages { get; }
[ObservableProperty]
private IInstallerPage? _selectedPage;
[ObservableProperty]
private string? _statusMessage;
[ObservableProperty]
private bool _isStatusError;
[ObservableProperty]
private string _versionLabel = "";
[ObservableProperty]
private bool _removeAppData;
public SettingsViewModel(
PageResolver resolver,
InstallContext context,
ILocalizer localizer,
IReleaseClient releases,
StopWorkerStep stopService,
StartWorkerStep startService,
RegisterAutostartStep registerAutostart,
DownloadAndExtractStep downloadStep,
UninstallRunner uninstallRunner)
{
Pages = resolver.SettingsPages;
_context = context;
_localizer = localizer;
_releases = releases;
_stopService = stopService;
_startService = startService;
_registerAutostart = registerAutostart;
_downloadStep = downloadStep;
_uninstallRunner = uninstallRunner;
_selectedPage = Pages.FirstOrDefault();
var label = $"Installed: {context.InstalledVersion ?? "?"} Installer: {context.InstallerVersion ?? "?"}";
if (!string.IsNullOrEmpty(context.LatestVersion))
label += $" Latest: {context.LatestVersion}";
if (context.LatestTagUnparseable)
label += " (pre-release tag — auto-update disabled)";
VersionLabel = label;
_ = LoadAllAsync();
}
private async Task LoadAllAsync()
{
foreach (var page in Pages)
await page.LoadAsync();
}
[RelayCommand]
private async Task Save()
{
foreach (var page in Pages)
{
if (!page.Validate())
{
SelectedPage = page;
StatusMessage = $"Validation failed on {page.Title}. Please fix the errors.";
IsStatusError = true;
return;
}
}
foreach (var page in Pages)
await page.ApplyAsync();
var workerCfg = new InstallerWorkerConfig
{
DbPath = _context.DbPath,
SandboxRoot = _context.SandboxRoot,
LogRoot = _context.LogRoot,
WorktreeRootStrategy = _context.WorktreeRootStrategy,
CentralWorktreeRoot = _context.CentralWorktreeRoot,
QueueBackstopIntervalMs = _context.QueueBackstopIntervalMs,
SignalRPort = _context.SignalRPort,
ClaudeBin = _context.ClaudeBin,
};
workerCfg.Save();
var uiCfg = new InstallerAppSettings
{
DbPath = _context.UiDbPath,
SignalRUrl = _context.SignalRUrl,
Language = _localizer.CurrentCode,
};
uiCfg.Save();
IsStatusError = false;
StatusMessage = "Settings saved.";
// Worker reads its config at process start, so changes only take effect after a restart.
var restart = MessageBox.Show(
"Restart the worker service now so the new settings take effect?",
"Restart Worker",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (restart != MessageBoxResult.Yes)
{
StatusMessage = "Settings saved. Restart the worker service manually to apply.";
return;
}
var progress = new Progress<string>(msg => StatusMessage = msg);
var stop = await _stopService.ExecuteAsync(_context, progress, CancellationToken.None);
if (!stop.Success)
{
StatusMessage = $"Saved, but worker stop failed: {stop.ErrorMessage}";
IsStatusError = true;
return;
}
var start = await _startService.ExecuteAsync(_context, progress, CancellationToken.None);
if (!start.Success)
{
StatusMessage = $"Saved, but worker start failed: {start.ErrorMessage}";
IsStatusError = true;
return;
}
StatusMessage = "Settings saved. Worker restarted.";
}
[RelayCommand]
private async Task Repair()
{
var confirm = MessageBox.Show(
"Re-download and reinstall the ClaudeDo binaries? Your config and database are NOT affected.",
"Repair ClaudeDo",
MessageBoxButton.OKCancel,
MessageBoxImage.Question);
if (confirm != MessageBoxResult.OK) return;
StatusMessage = "Repairing...";
IsStatusError = false;
var progress = new Progress<string>(msg => StatusMessage = msg);
var steps = new IInstallStep[] { _stopService, _downloadStep, _registerAutostart, _startService };
foreach (var step in steps)
{
var r = await step.ExecuteAsync(_context, progress, CancellationToken.None);
if (!r.Success)
{
StatusMessage = $"{step.Name} failed: {r.ErrorMessage}";
IsStatusError = true;
return;
}
}
StatusMessage = "Repair complete.";
}
[RelayCommand]
private async Task Uninstall()
{
var dataNote = RemoveAppData
? "This will remove ClaudeDo AND delete all of your tasks, configuration, and database.\n\nContinue?"
: "This will remove ClaudeDo. Your tasks, configuration, and database in ~/.todo-app will be kept.\n\nContinue?";
var confirm = MessageBox.Show(
dataNote,
"Uninstall ClaudeDo",
MessageBoxButton.YesNo,
MessageBoxImage.Warning);
if (confirm != MessageBoxResult.Yes) return;
var progress = new Progress<string>(msg => StatusMessage = msg);
var r = await _uninstallRunner.RunAsync(RemoveAppData, progress, CancellationToken.None);
if (!r.Success)
{
StatusMessage = $"Uninstall failed: {r.ErrorMessage}";
IsStatusError = true;
return;
}
MessageBox.Show("ClaudeDo has been removed.", "Uninstall complete",
MessageBoxButton.OK, MessageBoxImage.Information);
Application.Current.Shutdown();
}
[RelayCommand]
private void Close() => Application.Current.Shutdown();
}