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
@@ -1,6 +1,7 @@
<Window x:Class="ClaudeDo.Installer.Views.SelfUpdatePromptWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:ClaudeDo.Installer.Localization"
Title="ClaudeDo Installer Update"
Width="460" Height="200"
WindowStartupLocation="CenterScreen"
@@ -13,13 +14,13 @@
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" FontSize="16" FontWeight="SemiBold" Text="A newer installer is available"/>
<TextBlock Grid.Row="0" FontSize="16" FontWeight="SemiBold" Text="{loc:Tr installer.selfUpdate.heading}"/>
<TextBlock Grid.Row="1" Margin="0,8,0,0" TextWrapping="Wrap" x:Name="DetailText"/>
<TextBlock Grid.Row="2" Margin="0,12,0,0" TextWrapping="Wrap" Foreground="#a0a0a0" x:Name="ProgressText" Visibility="Collapsed"/>
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="UpdateBtn" Content="Update" MinWidth="90" Margin="4,0" Padding="10,4" Click="UpdateBtn_Click" IsDefault="True"/>
<Button x:Name="ContinueBtn" Content="Continue anyway" MinWidth="140" Margin="4,0" Padding="10,4" Click="ContinueBtn_Click"/>
<Button x:Name="CancelBtn" Content="Cancel" MinWidth="90" Margin="4,0" Padding="10,4" Click="CancelBtn_Click" IsCancel="True"/>
<Button x:Name="UpdateBtn" Content="{loc:Tr installer.selfUpdate.update}" MinWidth="90" Margin="4,0" Padding="10,4" Click="UpdateBtn_Click" IsDefault="True"/>
<Button x:Name="ContinueBtn" Content="{loc:Tr installer.selfUpdate.continueAnyway}" MinWidth="140" Margin="4,0" Padding="10,4" Click="ContinueBtn_Click"/>
<Button x:Name="CancelBtn" Content="{loc:Tr installer.nav.cancel}" MinWidth="90" Margin="4,0" Padding="10,4" Click="CancelBtn_Click" IsCancel="True"/>
</StackPanel>
</Grid>
</Window>
@@ -1,5 +1,6 @@
using System.Windows;
using ClaudeDo.Installer.Core;
using ClaudeDo.Localization;
using ClaudeDo.Releases;
using ClaudeDo.Installer.Steps;
using CommunityToolkit.Mvvm.ComponentModel;
@@ -10,6 +11,7 @@ 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;
@@ -37,6 +39,7 @@ public partial class SettingsViewModel : ObservableObject
public SettingsViewModel(
PageResolver resolver,
InstallContext context,
ILocalizer localizer,
IReleaseClient releases,
StopWorkerStep stopService,
StartWorkerStep startService,
@@ -46,6 +49,7 @@ public partial class SettingsViewModel : ObservableObject
{
Pages = resolver.SettingsPages;
_context = context;
_localizer = localizer;
_releases = releases;
_stopService = stopService;
_startService = startService;
@@ -104,6 +108,7 @@ public partial class SettingsViewModel : ObservableObject
{
DbPath = _context.UiDbPath,
SignalRUrl = _context.SignalRUrl,
Language = _localizer.CurrentCode,
};
uiCfg.Save();
@@ -2,6 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:ClaudeDo.Installer.Views"
xmlns:loc="clr-namespace:ClaudeDo.Installer.Localization"
Title="ClaudeDo Settings"
Icon="/ClaudeTaskSetup.ico"
Width="720" Height="520"
@@ -90,16 +91,16 @@
</StackPanel>
<CheckBox Grid.Column="1" IsChecked="{Binding RemoveAppData}"
Content="Remove user data (tasks, logs, configs in ~/.todo-app)"
Content="{loc:Tr installer.settings.removeUserData}"
Margin="0,0,12,0" VerticalAlignment="Center"/>
<Button Grid.Column="2" Content="Uninstall" Margin="0,0,8,0"
<Button Grid.Column="2" Content="{loc:Tr installer.settings.uninstall}" Margin="0,0,8,0"
Command="{Binding UninstallCommand}"/>
<Button Grid.Column="3" Content="Repair" Margin="0,0,8,0"
<Button Grid.Column="3" Content="{loc:Tr installer.settings.repair}" Margin="0,0,8,0"
Command="{Binding RepairCommand}"/>
<Button Grid.Column="4" Content="Save" Margin="0,0,8,0"
<Button Grid.Column="4" Content="{loc:Tr installer.settings.save}" Margin="0,0,8,0"
Command="{Binding SaveCommand}"
Style="{StaticResource AccentButton}"/>
<Button Grid.Column="5" Content="Close"
<Button Grid.Column="5" Content="{loc:Tr installer.settings.close}"
Command="{Binding CloseCommand}"/>
</Grid>
</Border>
@@ -3,6 +3,7 @@ using System.Windows;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Pages.InstallPage;
using ClaudeDo.Installer.Pages.WelcomePage;
using ClaudeDo.Localization;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -11,8 +12,20 @@ namespace ClaudeDo.Installer.Views;
public partial class WizardViewModel : ObservableObject
{
private readonly InstallContext _context;
private readonly ILocalizer _localizer;
public IReadOnlyList<IInstallerPage> Pages { get; }
public IReadOnlyList<LanguageOption> Languages { get; }
[ObservableProperty]
private LanguageOption? _selectedLanguage;
partial void OnSelectedLanguageChanged(LanguageOption? value)
{
if (value is null) return;
_localizer.SetLanguage(value.Value.Code);
_context.Language = value.Value.Code;
}
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanGoBack))]
@@ -24,14 +37,20 @@ public partial class WizardViewModel : ObservableObject
public IInstallerPage CurrentPage => Pages[CurrentPageIndex];
public bool CanGoBack => CurrentPageIndex > 0;
public bool IsLastPage => CurrentPageIndex == Pages.Count - 1;
public string NextButtonText => IsLastPage ? "Install" : "Next \u2192";
public string NextButtonText => IsLastPage
? (_localizer["installer.nav.install"])
: (_localizer["installer.nav.next"]);
[ObservableProperty]
private string? _validationError;
public WizardViewModel(PageResolver resolver, InstallContext context)
public WizardViewModel(PageResolver resolver, InstallContext context, ILocalizer localizer)
{
_context = context;
_localizer = localizer;
Languages = localizer.AvailableLanguages;
_selectedLanguage = Languages.FirstOrDefault(l => l.Code == localizer.CurrentCode);
_context.Language = localizer.CurrentCode;
var all = resolver.WizardPages;
Pages = context.Mode == InstallerMode.Update
+40 -32
View File
@@ -2,6 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:ClaudeDo.Installer.Views"
xmlns:loc="clr-namespace:ClaudeDo.Installer.Localization"
Title="ClaudeDo Installer"
Icon="/ClaudeTaskSetup.ico"
Width="720" Height="520"
@@ -27,40 +28,47 @@
<Border Grid.Row="0" Background="{StaticResource IslandBgBrush}"
BorderBrush="{StaticResource BorderSubtleBrush}" BorderThickness="0,0,0,1"
Padding="20,14">
<ItemsControl ItemsSource="{Binding Pages}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border x:Name="StepBorder" CornerRadius="4" Padding="10,5" Margin="0,0,6,0"
BorderThickness="1">
<Border.Background>
<MultiBinding Converter="{StaticResource StepActiveConverter}" ConverterParameter="Background">
<Binding/>
<Binding RelativeSource="{RelativeSource AncestorType=Window}" Path="DataContext.CurrentPage"/>
</MultiBinding>
</Border.Background>
<Border.BorderBrush>
<MultiBinding Converter="{StaticResource StepActiveConverter}" ConverterParameter="BorderBrush">
<Binding/>
<Binding RelativeSource="{RelativeSource AncestorType=Window}" Path="DataContext.CurrentPage"/>
</MultiBinding>
</Border.BorderBrush>
<TextBlock Text="{Binding Title}" FontSize="12">
<TextBlock.Foreground>
<MultiBinding Converter="{StaticResource StepActiveConverter}" ConverterParameter="Foreground">
<DockPanel>
<ComboBox DockPanel.Dock="Right"
ItemsSource="{Binding Languages}"
SelectedItem="{Binding SelectedLanguage, Mode=TwoWay}"
DisplayMemberPath="Name"
Width="150" HorizontalAlignment="Right" VerticalAlignment="Center"/>
<ItemsControl ItemsSource="{Binding Pages}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border x:Name="StepBorder" CornerRadius="4" Padding="10,5" Margin="0,0,6,0"
BorderThickness="1">
<Border.Background>
<MultiBinding Converter="{StaticResource StepActiveConverter}" ConverterParameter="Background">
<Binding/>
<Binding RelativeSource="{RelativeSource AncestorType=Window}" Path="DataContext.CurrentPage"/>
</MultiBinding>
</TextBlock.Foreground>
</TextBlock>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border.Background>
<Border.BorderBrush>
<MultiBinding Converter="{StaticResource StepActiveConverter}" ConverterParameter="BorderBrush">
<Binding/>
<Binding RelativeSource="{RelativeSource AncestorType=Window}" Path="DataContext.CurrentPage"/>
</MultiBinding>
</Border.BorderBrush>
<TextBlock Text="{Binding Title}" FontSize="12">
<TextBlock.Foreground>
<MultiBinding Converter="{StaticResource StepActiveConverter}" ConverterParameter="Foreground">
<Binding/>
<Binding RelativeSource="{RelativeSource AncestorType=Window}" Path="DataContext.CurrentPage"/>
</MultiBinding>
</TextBlock.Foreground>
</TextBlock>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DockPanel>
</Border>
<!-- Page Content -->
@@ -85,7 +93,7 @@
VerticalAlignment="Center" FontSize="12"
Visibility="{Binding ValidationError, Converter={StaticResource NullToCollapsedConverter}}"/>
<Button Grid.Column="1" Content="Back"
<Button Grid.Column="1" Content="{loc:Tr installer.nav.back}"
Command="{Binding GoBackCommand}"
IsEnabled="{Binding CanGoBack}"
Margin="0,0,8,0" MinWidth="80"/>