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>
This commit is contained in:
mika kuns
2026-06-03 12:55:08 +02:00
co-authored by Claude Sonnet 4.6
parent 2fbf054a57
commit 364a037cb3
19 changed files with 210 additions and 83 deletions
@@ -2,6 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ClaudeDo.Installer.Pages.InstallPage"
xmlns:loc="clr-namespace:ClaudeDo.Installer.Localization"
d:DataContext="{d:DesignInstance local:InstallPageViewModel}"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -17,8 +18,8 @@
<!-- Header -->
<StackPanel Grid.Row="0" Margin="0,0,0,16">
<TextBlock Text="Installation" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,4"/>
<TextBlock Text="Click Install to build and deploy ClaudeDo."
<TextBlock Text="{loc:Tr installer.install.title}" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,4"/>
<TextBlock Text="{loc:Tr installer.install.subtitle}"
Foreground="{StaticResource TextSecondaryBrush}" TextWrapping="Wrap"/>
</StackPanel>
@@ -89,11 +90,11 @@
<!-- Action buttons -->
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
<Button Content="Cancel" Command="{Binding CancelInstallCommand}"
<Button Content="{loc:Tr installer.nav.cancel}" Command="{Binding CancelInstallCommand}"
Visibility="{Binding IsInstalling, Converter={StaticResource BoolToVisConverter}}"
Margin="0,0,8,0"/>
<Button Content="Launch ClaudeDo" Command="{Binding LaunchAppCommand}"
<Button Content="{loc:Tr installer.install.launch}" Command="{Binding LaunchAppCommand}"
Style="{StaticResource AccentButton}"
Visibility="{Binding IsComplete, Converter={StaticResource BoolToVisConverter}}"/>
</StackPanel>
@@ -3,6 +3,7 @@ using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Localization;
using ClaudeDo.Installer.Steps;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -29,7 +30,7 @@ public partial class InstallPageViewModel : ObservableObject, IInstallerPage
private InstallPageView? _view;
private CancellationTokenSource? _cts;
public string Title => "Install";
public string Title => TrExtension.Localizer?["installer.install.title"] ?? "Install";
public string Icon => "\uE896";
public int Order => 99;
public bool ShowInWizard => true;
@@ -2,6 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ClaudeDo.Installer.Pages.PathsPage"
xmlns:loc="clr-namespace:ClaudeDo.Installer.Localization"
d:DataContext="{d:DesignInstance local:PathsPageViewModel}"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -9,28 +10,28 @@
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel MaxWidth="520">
<TextBlock Text="Data Paths" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,4"/>
<TextBlock Text="Configure where ClaudeDo stores its data."
<TextBlock Text="{loc:Tr installer.paths.title}" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,4"/>
<TextBlock Text="{loc:Tr installer.paths.subtitle}"
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,20"
TextWrapping="Wrap"/>
<Label Content="Database Path"/>
<Label Content="{loc:Tr installer.paths.databasePath}"/>
<TextBox Text="{Binding DbPath, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,12"/>
<Label Content="Log Directory"/>
<Label Content="{loc:Tr installer.paths.logDirectory}"/>
<TextBox Text="{Binding LogRoot, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,12"/>
<Label Content="Sandbox Root"/>
<Label Content="{loc:Tr installer.paths.sandboxRoot}"/>
<TextBox Text="{Binding SandboxRoot, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,12"/>
<Label Content="Worktree Strategy"/>
<Label Content="{loc:Tr installer.paths.worktreeStrategy}"/>
<ComboBox SelectedItem="{Binding WorktreeRootStrategy}" Margin="0,0,0,12">
<sys:String xmlns:sys="clr-namespace:System;assembly=mscorlib">sibling</sys:String>
<sys:String xmlns:sys="clr-namespace:System;assembly=mscorlib">central</sys:String>
</ComboBox>
<StackPanel Visibility="{Binding IsCentralVisible, Converter={StaticResource BoolToVisConverter}}">
<Label Content="Central Worktree Root"/>
<Label Content="{loc:Tr installer.paths.centralWorktreeRoot}"/>
<TextBox Text="{Binding CentralWorktreeRoot, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,12"/>
</StackPanel>
@@ -1,5 +1,6 @@
using System.Windows.Controls;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Localization;
using CommunityToolkit.Mvvm.ComponentModel;
namespace ClaudeDo.Installer.Pages.PathsPage;
@@ -9,7 +10,7 @@ public partial class PathsPageViewModel : ObservableObject, IInstallerPage
private readonly InstallContext _context;
private PathsPageView? _view;
public string Title => "Paths";
public string Title => TrExtension.Localizer?["installer.paths.title"] ?? "Paths";
public string Icon => "\uE8B7";
public int Order => 1;
public bool ShowInWizard => true;
@@ -2,6 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ClaudeDo.Installer.Pages.ServicePage"
xmlns:loc="clr-namespace:ClaudeDo.Installer.Localization"
d:DataContext="{d:DesignInstance local:ServicePageViewModel}"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -9,37 +10,37 @@
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel MaxWidth="520">
<TextBlock Text="Worker" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,4"/>
<TextBlock Text="Configure the ClaudeDo background worker."
<TextBlock Text="{loc:Tr installer.service.title}" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,4"/>
<TextBlock Text="{loc:Tr installer.service.subtitle}"
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,20"
TextWrapping="Wrap"/>
<Label Content="SignalR Port"/>
<Label Content="{loc:Tr installer.service.signalRPort}"/>
<TextBox Text="{Binding SignalRPort, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,12"/>
<Label Content="Queue Backstop Interval (ms)"/>
<Label Content="{loc:Tr installer.service.queueBackstopInterval}"/>
<TextBox Text="{Binding QueueBackstopIntervalMs, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,12"/>
<Label Content="Claude CLI Path"/>
<Label Content="{loc:Tr installer.service.claudeCliPath}"/>
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding ClaudeBin, UpdateSourceTrigger=PropertyChanged}"/>
<Button Grid.Column="1" Content="Browse..." Command="{Binding BrowseClaudeCommand}"
<Button Grid.Column="1" Content="{loc:Tr installer.nav.browse}" Command="{Binding BrowseClaudeCommand}"
Margin="8,0,0,0"/>
</Grid>
<Separator Margin="0,4,0,12"/>
<TextBlock Text="The worker runs as you (the logged-in user) via a per-user logon task, so it can use your Claude CLI authentication."
<TextBlock Text="{loc:Tr installer.service.autostartHint}"
Foreground="{StaticResource TextDimBrush}" FontSize="11" Margin="0,0,0,12"
TextWrapping="Wrap"/>
<CheckBox Content="Start worker automatically at logon" IsChecked="{Binding AutoStart}" Margin="0,0,0,12"/>
<CheckBox Content="{loc:Tr installer.service.autostart}" IsChecked="{Binding AutoStart}" Margin="0,0,0,12"/>
<Label Content="Restart Delay (ms)"/>
<Label Content="{loc:Tr installer.service.restartDelay}"/>
<TextBox Text="{Binding RestartDelayMs, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,12"/>
<TextBlock Text="{Binding ValidationError}" Foreground="{StaticResource ErrorBrush}" FontSize="11"
@@ -1,5 +1,6 @@
using System.Windows.Controls;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Localization;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Win32;
@@ -11,7 +12,7 @@ public partial class ServicePageViewModel : ObservableObject, IInstallerPage
private readonly InstallContext _context;
private ServicePageView? _view;
public string Title => "Service";
public string Title => TrExtension.Localizer?["installer.service.title"] ?? "Service";
public string Icon => "\uE912";
public int Order => 2;
public bool ShowInWizard => true;
@@ -2,6 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ClaudeDo.Installer.Pages.UiSettingsPage"
xmlns:loc="clr-namespace:ClaudeDo.Installer.Localization"
d:DataContext="{d:DesignInstance local:UiSettingsPageViewModel}"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -9,22 +10,22 @@
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel MaxWidth="520">
<TextBlock Text="UI Settings" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,4"/>
<TextBlock Text="Configure the ClaudeDo desktop UI connection settings."
<TextBlock Text="{loc:Tr installer.uiSettings.title}" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,4"/>
<TextBlock Text="{loc:Tr installer.uiSettings.subtitle}"
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,20"
TextWrapping="Wrap"/>
<CheckBox Content="Sync with service settings" IsChecked="{Binding IsSynced}" Margin="0,0,0,16"/>
<CheckBox Content="{loc:Tr installer.uiSettings.syncWithService}" IsChecked="{Binding IsSynced}" Margin="0,0,0,16"/>
<Label Content="SignalR URL"/>
<Label Content="{loc:Tr installer.uiSettings.signalRUrl}"/>
<TextBox Text="{Binding SignalRUrl, UpdateSourceTrigger=PropertyChanged}"
IsReadOnly="{Binding IsSynced}" Margin="0,0,0,12"/>
<Label Content="Database Path"/>
<Label Content="{loc:Tr installer.paths.databasePath}"/>
<TextBox Text="{Binding UiDbPath, UpdateSourceTrigger=PropertyChanged}"
IsReadOnly="{Binding IsSynced}" Margin="0,0,0,12"/>
<TextBlock Text="When synced, these values are derived from the Service and Paths pages."
<TextBlock Text="{loc:Tr installer.uiSettings.syncHint}"
Foreground="{StaticResource TextDimBrush}" FontSize="11" TextWrapping="Wrap"
Visibility="{Binding IsSynced, Converter={StaticResource BoolToVisConverter}}"
Margin="0,0,0,12"/>
@@ -1,5 +1,6 @@
using System.Windows.Controls;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Localization;
using CommunityToolkit.Mvvm.ComponentModel;
namespace ClaudeDo.Installer.Pages.UiSettingsPage;
@@ -9,7 +10,7 @@ public partial class UiSettingsPageViewModel : ObservableObject, IInstallerPage
private readonly InstallContext _context;
private UiSettingsPageView? _view;
public string Title => "UI Settings";
public string Title => TrExtension.Localizer?["installer.uiSettings.title"] ?? "UI Settings";
public string Icon => "\uE771";
public int Order => 3;
public bool ShowInWizard => true;
@@ -2,6 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ClaudeDo.Installer.Pages.WelcomePage"
xmlns:loc="clr-namespace:ClaudeDo.Installer.Localization"
d:DataContext="{d:DesignInstance local:WelcomePageViewModel}"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -14,7 +15,7 @@
<TextBlock Text="{Binding Subheading}" TextWrapping="Wrap"
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,24"/>
<Label Content="Install Directory"/>
<Label Content="{loc:Tr installer.welcome.installDirectory}"/>
<Grid Margin="0,0,0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
@@ -24,7 +25,7 @@
Text="{Binding InstallDirectory, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding InstallDirEditable}"/>
<Button Grid.Column="1"
Content="Browse..."
Content="{loc:Tr installer.nav.browse}"
Margin="8,0,0,0"
Command="{Binding BrowseInstallCommand}"
IsEnabled="{Binding InstallDirEditable}"/>
@@ -32,10 +33,10 @@
<TextBlock Text="{Binding InstallError}" Foreground="{StaticResource ErrorBrush}" FontSize="11"
Visibility="{Binding InstallError, Converter={StaticResource NullToCollapsedConverter}}"/>
<CheckBox Content="Register MCP server with Claude"
<CheckBox Content="{loc:Tr installer.welcome.registerMcp}"
IsChecked="{Binding RegisterMcp}"
Margin="0,24,0,0"/>
<TextBlock Text="Runs 'claude mcp add' so Claude can view and manage your ClaudeDo tasks. You can change this later."
<TextBlock Text="{loc:Tr installer.welcome.registerMcpHint}"
TextWrapping="Wrap" FontSize="11"
Foreground="{StaticResource TextSecondaryBrush}"
Margin="0,4,0,0"/>
@@ -1,6 +1,7 @@
using System.IO;
using System.Windows.Controls;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Localization;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Win32;
@@ -12,7 +13,7 @@ public partial class WelcomePageViewModel : ObservableObject, IInstallerPage
private readonly InstallContext _context;
private WelcomePageView? _view;
public string Title => "Welcome";
public string Title => TrExtension.Localizer?["installer.welcome.title"] ?? "Welcome";
public string Icon => "\uE80F";
public int Order => 0;
public bool ShowInWizard => true;
@@ -37,17 +38,18 @@ public partial class WelcomePageViewModel : ObservableObject, IInstallerPage
? @"C:\Program Files\ClaudeDo"
: _context.InstallDirectory;
var loc = TrExtension.Localizer;
switch (_context.Mode)
{
case InstallerMode.FreshInstall:
Heading = "Install ClaudeDo";
Subheading = "Choose where to install ClaudeDo, then click Next.";
Heading = loc?["installer.welcome.heading"] ?? "Install ClaudeDo";
Subheading = loc?["installer.welcome.subheading"] ?? "Choose where to install ClaudeDo, then click Next.";
InstallDirEditable = true;
break;
case InstallerMode.Update:
Heading = $"Update ClaudeDo {_context.InstalledVersion ?? "?"} -> {_context.LatestVersion ?? "?"}";
Subheading = "Your tasks, config, and database will be preserved. Click Next to continue.";
Subheading = loc?["installer.welcome.updateSubheading"] ?? "Your tasks, config, and database will be preserved. Click Next to continue.";
InstallDirEditable = false; // stay where we were installed
break;