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.Installations;
using PSLauncher.Core.Integrity; using PSLauncher.Core.Integrity;
using PSLauncher.Core.Licensing; using PSLauncher.Core.Licensing;
using PSLauncher.Core.Localization;
using PSLauncher.Core.Manifests; using PSLauncher.Core.Manifests;
using PSLauncher.Core.Process; using PSLauncher.Core.Process;
using PSLauncher.Core.Updates; using PSLauncher.Core.Updates;
@@ -63,6 +64,20 @@ public partial class App : Application
"PSLauncher", "logs"); "PSLauncher", "logs");
Directory.CreateDirectory(LogsDirectory); 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() Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug() .MinimumLevel.Debug()
.Enrich.FromLogContext() .Enrich.FromLogContext()
@@ -143,6 +158,9 @@ public partial class App : Application
var window = _host.Services.GetRequiredService<MainWindow>(); var window = _host.Services.GetRequiredService<MainWindow>();
window.DataContext = _host.Services.GetRequiredService<MainViewModel>(); 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; MainWindow = window;
window.Show(); window.Show();
} }

View File

@@ -13,6 +13,8 @@ using PSLauncher.Models;
namespace PSLauncher.App.ViewModels; namespace PSLauncher.App.ViewModels;
public sealed record LanguageOption(string Code, string Name);
public sealed partial class SettingsViewModel : ObservableObject public sealed partial class SettingsViewModel : ObservableObject
{ {
private readonly IConfigStore _configStore; private readonly IConfigStore _configStore;
@@ -29,6 +31,12 @@ public sealed partial class SettingsViewModel : ObservableObject
[ObservableProperty] private string _serverBaseUrl; [ObservableProperty] private string _serverBaseUrl;
[ObservableProperty] private string _installRoot; [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 string? _connectionStatus;
[ObservableProperty] private bool _isTestingConnection; [ObservableProperty] private bool _isTestingConnection;
[ObservableProperty] private string _cacheSizeDisplay = "—"; [ObservableProperty] private string _cacheSizeDisplay = "—";
@@ -102,6 +110,7 @@ public sealed partial class SettingsViewModel : ObservableObject
_serverBaseUrl = config.ServerBaseUrl; _serverBaseUrl = config.ServerBaseUrl;
_installRoot = config.InstallRoot; _installRoot = config.InstallRoot;
_language = string.IsNullOrWhiteSpace(config.Language) ? "auto" : config.Language;
LauncherVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0"; LauncherVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0";
MachineId = licenseService.GetMachineId(); MachineId = licenseService.GetMachineId();
@@ -201,10 +210,48 @@ public sealed partial class SettingsViewModel : ObservableObject
[RelayCommand] [RelayCommand]
private void Save() private void Save()
{ {
var languageChanged = !string.Equals(
(_config.Language ?? "auto").ToLowerInvariant(),
(Language ?? "auto").ToLowerInvariant(),
StringComparison.Ordinal);
_config.ServerBaseUrl = ServerBaseUrl.TrimEnd('/'); _config.ServerBaseUrl = ServerBaseUrl.TrimEnd('/');
_config.InstallRoot = InstallRoot; _config.InstallRoot = InstallRoot;
_config.Language = Language ?? "auto";
_configStore.Save(_config); _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] [RelayCommand]

View File

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

View File

@@ -1,7 +1,8 @@
<Window x:Class="PSLauncher.App.Views.OnboardingDialog" <Window x:Class="PSLauncher.App.Views.OnboardingDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 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" Icon="pack://application:,,,/Resources/favicon.ico"
Width="540" Height="420" Width="540" Height="420"
MinWidth="480" MinHeight="380" MinWidth="480" MinHeight="380"
@@ -18,17 +19,17 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Activer votre license" <TextBlock Grid.Row="0" Text="{x:Static loc:Strings.OnboardingHeading}"
FontSize="22" FontWeight="SemiBold" FontSize="22" FontWeight="SemiBold"
Foreground="{StaticResource Brush.Text.Primary}" /> Foreground="{StaticResource Brush.Text.Primary}" />
<TextBlock Grid.Row="1" <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" TextWrapping="Wrap"
Foreground="{StaticResource Brush.Text.Secondary}" Foreground="{StaticResource Brush.Text.Secondary}"
Margin="0,8,0,18" /> 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" FontSize="12" FontWeight="Bold"
Foreground="{StaticResource Brush.Text.Secondary}" /> Foreground="{StaticResource Brush.Text.Secondary}" />
<TextBox Grid.Row="3" x:Name="KeyBox" <TextBox Grid.Row="3" x:Name="KeyBox"
@@ -52,12 +53,12 @@
<StackPanel Grid.Row="5" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,18,0,0"> <StackPanel Grid.Row="5" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,18,0,0">
<Button Style="{StaticResource SecondaryButton}" <Button Style="{StaticResource SecondaryButton}"
Content="Plus tard" Content="{x:Static loc:Strings.ActionLater}"
IsCancel="True" IsCancel="True"
Margin="0,0,12,0" Margin="0,0,12,0"
Click="OnLater" /> Click="OnLater" />
<Button Style="{StaticResource AccentButton}" <Button Style="{StaticResource AccentButton}"
Content="Activer" Content="{x:Static loc:Strings.OnboardingActivate}"
Padding="32,10" Padding="32,10"
IsDefault="True" IsDefault="True"
Click="OnActivate" Click="OnActivate"

View File

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

View File

@@ -0,0 +1,155 @@
using System.Globalization;
namespace PSLauncher.Core.Localization;
/// <summary>
/// Strings localisés du launcher. Approche statique : la langue est résolue au
/// startup (auto-détection ou setting utilisateur) puis fixée pour toute la
/// session. Tout changement de langue nécessite un redémarrage du launcher
/// (les bindings XAML x:Static ne refresh pas dynamiquement).
///
/// Langues supportées : fr (par défaut), en, zh (Simplified), th, ar.
/// Pour Arabic, FlowDirection doit aussi être basculé en RTL côté Window.
/// </summary>
public static class Strings
{
private static string _lang = "fr";
/// <summary>Code ISO 639-1 (2 lettres) actuellement actif.</summary>
public static string Lang => _lang;
/// <summary>True quand la langue actuelle s'écrit de droite à gauche.</summary>
public static bool IsRightToLeft => _lang == "ar";
/// <summary>Langues proposées au sélecteur des Settings (code → libellé natif).</summary>
public static readonly (string Code, string Name)[] Available =
{
("auto", "Automatique (langue du système)"),
("fr", "Français"),
("en", "English"),
("zh", "中文"),
("th", "ไทย"),
("ar", "العربية"),
};
/// <summary>
/// Initialise la langue à partir de la valeur enregistrée dans la config.
/// "auto" ou code inconnu → on prend la langue Windows si elle correspond
/// à une des langues supportées, sinon on tombe sur l'anglais.
/// </summary>
public static void Init(string? configValue)
{
var supported = new[] { "fr", "en", "zh", "th", "ar" };
var v = configValue?.Trim().ToLowerInvariant() ?? "auto";
if (v == "auto" || string.IsNullOrEmpty(v) || Array.IndexOf(supported, v) < 0)
{
// Auto-détection depuis la culture utilisateur Windows
var sys = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
v = Array.IndexOf(supported, sys) >= 0 ? sys : "en";
}
_lang = v;
// On force aussi la culture système sur cette langue (formats date / nombre)
try
{
var ci = CultureInfo.GetCultureInfo(_lang);
CultureInfo.DefaultThreadCurrentCulture = ci;
CultureInfo.DefaultThreadCurrentUICulture = ci;
Thread.CurrentThread.CurrentCulture = ci;
Thread.CurrentThread.CurrentUICulture = ci;
}
catch { /* culture inconnue, on garde le défaut */ }
}
private static string T(string fr, string en, string zh, string th, string ar) => _lang switch
{
"en" => en,
"zh" => zh,
"th" => th,
"ar" => ar,
_ => fr,
};
// ==================== TOP BAR ====================
public static string TopBarCheckUpdates => T("🔄 Vérifier les MAJ", "🔄 Check for updates", "🔄 检查更新", "🔄 ตรวจสอบการอัปเดต", "🔄 التحقق من التحديثات");
public static string TopBarSettings => T("⚙ Paramètres", "⚙ Settings", "⚙ 设置", "⚙ ตั้งค่า", "⚙ الإعدادات");
// ==================== BODY ====================
public static string BodyVersions => T("Versions", "Versions", "版本", "เวอร์ชัน", "النسخ");
public static string BodyVersionsHint => T(
"Lance, installe ou supprime une version. Les versions installées et celles disponibles sur le serveur cohabitent.",
"Launch, install or remove a version. Installed and available versions can coexist.",
"启动、安装或删除版本。已安装的版本与服务器上可用的版本可以共存。",
"เปิด ติดตั้ง หรือลบเวอร์ชัน เวอร์ชันที่ติดตั้งและที่มีในเซิร์ฟเวอร์อยู่ร่วมกันได้",
"تشغيل أو تثبيت أو إزالة نسخة. النسخ المثبتة والمتاحة على الخادم يمكن أن تتعايش."
);
public static string FeaturedCurrent => T("VERSION COURANTE", "CURRENT VERSION", "当前版本", "เวอร์ชันปัจจุบัน", "النسخة الحالية");
public static string SectionOther => T("AUTRES VERSIONS", "OTHER VERSIONS", "其他版本", "เวอร์ชันอื่นๆ", "نسخ أخرى");
public static string ReleasedOn => T("Sortie le ", "Released ", "发布于 ", "เผยแพร่เมื่อ ", "صدر في ");
// ==================== STATUS BADGES ====================
public static string StatusInstalled => T("● Installée", "● Installed", "● 已安装", "● ติดตั้งแล้ว", "● مثبتة");
public static string StatusAvailable => T("○ Disponible", "○ Available", "○ 可用", "○ พร้อมใช้งาน", "○ متاحة");
public static string StatusDownloading => T("⬇ Téléchargement…", "⬇ Downloading…", "⬇ 下载中…", "⬇ กำลังดาวน์โหลด…", "⬇ جارٍ التنزيل…");
public static string StatusInstalling => T("📦 Installation…", "📦 Installing…", "📦 安装中…", "📦 กำลังติดตั้ง…", "📦 جارٍ التثبيت…");
public static string StatusUninstalling => T("🗑 Suppression…", "🗑 Uninstalling…", "🗑 卸载中…", "🗑 กำลังถอนการติดตั้ง…", "🗑 جارٍ الإزالة…");
// ==================== ACTIONS ====================
public static string ActionLaunch => T("▶ Lancer", "▶ Launch", "▶ 启动", "▶ เปิด", "▶ تشغيل");
public static string ActionLaunchBig => T("▶ LANCER", "▶ LAUNCH", "▶ 启动", "▶ เปิด", "▶ تشغيل");
public static string ActionInstall => T("⬇ Installer", "⬇ Install", "⬇ 安装", "⬇ ติดตั้ง", "⬇ تثبيت");
public static string ActionInstallBig => T("⬇ INSTALLER", "⬇ INSTALL", "⬇ 安装", "⬇ ติดตั้ง", "⬇ تثبيت");
public static string ActionCancel => T("Annuler", "Cancel", "取消", "ยกเลิก", "إلغاء");
public static string ActionLater => T("Plus tard", "Later", "稍后", "ภายหลัง", "لاحقاً");
public static string ActionSave => T("Enregistrer", "Save", "保存", "บันทึก", "حفظ");
public static string ActionClose => T("Fermer", "Close", "关闭", "ปิด", "إغلاق");
public static string ActionLicenseInsufficient => T("🔒 License insuffisante", "🔒 Insufficient license", "🔒 许可证不足", "🔒 ใบอนุญาตไม่เพียงพอ", "🔒 ترخيص غير كافٍ");
// ==================== MENU "..." ====================
public static string MenuReleaseNotes => T("📜 Voir les release notes", "📜 View release notes", "📜 查看版本说明", "📜 ดูบันทึกการเปลี่ยนแปลง", "📜 عرض ملاحظات الإصدار");
public static string MenuOpenFolder => T("📁 Ouvrir le dossier", "📁 Open folder", "📁 打开文件夹", "📁 เปิดโฟลเดอร์", "📁 فتح المجلد");
public static string MenuUninstall => T("🗑 Supprimer cette version…", "🗑 Uninstall this version…", "🗑 删除此版本…", "🗑 ถอนการติดตั้งเวอร์ชันนี้…", "🗑 إزالة هذه النسخة…");
// ==================== LICENSE BADGE ====================
public static string LicenseNone => T("Aucune license", "No license", "无许可证", "ไม่มีใบอนุญาต", "لا يوجد ترخيص");
public static string LicenseRevoked => T("Révoquée", "Revoked", "已撤销", "ถูกเพิกถอน", "تم إلغاؤه");
public static string LicenseTooltip => T("Voir / changer la license", "View / change license", "查看 / 更改许可证", "ดู / เปลี่ยนใบอนุญาต", "عرض / تغيير الترخيص");
// ==================== SETTINGS ====================
public static string SettingsTitle => T("Paramètres", "Settings", "设置", "ตั้งค่า", "الإعدادات");
public static string SettingsLicense => T("LICENSE", "LICENSE", "许可证", "ใบอนุญาต", "الترخيص");
public static string SettingsServer => T("SERVEUR", "SERVER", "服务器", "เซิร์ฟเวอร์", "الخادم");
public static string SettingsInstallation => T("INSTALLATION", "INSTALLATION", "安装", "การติดตั้ง", "التثبيت");
public static string SettingsCache => T("CACHE DE TÉLÉCHARGEMENTS", "DOWNLOAD CACHE", "下载缓存", "แคชดาวน์โหลด", "ذاكرة التخزين المؤقت للتنزيلات");
public static string SettingsLogs => T("LOGS & APPLICATION", "LOGS & APP", "日志与应用", "บันทึกและแอป", "السجلات والتطبيق");
public static string SettingsLanguage => T("LANGUE", "LANGUAGE", "语言", "ภาษา", "اللغة");
public static string SettingsLanguageHint => T(
"Le launcher redémarrera pour appliquer le changement.",
"The launcher will restart to apply the change.",
"启动器将重启以应用更改。",
"Launcher จะรีสตาร์ทเพื่อใช้การเปลี่ยนแปลง",
"سيتم إعادة تشغيل المُشغِّل لتطبيق التغيير."
);
public static string SettingsTest => T("Tester", "Test", "测试", "ทดสอบ", "اختبار");
public static string SettingsBrowse => T("📁 Parcourir", "📁 Browse", "📁 浏览", "📁 เรียกดู", "📁 استعراض");
public static string SettingsClearCache => T("🗑 Vider", "🗑 Clear", "🗑 清空", "🗑 ล้าง", "🗑 مسح");
// ==================== ONBOARDING ====================
public static string OnboardingTitle => T("Activation PROSERVE Launcher", "PROSERVE Launcher activation", "PROSERVE Launcher 激活", "เปิดใช้งาน PROSERVE Launcher", "تنشيط PROSERVE Launcher");
public static string OnboardingHeading => T("Activer votre license", "Activate your license", "激活您的许可证", "เปิดใช้งานใบอนุญาตของคุณ", "تنشيط الترخيص الخاص بك");
public static string OnboardingBody => T(
"Saisis la clé fournie par ASTERION. Elle te donne accès aux téléchargements jusqu'à la date de validité associée.",
"Enter the key provided by ASTERION. It grants you download access until the associated expiration date.",
"输入 ASTERION 提供的密钥。它授予您在关联的到期日期之前的下载权限。",
"ป้อนคีย์ที่ ASTERION ให้มา ซึ่งจะให้สิทธิ์ดาวน์โหลดจนถึงวันหมดอายุที่เกี่ยวข้อง",
"أدخل المفتاح المقدم من ASTERION. يمنحك صلاحية التنزيل حتى تاريخ انتهاء الصلاحية المرتبط."
);
public static string OnboardingLicenseKey => T("Clé de license", "License key", "许可证密钥", "คีย์ใบอนุญาต", "مفتاح الترخيص");
public static string OnboardingActivate => T("Activer", "Activate", "激活", "เปิดใช้งาน", "تنشيط");
// ==================== UPDATE ====================
public static string UpdateAvailableTitle => T("Mise à jour disponible", "Update available", "有可用更新", "มีการอัปเดต", "تحديث متاح");
public static string UpdateDownload => T("⬇ Télécharger", "⬇ Download", "⬇ 下载", "⬇ ดาวน์โหลด", "⬇ تنزيل");
}

View File

@@ -6,6 +6,13 @@ public sealed class LocalConfig
public string ServerBaseUrl { get; set; } = "https://example.com/PS_Launcher/api"; public string ServerBaseUrl { get; set; } = "https://example.com/PS_Launcher/api";
public string InstallRoot { get; set; } = string.Empty; public string InstallRoot { get; set; } = string.Empty;
public string? LastLaunchedVersion { get; set; } public string? LastLaunchedVersion { get; set; }
/// <summary>
/// Code de langue de l'UI : "auto" (suit la langue Windows), "fr", "en",
/// "zh", "th" ou "ar". Tout changement nécessite un redémarrage du launcher.
/// </summary>
public string Language { get; set; } = "auto";
public LicenseConfig License { get; set; } = new(); public LicenseConfig License { get; set; } = new();
} }