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

@@ -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", "⬇ 下载", "⬇ ดาวน์โหลด", "⬇ تنزيل");
}