From 01a11d5616103a1dc03d46a431e6cce2b737aa27 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Sat, 2 May 2026 13:02:50 +0200 Subject: [PATCH] i18n: 5 languages (FR/EN/ZH/TH/AR), auto-detect from Windows + Settings picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/PSLauncher.App/App.xaml.cs | 18 ++ .../ViewModels/SettingsViewModel.cs | 49 +++++- src/PSLauncher.App/Views/MainWindow.xaml | 29 ++-- .../Views/OnboardingDialog.xaml | 13 +- src/PSLauncher.App/Views/SettingsDialog.xaml | 48 ++++-- src/PSLauncher.Core/Localization/Strings.cs | 155 ++++++++++++++++++ src/PSLauncher.Models/LocalConfig.cs | 7 + 7 files changed, 286 insertions(+), 33 deletions(-) create mode 100644 src/PSLauncher.Core/Localization/Strings.cs diff --git a/src/PSLauncher.App/App.xaml.cs b/src/PSLauncher.App/App.xaml.cs index bffe8f8..7a0f1eb 100644 --- a/src/PSLauncher.App/App.xaml.cs +++ b/src/PSLauncher.App/App.xaml.cs @@ -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.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(); window.DataContext = _host.Services.GetRequiredService(); + // Mise en page droite-à-gauche pour les langues RTL (arabe). + if (Strings.IsRightToLeft) + window.FlowDirection = FlowDirection.RightToLeft; MainWindow = window; window.Show(); } diff --git a/src/PSLauncher.App/ViewModels/SettingsViewModel.cs b/src/PSLauncher.App/ViewModels/SettingsViewModel.cs index e459b29..447496f 100644 --- a/src/PSLauncher.App/ViewModels/SettingsViewModel.cs +++ b/src/PSLauncher.App/ViewModels/SettingsViewModel.cs @@ -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 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] diff --git a/src/PSLauncher.App/Views/MainWindow.xaml b/src/PSLauncher.App/Views/MainWindow.xaml index 53d62fd..8e64541 100644 --- a/src/PSLauncher.App/Views/MainWindow.xaml +++ b/src/PSLauncher.App/Views/MainWindow.xaml @@ -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 @@