i18n: 5 languages (FR/EN/ZH/TH/AR), auto-detect from Windows + Settings picker

Localization infrastructure
---------------------------
PSLauncher.Core/Localization/Strings.cs is a static class exposing each
UI string as a static property. T(fr,en,zh,th,ar) helper switches by the
current language code. ~50 keys cover the visible top-level strings:
top bar, body, status badges, action buttons, "..." menu items, license
badge texts, Settings section headers, Onboarding dialog, Update dialog.

Bindings via x:Static in XAML:
  xmlns:loc="clr-namespace:PSLauncher.Core.Localization;assembly=PSLauncher.Core"
  Content="{x:Static loc:Strings.ActionLaunch}"

Auto-detection
--------------
LocalConfig gains a Language field defaulting to "auto". Strings.Init()
called at App.xaml.cs OnStartup before any UI:
- "auto" (or unknown code) → reads CultureInfo.CurrentUICulture
  .TwoLetterISOLanguageName, picks the matching supported language,
  falls back to English.
- explicit code → forced.
The chosen culture is then propagated to CurrentCulture / CurrentUICulture
so date/number formats follow.

Settings picker
---------------
SettingsDialog gets a top section "LANGUE" with a ComboBox bound to
SettingsViewModel.AvailableLanguages (Auto / FR / EN / ZH / TH / AR).
On Save, if the language code changed, prompt the user to confirm
restart, spawn `cmd /c timeout 1 & start PSLauncher.exe` and Shutdown
the current process — the new instance picks up the language at
bootstrap.

RTL for Arabic
--------------
Strings.IsRightToLeft is true when lang=="ar". App.xaml.cs sets
window.FlowDirection = RightToLeft on the MainWindow — WPF mirrors the
layout (icons on right, text aligned right).

Translations
------------
Done as best-effort by the assistant. The product wordmark "PROSERVE"
stays untranslated (brand). Logs and debug-level messages remain in
French in code — only user-visible UI is localized. Operator can
refine translations by editing Strings.cs and rebuilding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-02 13:02:50 +02:00
parent 9c1b34b041
commit 01a11d5616
7 changed files with 286 additions and 33 deletions

View File

@@ -14,6 +14,7 @@ using PSLauncher.Core.Downloads;
using PSLauncher.Core.Installations;
using PSLauncher.Core.Integrity;
using PSLauncher.Core.Licensing;
using PSLauncher.Core.Localization;
using PSLauncher.Core.Manifests;
using PSLauncher.Core.Process;
using PSLauncher.Core.Updates;
@@ -63,6 +64,20 @@ public partial class App : Application
"PSLauncher", "logs");
Directory.CreateDirectory(LogsDirectory);
// Bootstrap i18n : on lit d'abord la config (sans services DI) pour fixer la
// langue avant que le moindre élément XAML soit instancié.
try
{
var bootstrapStore = new ConfigStore(
Microsoft.Extensions.Logging.Abstractions.NullLogger<ConfigStore>.Instance);
var bootstrapCfg = bootstrapStore.Load();
Strings.Init(bootstrapCfg.Language);
}
catch
{
Strings.Init("auto");
}
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.Enrich.FromLogContext()
@@ -143,6 +158,9 @@ public partial class App : Application
var window = _host.Services.GetRequiredService<MainWindow>();
window.DataContext = _host.Services.GetRequiredService<MainViewModel>();
// Mise en page droite-à-gauche pour les langues RTL (arabe).
if (Strings.IsRightToLeft)
window.FlowDirection = FlowDirection.RightToLeft;
MainWindow = window;
window.Show();
}

View File

@@ -13,6 +13,8 @@ using PSLauncher.Models;
namespace PSLauncher.App.ViewModels;
public sealed record LanguageOption(string Code, string Name);
public sealed partial class SettingsViewModel : ObservableObject
{
private readonly IConfigStore _configStore;
@@ -29,6 +31,12 @@ public sealed partial class SettingsViewModel : ObservableObject
[ObservableProperty] private string _serverBaseUrl;
[ObservableProperty] private string _installRoot;
[ObservableProperty] private string _language;
public IReadOnlyList<LanguageOption> AvailableLanguages { get; } =
PSLauncher.Core.Localization.Strings.Available
.Select(t => new LanguageOption(t.Code, t.Name))
.ToList();
[ObservableProperty] private string? _connectionStatus;
[ObservableProperty] private bool _isTestingConnection;
[ObservableProperty] private string _cacheSizeDisplay = "—";
@@ -102,6 +110,7 @@ public sealed partial class SettingsViewModel : ObservableObject
_serverBaseUrl = config.ServerBaseUrl;
_installRoot = config.InstallRoot;
_language = string.IsNullOrWhiteSpace(config.Language) ? "auto" : config.Language;
LauncherVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0";
MachineId = licenseService.GetMachineId();
@@ -201,10 +210,48 @@ public sealed partial class SettingsViewModel : ObservableObject
[RelayCommand]
private void Save()
{
var languageChanged = !string.Equals(
(_config.Language ?? "auto").ToLowerInvariant(),
(Language ?? "auto").ToLowerInvariant(),
StringComparison.Ordinal);
_config.ServerBaseUrl = ServerBaseUrl.TrimEnd('/');
_config.InstallRoot = InstallRoot;
_config.Language = Language ?? "auto";
_configStore.Save(_config);
_logger.LogInformation("Settings saved");
_logger.LogInformation("Settings saved (language={Lang}, changed={Changed})", _config.Language, languageChanged);
if (languageChanged)
{
// Le launcher doit être relancé pour que les bindings x:Static reflètent
// la nouvelle langue. Plutôt que de demander à l'utilisateur de fermer/rouvrir,
// on spawne un cmd qui attend la fermeture puis relance.
var ok = MessageBox.Show(
"Le launcher va redémarrer pour appliquer la nouvelle langue.",
"Changement de langue",
MessageBoxButton.OKCancel, MessageBoxImage.Information, MessageBoxResult.OK);
if (ok == MessageBoxResult.OK)
{
try
{
var exe = System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName;
if (!string.IsNullOrEmpty(exe))
{
var pid = Environment.ProcessId;
var cmd = $"/c (echo Wait && timeout /t 1 /nobreak > nul) & start \"\" \"{exe}\"";
Process.Start(new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = cmd,
UseShellExecute = false,
CreateNoWindow = true,
});
Application.Current.Shutdown();
}
}
catch (Exception ex) { _logger.LogError(ex, "Restart on language change failed"); }
}
}
}
[RelayCommand]

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:vm="clr-namespace:PSLauncher.App.ViewModels"
xmlns:loc="clr-namespace:PSLauncher.Core.Localization;assembly=PSLauncher.Core"
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
Title="PROSERVE Launcher"
Icon="pack://application:,,,/Resources/favicon.ico"
@@ -113,7 +114,7 @@
<!-- Action button -->
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center" Margin="12,0,0,0">
<Button Style="{StaticResource PrimaryButton}"
Content="Lancer" Padding="22,8" FontSize="13"
Content="{x:Static loc:Strings.ActionLaunch}" Padding="22,8" FontSize="13"
Command="{Binding LaunchCommand}"
Visibility="{Binding IsInstalled, Converter={StaticResource BoolToVisibility}}" />
<Button Style="{StaticResource AccentButton}"
@@ -143,13 +144,13 @@
Click="OnMoreMenuClick">
<Button.ContextMenu>
<ContextMenu>
<MenuItem Header="📜 Voir les release notes"
<MenuItem Header="{x:Static loc:Strings.MenuReleaseNotes}"
Command="{Binding PlacementTarget.Tag.ShowReleaseNotesCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
<MenuItem Header="📁 Ouvrir le dossier"
<MenuItem Header="{x:Static loc:Strings.MenuOpenFolder}"
Command="{Binding PlacementTarget.Tag.OpenFolderCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
Visibility="{Binding PlacementTarget.Tag.IsInstalled, RelativeSource={RelativeSource AncestorType=ContextMenu}, Converter={StaticResource BoolToVisibility}}" />
<Separator Visibility="{Binding PlacementTarget.Tag.IsInstalled, RelativeSource={RelativeSource AncestorType=ContextMenu}, Converter={StaticResource BoolToVisibility}}" />
<MenuItem Header="🗑 Supprimer cette version…"
<MenuItem Header="{x:Static loc:Strings.MenuUninstall}"
Command="{Binding PlacementTarget.Tag.UninstallCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
Visibility="{Binding PlacementTarget.Tag.IsInstalled, RelativeSource={RelativeSource AncestorType=ContextMenu}, Converter={StaticResource BoolToVisibility}}" />
</ContextMenu>
@@ -219,7 +220,7 @@
Cursor="Hand"
CornerRadius="16" Padding="14,6"
shell:WindowChrome.IsHitTestVisibleInChrome="True"
ToolTip="Voir / changer la license">
ToolTip="{x:Static loc:Strings.LicenseTooltip}">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="#2A1414" />
@@ -283,7 +284,7 @@
<StackPanel Grid.Column="2" Orientation="Horizontal"
HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Style="{StaticResource SecondaryButton}"
Content="⚙ Paramètres"
Content="{x:Static loc:Strings.TopBarSettings}"
shell:WindowChrome.IsHitTestVisibleInChrome="True"
Command="{Binding OpenSettingsCommand}"
Margin="0,0,16,0" />
@@ -368,7 +369,7 @@
<!-- Label "Version courante" -->
<TextBlock Grid.Row="0" Grid.Column="0"
Text="VERSION COURANTE"
Text="{x:Static loc:Strings.FeaturedCurrent}"
FontSize="11" FontWeight="Bold"
Foreground="{StaticResource Brush.Text.Secondary}"
Margin="0,0,0,4" />
@@ -412,7 +413,7 @@
<!-- Action principale (gros bouton) -->
<Button Grid.Row="0" Grid.RowSpan="3" Grid.Column="1"
Style="{StaticResource PrimaryButton}"
Content="▶ LANCER"
Content="{x:Static loc:Strings.ActionLaunchBig}"
FontSize="20" Padding="56,16"
VerticalAlignment="Center"
Command="{Binding FeaturedVersion.LaunchCommand}"
@@ -454,13 +455,13 @@
Click="OnMoreMenuClick">
<Button.ContextMenu>
<ContextMenu>
<MenuItem Header="📜 Voir les release notes"
<MenuItem Header="{x:Static loc:Strings.MenuReleaseNotes}"
Command="{Binding PlacementTarget.Tag.ShowReleaseNotesCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
<MenuItem Header="📁 Ouvrir le dossier"
<MenuItem Header="{x:Static loc:Strings.MenuOpenFolder}"
Command="{Binding PlacementTarget.Tag.OpenFolderCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
Visibility="{Binding PlacementTarget.Tag.IsInstalled, RelativeSource={RelativeSource AncestorType=ContextMenu}, Converter={StaticResource BoolToVisibility}}" />
<Separator Visibility="{Binding PlacementTarget.Tag.IsInstalled, RelativeSource={RelativeSource AncestorType=ContextMenu}, Converter={StaticResource BoolToVisibility}}" />
<MenuItem Header="🗑 Supprimer cette version…"
<MenuItem Header="{x:Static loc:Strings.MenuUninstall}"
Command="{Binding PlacementTarget.Tag.UninstallCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
Visibility="{Binding PlacementTarget.Tag.IsInstalled, RelativeSource={RelativeSource AncestorType=ContextMenu}, Converter={StaticResource BoolToVisibility}}" />
</ContextMenu>
@@ -477,7 +478,7 @@
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="AUTRES VERSIONS"
<TextBlock Grid.Column="0" Text="{x:Static loc:Strings.SectionOther}"
FontSize="12" FontWeight="Bold"
Foreground="{StaticResource Brush.Text.Secondary}"
VerticalAlignment="Center" />
@@ -508,7 +509,7 @@
<Button HorizontalAlignment="Left" VerticalAlignment="Bottom"
Margin="32,0,0,24"
Style="{StaticResource SecondaryButton}"
Content="🔄 Vérifier les MAJ"
Content="{x:Static loc:Strings.TopBarCheckUpdates}"
Command="{Binding CheckForUpdatesCommand}" />
<!-- Copyright flottant en bas centré, sur une pill sombre pour rester
@@ -546,7 +547,7 @@
</StackPanel>
<Button Grid.Column="1"
Style="{StaticResource SecondaryButton}"
Content="Annuler"
Content="{x:Static loc:Strings.ActionCancel}"
VerticalAlignment="Center" Margin="12,0,0,0"
Command="{Binding CancelDownloadCommand}"
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibility}}" />

View File

@@ -1,7 +1,8 @@
<Window x:Class="PSLauncher.App.Views.OnboardingDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Activation PROSERVE Launcher"
xmlns:loc="clr-namespace:PSLauncher.Core.Localization;assembly=PSLauncher.Core"
Title="{x:Static loc:Strings.OnboardingTitle}"
Icon="pack://application:,,,/Resources/favicon.ico"
Width="540" Height="420"
MinWidth="480" MinHeight="380"
@@ -18,17 +19,17 @@
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Activer votre license"
<TextBlock Grid.Row="0" Text="{x:Static loc:Strings.OnboardingHeading}"
FontSize="22" FontWeight="SemiBold"
Foreground="{StaticResource Brush.Text.Primary}" />
<TextBlock Grid.Row="1"
Text="Saisis la clé fournie par ASTERION. Elle te donne accès aux téléchargements jusqu'à la date de validité associée."
Text="{x:Static loc:Strings.OnboardingBody}"
TextWrapping="Wrap"
Foreground="{StaticResource Brush.Text.Secondary}"
Margin="0,8,0,18" />
<TextBlock Grid.Row="2" Text="Clé de license"
<TextBlock Grid.Row="2" Text="{x:Static loc:Strings.OnboardingLicenseKey}"
FontSize="12" FontWeight="Bold"
Foreground="{StaticResource Brush.Text.Secondary}" />
<TextBox Grid.Row="3" x:Name="KeyBox"
@@ -52,12 +53,12 @@
<StackPanel Grid.Row="5" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,18,0,0">
<Button Style="{StaticResource SecondaryButton}"
Content="Plus tard"
Content="{x:Static loc:Strings.ActionLater}"
IsCancel="True"
Margin="0,0,12,0"
Click="OnLater" />
<Button Style="{StaticResource AccentButton}"
Content="Activer"
Content="{x:Static loc:Strings.OnboardingActivate}"
Padding="32,10"
IsDefault="True"
Click="OnActivate"

View File

@@ -2,7 +2,8 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:PSLauncher.App.ViewModels"
Title="Paramètres"
xmlns:loc="clr-namespace:PSLauncher.Core.Localization;assembly=PSLauncher.Core"
Title="{x:Static loc:Strings.SettingsTitle}"
Icon="pack://application:,,,/Resources/favicon.ico"
Width="640" Height="720"
MinWidth="540" MinHeight="500"
@@ -22,13 +23,36 @@
<!-- Header -->
<Border Grid.Row="0" Padding="32,24,32,16">
<TextBlock Text="Paramètres" FontSize="22" FontWeight="SemiBold"
<TextBlock Text="{x:Static loc:Strings.SettingsTitle}" FontSize="22" FontWeight="SemiBold"
Foreground="{StaticResource Brush.Text.Primary}" />
</Border>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Padding="32,0,32,16">
<StackPanel>
<!-- Langue de l'interface -->
<Border Background="{StaticResource Brush.Bg.Card}"
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
CornerRadius="6" Padding="20" Margin="0,0,0,16">
<StackPanel>
<TextBlock Text="{x:Static loc:Strings.SettingsLanguage}"
FontSize="11" FontWeight="Bold"
Foreground="{StaticResource Brush.Text.Secondary}"
Margin="0,0,0,8" />
<ComboBox ItemsSource="{Binding AvailableLanguages}"
DisplayMemberPath="Name"
SelectedValuePath="Code"
SelectedValue="{Binding Language, Mode=TwoWay}"
Background="#1A1A20"
Foreground="{StaticResource Brush.Text.Primary}"
BorderBrush="{StaticResource Brush.Border}"
BorderThickness="1" Padding="8" />
<TextBlock Text="{x:Static loc:Strings.SettingsLanguageHint}"
FontSize="11" Foreground="{StaticResource Brush.Text.Secondary}"
FontStyle="Italic" Margin="0,6,0,0" TextWrapping="Wrap" />
</StackPanel>
</Border>
<!-- License (en tête, bordure colorée selon état) -->
<Border BorderThickness="2"
CornerRadius="6" Padding="20" Margin="0,0,0,16">
@@ -83,7 +107,7 @@
<!-- Header text -->
<TextBlock Grid.Row="0" Grid.Column="1"
Text="LICENSE"
Text="{x:Static loc:Strings.SettingsLicense}"
FontSize="11" FontWeight="Bold"
Foreground="{StaticResource Brush.Text.Secondary}" />
<TextBlock Grid.Row="1" Grid.Column="1"
@@ -121,7 +145,7 @@
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
CornerRadius="6" Padding="20" Margin="0,0,0,16">
<StackPanel>
<TextBlock Text="SERVEUR"
<TextBlock Text="{x:Static loc:Strings.SettingsServer}"
FontSize="11" FontWeight="Bold"
Foreground="{StaticResource Brush.Text.Secondary}"
Margin="0,0,0,8" />
@@ -141,7 +165,7 @@
BorderThickness="1" Padding="8" />
<Button Grid.Column="1" Margin="8,0,0,0"
Style="{StaticResource SecondaryButton}"
Content="Tester"
Content="{x:Static loc:Strings.SettingsTest}"
Command="{Binding TestConnectionCommand}"
IsEnabled="{Binding IsTestingConnection, Converter={StaticResource InverseBoolToBool}}" />
</Grid>
@@ -156,7 +180,7 @@
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
CornerRadius="6" Padding="20" Margin="0,0,0,16">
<StackPanel>
<TextBlock Text="INSTALLATION"
<TextBlock Text="{x:Static loc:Strings.SettingsInstallation}"
FontSize="11" FontWeight="Bold"
Foreground="{StaticResource Brush.Text.Secondary}"
Margin="0,0,0,8" />
@@ -177,7 +201,7 @@
FontFamily="Consolas" />
<Button Grid.Column="1" Margin="8,0,0,0"
Style="{StaticResource SecondaryButton}"
Content="📁 Parcourir"
Content="{x:Static loc:Strings.SettingsBrowse}"
Command="{Binding BrowseInstallRootCommand}" />
</Grid>
</StackPanel>
@@ -188,7 +212,7 @@
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
CornerRadius="6" Padding="20" Margin="0,0,0,16">
<StackPanel>
<TextBlock Text="CACHE DE TÉLÉCHARGEMENTS"
<TextBlock Text="{x:Static loc:Strings.SettingsCache}"
FontSize="11" FontWeight="Bold"
Foreground="{StaticResource Brush.Text.Secondary}"
Margin="0,0,0,8" />
@@ -215,7 +239,7 @@
Command="{Binding OpenCacheFolderCommand}" />
<Button Grid.Column="2" Margin="8,0,0,0"
Style="{StaticResource SecondaryButton}"
Content="🗑 Vider"
Content="{x:Static loc:Strings.SettingsClearCache}"
Command="{Binding ClearCacheCommand}" />
</Grid>
</StackPanel>
@@ -226,7 +250,7 @@
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
CornerRadius="6" Padding="20" Margin="0,0,0,16">
<StackPanel>
<TextBlock Text="LOGS &amp; APPLICATION"
<TextBlock Text="{x:Static loc:Strings.SettingsLogs}"
FontSize="11" FontWeight="Bold"
Foreground="{StaticResource Brush.Text.Secondary}"
Margin="0,0,0,8" />
@@ -271,12 +295,12 @@
Padding="20,12">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Style="{StaticResource SecondaryButton}"
Content="Annuler"
Content="{x:Static loc:Strings.ActionCancel}"
IsCancel="True"
Margin="0,0,12,0"
Click="OnCancel" />
<Button Style="{StaticResource AccentButton}"
Content="Enregistrer"
Content="{x:Static loc:Strings.ActionSave}"
Padding="32,8"
IsDefault="True"
Click="OnSave" />