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>
176 lines
7.0 KiB
C#
176 lines
7.0 KiB
C#
using System.IO;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Headers;
|
|
using System.Runtime.InteropServices;
|
|
using System.Windows;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using PSLauncher.App.Services;
|
|
using PSLauncher.App.ViewModels;
|
|
using PSLauncher.App.Views;
|
|
using PSLauncher.Core.Configuration;
|
|
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;
|
|
using PSLauncher.Models;
|
|
using Serilog;
|
|
|
|
namespace PSLauncher.App;
|
|
|
|
public partial class App : Application
|
|
{
|
|
private IHost? _host;
|
|
private static System.Threading.Mutex? _singleInstanceMutex;
|
|
|
|
private const string SingleInstanceMutexName = "Global\\PSLauncher-3F8E2C1A-9B47-4D5E-A0F8-2E9D1B6C7A3F";
|
|
private const string BringToFrontMessage = "PSLAUNCHER_BRING_TO_FRONT";
|
|
private static readonly int WM_PSLAUNCHER_BRING = RegisterWindowMessage(BringToFrontMessage);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern int RegisterWindowMessage(string lpString);
|
|
|
|
[DllImport("user32.dll")]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static extern bool PostMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
|
|
|
|
private const int HWND_BROADCAST = 0xFFFF;
|
|
|
|
public static string LogsDirectory { get; private set; } = string.Empty;
|
|
|
|
protected override void OnStartup(StartupEventArgs e)
|
|
{
|
|
base.OnStartup(e);
|
|
|
|
// Single-instance : si une autre instance tourne déjà, on lui demande de
|
|
// se mettre au premier plan via un broadcast Windows et on quitte tout de suite.
|
|
_singleInstanceMutex = new System.Threading.Mutex(initiallyOwned: true,
|
|
name: SingleInstanceMutexName, out var createdNew);
|
|
if (!createdNew)
|
|
{
|
|
PostMessage((IntPtr)HWND_BROADCAST, WM_PSLAUNCHER_BRING, IntPtr.Zero, IntPtr.Zero);
|
|
Shutdown();
|
|
return;
|
|
}
|
|
|
|
// Logs avant tout — pour ne rien perdre des erreurs au boot
|
|
LogsDirectory = Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
"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()
|
|
.WriteTo.Debug()
|
|
.WriteTo.File(
|
|
path: Path.Combine(LogsDirectory, "app-.log"),
|
|
rollingInterval: RollingInterval.Day,
|
|
retainedFileCountLimit: 10,
|
|
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}")
|
|
.CreateLogger();
|
|
|
|
Log.Information("PSLauncher starting (logs in {Path})", LogsDirectory);
|
|
|
|
AppDomain.CurrentDomain.UnhandledException += (_, args) =>
|
|
Log.Fatal((Exception)args.ExceptionObject, "Unhandled exception (AppDomain)");
|
|
DispatcherUnhandledException += (_, args) =>
|
|
{
|
|
Log.Error(args.Exception, "Unhandled UI exception");
|
|
args.Handled = false;
|
|
};
|
|
|
|
_host = Host.CreateDefaultBuilder()
|
|
.UseSerilog()
|
|
.ConfigureServices((_, services) =>
|
|
{
|
|
services.AddSingleton<IConfigStore, ConfigStore>();
|
|
services.AddSingleton<LocalConfig>(sp => sp.GetRequiredService<IConfigStore>().Load());
|
|
|
|
services.AddSingleton(sp =>
|
|
{
|
|
var http = new HttpClient(new SocketsHttpHandler
|
|
{
|
|
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
|
|
AutomaticDecompression = System.Net.DecompressionMethods.All
|
|
})
|
|
{
|
|
Timeout = Timeout.InfiniteTimeSpan
|
|
};
|
|
http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("PSLauncher", "0.5"));
|
|
return http;
|
|
});
|
|
|
|
services.AddSingleton<IInstallationRegistry>(sp =>
|
|
new InstallationRegistry(
|
|
sp.GetRequiredService<ILogger<InstallationRegistry>>(),
|
|
() => sp.GetRequiredService<LocalConfig>().InstallRoot));
|
|
|
|
services.AddSingleton<IProcessLauncher, ProcessLauncher>();
|
|
services.AddSingleton<IIntegrityService, IntegrityService>();
|
|
services.AddSingleton<IZipInstaller, ZipInstaller>();
|
|
services.AddSingleton<IDownloadStateStore, DownloadStateStore>();
|
|
|
|
services.AddSingleton<IManifestService>(sp =>
|
|
new ManifestService(
|
|
sp.GetRequiredService<HttpClient>(),
|
|
() => sp.GetRequiredService<LocalConfig>().ServerBaseUrl,
|
|
sp.GetRequiredService<ILogger<ManifestService>>()));
|
|
|
|
services.AddSingleton<IDownloadManager, DownloadManager>();
|
|
services.AddSingleton<IUpdateChecker, UpdateChecker>();
|
|
services.AddSingleton<ILauncherSelfUpdater, LauncherSelfUpdater>();
|
|
|
|
services.AddSingleton<ILicenseService>(sp =>
|
|
new LicenseService(
|
|
sp.GetRequiredService<HttpClient>(),
|
|
() => sp.GetRequiredService<LocalConfig>().ServerBaseUrl,
|
|
sp.GetRequiredService<IConfigStore>(),
|
|
sp.GetRequiredService<LocalConfig>(),
|
|
sp.GetRequiredService<ILogger<LicenseService>>()));
|
|
|
|
services.AddSingleton<IToastService, ToastService>();
|
|
|
|
services.AddTransient<SettingsViewModel>();
|
|
services.AddSingleton<MainViewModel>();
|
|
services.AddSingleton<MainWindow>();
|
|
})
|
|
.Build();
|
|
|
|
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();
|
|
}
|
|
|
|
protected override void OnExit(ExitEventArgs e)
|
|
{
|
|
Log.Information("PSLauncher shutting down");
|
|
Log.CloseAndFlush();
|
|
_host?.Dispose();
|
|
base.OnExit(e);
|
|
}
|
|
}
|