v1.0.0 🎉 — Production release. i18n Espagnol + Allemand (launcher + emails).
Première release MAJEURE après ~30 itérations 0.x. Le système est complet et stable pour la production : distribution PROSERVE multi-channels, install launcher cross-fleet, monitoring santé VR, sécurité (signatures Ed25519, HMAC URLs présignées, settings lock per-license, RFC1918 filter cache LAN). i18n nouvelles langues (Espagnol + Allemand) : - Strings.cs : signature T() étendue avec 2 params optionnels (es?/de?) en plus des 5 obligatoires. Fallback automatique sur l'anglais quand non traduit → infrastructure prête immédiatement, traduction progressive sur les ~317 strings du launcher selon les besoins terrain. - ~30 strings critiques traduites maintenant (top bar, body, status badges, actions principales Launch/Install/Cancel/Yes/No, MessageBox titles). - Available[] inclut es/Español + de/Deutsch ; auto-détection Windows pour es-* / de-* funcionne au premier launch. - Init() whitelist mise à jour. Emails release announce localisés par license : - Migration 005 : ajout `language VARCHAR(8) NULL` sur licenses (whitelist fr/en/es/de/zh/th/ar, NULL = fallback English). - getEmailStrings() PHP étendu : dictionnaire 7 langues × 14 strings. Traduction complète es + de (gérable : 28 nouvelles strings, vs 634 pour traduire le launcher en intégralité). - notify_release boucle par license : récupère language + locale fallback 'en' + render avec la bonne locale → chaque destinataire reçoit son email dans la langue de SA license, indépendamment des autres clients. - Subject localisé aussi (« PROSERVE v1.5.4 ya está disponible » / « PROSERVE v1.5.4 ist jetzt verfügbar »). - Release notes body reste TOUJOURS en anglais (évite d'avoir à maintenir N traductions du Markdown des notes). - Direction RTL préservée pour l'arabe (dir="rtl" sur <html> + <table>). Admin UI : dropdown langue dans la modal Contacts d'une license + whitelist backend (set_contact_emails accepte fr/en/es/de/zh/th/ar). Bumps : 0.29.10 → 1.0.0 — milestone V1, premier release majeur en prod. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
|
||||
#define MyAppName "PROSERVE Launcher"
|
||||
#define MyAppShortName "PS_Launcher"
|
||||
#define MyAppVersion "0.29.10"
|
||||
#define MyAppVersion "1.0.0"
|
||||
#define MyAppPublisher "ASTERION VR"
|
||||
#define MyAppURL "https://asterionvr.com"
|
||||
#define MyAppExeName "PS_Launcher.exe"
|
||||
|
||||
@@ -169,25 +169,34 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
}
|
||||
elseif ($action === 'set_contact_emails') {
|
||||
// Met à jour la liste des emails de contact pour les notifications
|
||||
// de nouvelles versions. L'admin saisit en texte libre (1 par ligne,
|
||||
// virgules, point-virgules — peu importe), on parse + valide via
|
||||
// FILTER_VALIDATE_EMAIL et on stocke en CSV. Vide / aucun email
|
||||
// valide → on stocke NULL (= pas de notif possible pour cette
|
||||
// license, juste skip silencieux à l'envoi).
|
||||
// Met à jour la liste des emails de contact + la langue préférée
|
||||
// pour les notifications de nouvelles versions. L'admin saisit en
|
||||
// texte libre (1 par ligne, virgules, point-virgules — peu importe),
|
||||
// on parse + valide via FILTER_VALIDATE_EMAIL et on stocke en CSV.
|
||||
// Vide / aucun email valide → on stocke NULL (= pas de notif possible
|
||||
// pour cette license, juste skip silencieux à l'envoi).
|
||||
//
|
||||
// Language : whitelist stricte fr/en/zh/th/ar (= ce que le launcher
|
||||
// supporte côté Strings.cs). NULL ou inconnu → fallback English.
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$raw = (string)($_POST['contact_emails'] ?? '');
|
||||
$lang = trim((string)($_POST['language'] ?? ''));
|
||||
if ($id <= 0) {
|
||||
throw new Exception('License id invalide.');
|
||||
}
|
||||
$allowedLangs = ['fr', 'en', 'es', 'de', 'zh', 'th', 'ar'];
|
||||
if ($lang !== '' && !in_array($lang, $allowedLangs, true)) {
|
||||
throw new Exception("Code langue invalide : '{$lang}'. Attendu : " . implode(', ', $allowedLangs) . ' ou vide.');
|
||||
}
|
||||
$parsed = Mailer::parseEmails($raw);
|
||||
$stored = Mailer::joinForStorage($parsed);
|
||||
$db->prepare('UPDATE licenses SET contact_emails = ? WHERE id = ?')
|
||||
->execute([$stored, $id]);
|
||||
$db->prepare('UPDATE licenses SET contact_emails = ?, language = ? WHERE id = ?')
|
||||
->execute([$stored, $lang !== '' ? $lang : null, $id]);
|
||||
$count = count($parsed);
|
||||
$langDisplay = $lang !== '' ? $lang : 'en (défaut)';
|
||||
$message = $count === 0
|
||||
? "License #{$id} : contacts de notification supprimés."
|
||||
: "License #{$id} : {$count} contact(s) email enregistré(s) pour les notifications de release.";
|
||||
? "License #{$id} : contacts de notification supprimés. Langue email : {$langDisplay}."
|
||||
: "License #{$id} : {$count} contact(s) email enregistré(s), langue email : {$langDisplay}.";
|
||||
}
|
||||
elseif ($action === 'reset_machines') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
@@ -546,17 +555,45 @@ Layout::header('Licenses', 'licenses');
|
||||
version de PROSERVE accessible à cette license (matching channel +
|
||||
entitlement). Format libre : une adresse par ligne, virgules, ou
|
||||
point-virgules. Adresses invalides ignorées silencieusement.
|
||||
La langue choisie ci-dessous est utilisée pour le contenu de l'email
|
||||
(greeting + instructions) — les release notes elles-mêmes restent
|
||||
toujours en anglais.
|
||||
</p>
|
||||
<?php
|
||||
// Affichage en mode "1 par ligne" pour la lisibilité, peu importe
|
||||
// comment c'est stocké en DB (CSV).
|
||||
$currentEmails = Mailer::parseEmails($l['contact_emails'] ?? '');
|
||||
$currentDisplay = implode("\n", $currentEmails);
|
||||
$currentLang = trim((string)($l['language'] ?? ''));
|
||||
$langOptions = [
|
||||
'' => 'English (defaut)',
|
||||
'fr' => 'Français',
|
||||
'en' => 'English',
|
||||
'es' => 'Español',
|
||||
'de' => 'Deutsch',
|
||||
'zh' => '中文 (Chinese)',
|
||||
'th' => 'ภาษาไทย (Thai)',
|
||||
'ar' => 'العربية (Arabic)',
|
||||
];
|
||||
?>
|
||||
<form method="post">
|
||||
<?= Layout::csrfField() ?>
|
||||
<input type="hidden" name="action" value="set_contact_emails">
|
||||
<input type="hidden" name="id" value="<?= $l['id'] ?>">
|
||||
<div class="field">
|
||||
<label>Langue de l'email</label>
|
||||
<select name="language">
|
||||
<?php foreach ($langOptions as $code => $label):
|
||||
$sel = ($currentLang === $code) ? 'selected' : '';
|
||||
?>
|
||||
<option value="<?= htmlspecialchars($code) ?>" <?= $sel ?>><?= htmlspecialchars($label) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="hint" style="margin: 4px 0 0;">
|
||||
Langue dans laquelle l'email de notification sera rédigé.
|
||||
Le contenu des release notes reste toujours en anglais.
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Adresses email <span class="muted" style="font-weight: normal;">(<?= count($currentEmails) ?> actuellement)</span></label>
|
||||
<textarea name="contact_emails" rows="6"
|
||||
|
||||
@@ -102,33 +102,212 @@ function find_entry_index_by_id(array $manifest, string $id): ?int
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit le corps HTML de l'email de release announce. Message en anglais
|
||||
* (= langue par défaut du parc client international VR PROSERVE), inclut les
|
||||
* release notes brutes en Markdown (sans rendu — les MUA modernes Markdown
|
||||
* basique en clair reste très lisible et évite la dépendance Markdig PHP).
|
||||
* Le owner_name de la license est utilisé pour personnaliser le greeting.
|
||||
* Le logo (optionnel via <c>notifications.logo_url</c>) est inséré dans le
|
||||
* bandeau d'en-tête bleu — hotlinkable depuis n'importe quel MUA (pas de CID
|
||||
* embed pour rester compatible avec Gmail/Outlook qui bloquent les data URIs).
|
||||
* Strings localisées pour l'email de release announce. Couvre les 5 langues
|
||||
* supportées par le launcher (Strings.cs côté .NET) : fr, en, zh, th, ar.
|
||||
* Le release notes BODY lui-même reste TOUJOURS en anglais — seul le texte
|
||||
* d'accompagnement (greeting, instructions, CTAs) est localisé. Permet de
|
||||
* notifier des clients internationaux dans leur langue sans avoir à
|
||||
* maintenir N traductions du Markdown des notes.
|
||||
*
|
||||
* Placeholders dans les chaînes :
|
||||
* {version} → numéro de version (ex. "1.5.4.16")
|
||||
* {owner} → nom du client (ex. "Asterion VR")
|
||||
*
|
||||
* Fallback : si la locale demandée n'est pas en table → 'en' (= comportement
|
||||
* conservateur, le marché VR international a globalement l'anglais comme
|
||||
* lingua franca tech).
|
||||
*
|
||||
* @return array<string, string> dictionnaire de strings prêtes à interpoler
|
||||
*/
|
||||
function renderReleaseAnnounceEmail(string $version, string $ownerName, string $notesMarkdown, string $baseUrl, string $logoSrc = ''): string
|
||||
function getEmailStrings(string $locale): array
|
||||
{
|
||||
static $cache = null;
|
||||
if ($cache !== null) {
|
||||
return $cache[$locale] ?? $cache['en'];
|
||||
}
|
||||
$cache = [
|
||||
'en' => [
|
||||
'subject' => 'PROSERVE v{version} is now available',
|
||||
'title' => 'PROSERVE v{version} is available!',
|
||||
'subtitle' => 'A new version is ready to install via your PROSERVE Launcher.',
|
||||
'greeting' => 'Hi {owner},',
|
||||
'intro' => 'We\'re happy to let you know that <strong>PROSERVE version {version}</strong> has just been published and is now available for download.',
|
||||
'instructions' => 'If you already have the <strong>PROSERVE Launcher</strong> installed, simply open it on any of your machines, click <em>« Check for updates »</em>, and install the new version with one click.',
|
||||
'firstInstallTitle' => 'First install or new workstation?',
|
||||
'firstInstallBody' => 'If this is your first time, or you\'re setting up a new PC, download the PROSERVE Launcher installer below. <strong>You\'ll need to run it on EACH workstation</strong> where PROSERVE will be used.',
|
||||
'downloadButton' => '⬇ Download PROSERVE Launcher (Windows)',
|
||||
'directLink' => 'Direct link:',
|
||||
'releaseNotesTitle' => 'Release notes',
|
||||
'releaseNotesNone' => 'No release notes for this version.',
|
||||
'support' => 'As always, if you run into any issue (download failure, install error, anything weird in-game), don\'t hesitate to reach out — we\'re here to help.',
|
||||
'closing' => 'Happy training!',
|
||||
'signature' => 'The ASTERION VR team',
|
||||
'footer' => 'This automated notification was sent because your contact email is registered with one of your PROSERVE licenses.<br/>To stop receiving these, ask your ASTERION VR account manager to remove your address from the license contacts.',
|
||||
],
|
||||
'fr' => [
|
||||
'subject' => 'PROSERVE v{version} est disponible',
|
||||
'title' => 'PROSERVE v{version} est disponible !',
|
||||
'subtitle' => 'Une nouvelle version est prête à installer via votre PROSERVE Launcher.',
|
||||
'greeting' => 'Bonjour {owner},',
|
||||
'intro' => 'Nous avons le plaisir de vous annoncer que <strong>la version {version} de PROSERVE</strong> vient d\'être publiée et est désormais disponible au téléchargement.',
|
||||
'instructions' => 'Si vous avez déjà le <strong>PROSERVE Launcher</strong> installé, ouvrez-le simplement sur n\'importe lequel de vos postes, cliquez sur <em>« Vérifier les mises à jour »</em> et installez la nouvelle version en un clic.',
|
||||
'firstInstallTitle' => 'Première installation ou nouveau poste ?',
|
||||
'firstInstallBody' => 'S\'il s\'agit de votre première installation, ou si vous configurez un nouveau PC, téléchargez le PROSERVE Launcher ci-dessous. <strong>Il doit être lancé sur CHAQUE poste</strong> où PROSERVE sera utilisé.',
|
||||
'downloadButton' => '⬇ Télécharger PROSERVE Launcher (Windows)',
|
||||
'directLink' => 'Lien direct :',
|
||||
'releaseNotesTitle' => 'Notes de version',
|
||||
'releaseNotesNone' => 'Pas de notes de version pour cette release.',
|
||||
'support' => 'Comme toujours, en cas de problème (échec de téléchargement, erreur d\'installation, comportement inattendu en VR), n\'hésitez pas à nous contacter — nous sommes là pour vous aider.',
|
||||
'closing' => 'Bon entraînement !',
|
||||
'signature' => 'L\'équipe ASTERION VR',
|
||||
'footer' => 'Cette notification automatique a été envoyée parce que votre email est enregistré comme contact sur l\'une de vos licenses PROSERVE.<br/>Pour ne plus recevoir ces emails, demandez à votre gestionnaire de compte ASTERION VR de retirer votre adresse des contacts license.',
|
||||
],
|
||||
'zh' => [
|
||||
'subject' => 'PROSERVE v{version} 现已发布',
|
||||
'title' => 'PROSERVE v{version} 已发布!',
|
||||
'subtitle' => '新版本已准备好通过您的 PROSERVE Launcher 安装。',
|
||||
'greeting' => '你好 {owner},',
|
||||
'intro' => '我们很高兴地通知您,<strong>PROSERVE 版本 {version}</strong> 刚刚发布,现已可供下载。',
|
||||
'instructions' => '如果您已经安装了 <strong>PROSERVE Launcher</strong>,只需在任何一台机器上打开它,单击<em>「检查更新」</em>,然后单击「安装」即可完成新版本的安装。',
|
||||
'firstInstallTitle' => '首次安装或新工作站?',
|
||||
'firstInstallBody' => '如果这是您的第一次使用,或者您正在配置一台新 PC,请在下方下载 PROSERVE Launcher 安装程序。<strong>您需要在使用 PROSERVE 的每台工作站上运行它</strong>。',
|
||||
'downloadButton' => '⬇ 下载 PROSERVE Launcher (Windows)',
|
||||
'directLink' => '直接链接:',
|
||||
'releaseNotesTitle' => '版本说明',
|
||||
'releaseNotesNone' => '此版本没有版本说明。',
|
||||
'support' => '一如既往,如果您遇到任何问题(下载失败、安装错误、游戏中出现异常情况),请随时联系我们 — 我们随时为您提供帮助。',
|
||||
'closing' => '训练愉快!',
|
||||
'signature' => 'ASTERION VR 团队',
|
||||
'footer' => '此自动通知已发送,因为您的联系电子邮件已注册到您的某个 PROSERVE 许可证中。<br/>要停止接收这些通知,请要求您的 ASTERION VR 客户经理从许可证联系人中删除您的地址。',
|
||||
],
|
||||
'th' => [
|
||||
'subject' => 'PROSERVE v{version} พร้อมใช้งานแล้ว',
|
||||
'title' => 'PROSERVE v{version} พร้อมใช้งาน!',
|
||||
'subtitle' => 'เวอร์ชันใหม่พร้อมติดตั้งผ่าน PROSERVE Launcher ของคุณ',
|
||||
'greeting' => 'สวัสดี {owner},',
|
||||
'intro' => 'เรายินดีที่จะแจ้งให้คุณทราบว่า <strong>PROSERVE เวอร์ชัน {version}</strong> เพิ่งได้รับการเผยแพร่และพร้อมให้ดาวน์โหลดแล้ว',
|
||||
'instructions' => 'หากคุณติดตั้ง <strong>PROSERVE Launcher</strong> ไว้แล้ว เพียงเปิดบนเครื่องใดก็ได้ของคุณ คลิก <em>« ตรวจสอบการอัปเดต »</em> แล้วติดตั้งเวอร์ชันใหม่ด้วยคลิกเดียว',
|
||||
'firstInstallTitle' => 'ติดตั้งครั้งแรก หรือเวิร์กสเตชันใหม่?',
|
||||
'firstInstallBody' => 'หากเป็นครั้งแรกของคุณ หรือคุณกำลังตั้งค่า PC ใหม่ ดาวน์โหลด PROSERVE Launcher ด้านล่าง <strong>คุณต้องเรียกใช้บนเวิร์กสเตชันทุกเครื่อง</strong>ที่จะใช้ PROSERVE',
|
||||
'downloadButton' => '⬇ ดาวน์โหลด PROSERVE Launcher (Windows)',
|
||||
'directLink' => 'ลิงก์โดยตรง:',
|
||||
'releaseNotesTitle' => 'บันทึกการเผยแพร่',
|
||||
'releaseNotesNone' => 'ไม่มีบันทึกการเผยแพร่สำหรับเวอร์ชันนี้',
|
||||
'support' => 'เช่นเคย หากคุณพบปัญหาใดๆ (ดาวน์โหลดล้มเหลว ติดตั้งผิดพลาด อะไรแปลกๆ ในเกม) อย่าลังเลที่จะติดต่อ — เราพร้อมช่วยเหลือ',
|
||||
'closing' => 'ฝึกซ้อมให้สนุก!',
|
||||
'signature' => 'ทีม ASTERION VR',
|
||||
'footer' => 'การแจ้งเตือนอัตโนมัตินี้ถูกส่งเนื่องจากอีเมลติดต่อของคุณถูกลงทะเบียนกับหนึ่งในใบอนุญาต PROSERVE ของคุณ<br/>เพื่อหยุดรับการแจ้งเตือนเหล่านี้ ขอให้ผู้จัดการบัญชี ASTERION VR ของคุณลบที่อยู่ของคุณออกจากผู้ติดต่อใบอนุญาต',
|
||||
],
|
||||
'ar' => [
|
||||
'subject' => 'PROSERVE v{version} متوفر الآن',
|
||||
'title' => 'PROSERVE v{version} متوفر!',
|
||||
'subtitle' => 'إصدار جديد جاهز للتثبيت عبر PROSERVE Launcher الخاص بك.',
|
||||
'greeting' => 'مرحباً {owner}،',
|
||||
'intro' => 'يسعدنا أن نعلمك بأن <strong>الإصدار {version} من PROSERVE</strong> قد تم نشره للتو وأصبح متاحاً للتنزيل.',
|
||||
'instructions' => 'إذا كان لديك <strong>PROSERVE Launcher</strong> مثبتاً بالفعل، فما عليك سوى فتحه على أي من أجهزتك، والنقر فوق <em>«التحقق من التحديثات»</em>، وتثبيت الإصدار الجديد بنقرة واحدة.',
|
||||
'firstInstallTitle' => 'تثبيت لأول مرة أو محطة عمل جديدة؟',
|
||||
'firstInstallBody' => 'إذا كانت هذه المرة الأولى لك، أو إذا كنت تعد جهاز كمبيوتر جديداً، فقم بتنزيل PROSERVE Launcher أدناه. <strong>ستحتاج إلى تشغيله على كل محطة عمل</strong> سيتم استخدام PROSERVE فيها.',
|
||||
'downloadButton' => '⬇ تنزيل PROSERVE Launcher (Windows)',
|
||||
'directLink' => 'رابط مباشر:',
|
||||
'releaseNotesTitle' => 'ملاحظات الإصدار',
|
||||
'releaseNotesNone' => 'لا توجد ملاحظات إصدار لهذه النسخة.',
|
||||
'support' => 'كما هو الحال دائماً، إذا واجهت أي مشكلة (فشل التنزيل، خطأ في التثبيت، أي شيء غريب في اللعبة)، فلا تتردد في التواصل معنا — نحن هنا للمساعدة.',
|
||||
'closing' => 'تدريب سعيد!',
|
||||
'signature' => 'فريق ASTERION VR',
|
||||
'footer' => 'تم إرسال هذا الإشعار التلقائي لأن بريدك الإلكتروني للاتصال مسجل في أحد تراخيص PROSERVE الخاصة بك.<br/>للتوقف عن تلقي هذه الإشعارات، اطلب من مدير حسابك في ASTERION VR إزالة عنوانك من جهات اتصال الترخيص.',
|
||||
],
|
||||
'es' => [
|
||||
'subject' => 'PROSERVE v{version} ya está disponible',
|
||||
'title' => '¡PROSERVE v{version} ya está disponible!',
|
||||
'subtitle' => 'Una nueva versión está lista para instalar a través de tu PROSERVE Launcher.',
|
||||
'greeting' => 'Hola {owner},',
|
||||
'intro' => 'Nos complace informarte que <strong>la versión {version} de PROSERVE</strong> acaba de publicarse y ya está disponible para descargar.',
|
||||
'instructions' => 'Si ya tienes el <strong>PROSERVE Launcher</strong> instalado, simplemente ábrelo en cualquiera de tus máquinas, haz clic en <em>«Buscar actualizaciones»</em> e instala la nueva versión con un solo clic.',
|
||||
'firstInstallTitle' => '¿Primera instalación o nueva estación de trabajo?',
|
||||
'firstInstallBody' => 'Si es la primera vez, o si estás configurando un PC nuevo, descarga el instalador de PROSERVE Launcher abajo. <strong>Deberás ejecutarlo en CADA estación de trabajo</strong> donde se utilice PROSERVE.',
|
||||
'downloadButton' => '⬇ Descargar PROSERVE Launcher (Windows)',
|
||||
'directLink' => 'Enlace directo:',
|
||||
'releaseNotesTitle' => 'Notas de versión',
|
||||
'releaseNotesNone' => 'No hay notas de versión para esta release.',
|
||||
'support' => 'Como siempre, si encuentras algún problema (fallo de descarga, error de instalación, comportamiento inesperado en el juego), no dudes en contactarnos — estamos aquí para ayudar.',
|
||||
'closing' => '¡Buen entrenamiento!',
|
||||
'signature' => 'El equipo ASTERION VR',
|
||||
'footer' => 'Esta notificación automática se envió porque tu dirección de correo está registrada como contacto en una de tus licencias PROSERVE.<br/>Para dejar de recibir estos correos, pide a tu gestor de cuenta ASTERION VR que elimine tu dirección de los contactos de la licencia.',
|
||||
],
|
||||
'de' => [
|
||||
'subject' => 'PROSERVE v{version} ist jetzt verfügbar',
|
||||
'title' => 'PROSERVE v{version} ist verfügbar!',
|
||||
'subtitle' => 'Eine neue Version ist bereit zur Installation über deinen PROSERVE Launcher.',
|
||||
'greeting' => 'Hallo {owner},',
|
||||
'intro' => 'Wir freuen uns, dir mitzuteilen, dass <strong>die Version {version} von PROSERVE</strong> soeben veröffentlicht wurde und jetzt zum Download bereitsteht.',
|
||||
'instructions' => 'Wenn du den <strong>PROSERVE Launcher</strong> bereits installiert hast, öffne ihn einfach auf einer beliebigen deiner Maschinen, klicke auf <em>«Updates suchen»</em> und installiere die neue Version mit einem Klick.',
|
||||
'firstInstallTitle' => 'Erstinstallation oder neue Arbeitsstation?',
|
||||
'firstInstallBody' => 'Wenn dies das erste Mal ist oder du einen neuen PC einrichtest, lade den PROSERVE Launcher-Installer unten herunter. <strong>Du musst ihn auf JEDER Arbeitsstation ausführen</strong>, auf der PROSERVE verwendet wird.',
|
||||
'downloadButton' => '⬇ PROSERVE Launcher herunterladen (Windows)',
|
||||
'directLink' => 'Direkter Link:',
|
||||
'releaseNotesTitle' => 'Versionshinweise',
|
||||
'releaseNotesNone' => 'Keine Versionshinweise für diese Version.',
|
||||
'support' => 'Wie immer, wenn du auf Probleme stößt (Download-Fehler, Installationsfehler, irgendetwas Seltsames im Spiel), zögere nicht, uns zu kontaktieren — wir sind hier, um zu helfen.',
|
||||
'closing' => 'Viel Spaß beim Training!',
|
||||
'signature' => 'Das ASTERION VR Team',
|
||||
'footer' => 'Diese automatische Benachrichtigung wurde gesendet, weil deine Kontakt-E-Mail bei einer deiner PROSERVE-Lizenzen registriert ist.<br/>Um diese E-Mails nicht mehr zu erhalten, bitte deinen ASTERION VR Account Manager, deine Adresse aus den Lizenz-Kontakten zu entfernen.',
|
||||
],
|
||||
];
|
||||
return $cache[$locale] ?? $cache['en'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit le corps HTML de l'email de release announce, dans la langue
|
||||
* <c>$locale</c> demandée (fallback 'en'). Le contenu de la release note
|
||||
* elle-même (Markdown) reste en anglais — seul le texte d'accompagnement
|
||||
* (greeting, instructions, CTAs) est localisé via <see cref="getEmailStrings"/>.
|
||||
* Le owner_name personnalise le greeting. Le logo (optionnel via CID embed
|
||||
* <c>notifications.logo_path</c>) est inséré dans le bandeau d'en-tête navy.
|
||||
*
|
||||
* Pour l'arabe : on ajoute <c>dir="rtl"</c> sur le wrapper table → Outlook,
|
||||
* Gmail et Apple Mail rendent correctement le texte droit→gauche, et les
|
||||
* sous-éléments inline (HTML tags, code, URLs) restent affichés LTR comme
|
||||
* attendu (mixed bidi handling natif).
|
||||
*/
|
||||
function renderReleaseAnnounceEmail(string $version, string $ownerName, string $notesMarkdown, string $baseUrl, string $logoSrc = '', string $locale = 'en'): string
|
||||
{
|
||||
// Récupère le dictionnaire de strings localisées + interpolation simple
|
||||
// des placeholders {version} et {owner}. On échappe APRÈS interpolation
|
||||
// pour ne pas double-encoder les < > de balises HTML déjà dans les
|
||||
// strings (cf. <strong> dans les templates de getEmailStrings).
|
||||
$s = getEmailStrings($locale);
|
||||
$versionEsc = htmlspecialchars($version, ENT_QUOTES, 'UTF-8');
|
||||
$ownerEsc = htmlspecialchars($ownerName !== '' ? $ownerName : 'team', ENT_QUOTES, 'UTF-8');
|
||||
$notesEsc = htmlspecialchars(trim($notesMarkdown), ENT_QUOTES, 'UTF-8');
|
||||
// $logoSrc peut être une URL https://… (hotlink) OU un cid:xxx (inline embed).
|
||||
// Le caller décide selon ce qu'il préfère ; on échappe juste pour mettre dans src=.
|
||||
$logoEsc = htmlspecialchars($logoSrc, ENT_QUOTES, 'UTF-8');
|
||||
// Direction RTL pour l'arabe. Les autres langues sont LTR par défaut.
|
||||
// Note : on POSITIONNE rtl sur la table racine — les sub-blocks restent
|
||||
// align="center" / align="left" selon le contexte (les tags HTML inline
|
||||
// gardent leur direction naturelle via bidi-isolate du browser).
|
||||
$dirAttr = ($locale === 'ar') ? ' dir="rtl"' : '';
|
||||
|
||||
// Helper d'interpolation : remplace {version} et {owner} dans une string
|
||||
// localisée. NOTE : on ne réencode PAS — les strings dans getEmailStrings
|
||||
// contiennent volontairement des <strong>, <em>, etc. à rendre comme HTML.
|
||||
$tr = function (string $key) use ($s, $versionEsc, $ownerEsc): string {
|
||||
return strtr($s[$key] ?? '', ['{version}' => $versionEsc, '{owner}' => $ownerEsc]);
|
||||
};
|
||||
|
||||
// Wrap les release notes en <pre> pour préserver le Markdown formatting
|
||||
// (indentation, listes, line breaks). Pas de rendu HTML — l'admin sait que
|
||||
// ses notes sont en Markdown et l'opérateur peut lire le format brut.
|
||||
// IMPORTANT : le titre "Release notes" est localisé MAIS le contenu reste
|
||||
// toujours en anglais (= ce que l'admin a saisi dans la modal Notes).
|
||||
$notesBlock = $notesEsc !== ''
|
||||
? "<h3 style=\"color: #1F2937; margin: 24px 0 8px;\">Release notes</h3>\n"
|
||||
? "<h3 style=\"color: #1F2937; margin: 24px 0 8px;\">" . htmlspecialchars($s['releaseNotesTitle'], ENT_QUOTES, 'UTF-8') . "</h3>\n"
|
||||
. "<pre style=\"background: #F3F4F6; padding: 16px; border-radius: 6px; "
|
||||
. "font-family: 'Cascadia Code', Consolas, monospace; font-size: 13px; "
|
||||
. "white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; "
|
||||
. "direction: ltr; text-align: left; "
|
||||
. "border-left: 3px solid #3B82F6;\">{$notesEsc}</pre>"
|
||||
: '<p style="color: #6B7280; font-style: italic;">No release notes for this version.</p>';
|
||||
: '<p style="color: #6B7280; font-style: italic;">' . htmlspecialchars($s['releaseNotesNone'], ENT_QUOTES, 'UTF-8') . '</p>';
|
||||
|
||||
// Logo block (optionnel). Image hotlinkée + alt fallback : si le MUA bloque
|
||||
// les images (Outlook protégé par défaut), l'alt "ASTERION VR" s'affiche
|
||||
@@ -158,38 +337,50 @@ function renderReleaseAnnounceEmail(string $version, string $ownerName, string $
|
||||
: 'https://asterionvr.com/PS_Launcher/installer/PS_Launcher-Setup.exe';
|
||||
$installerUrlEsc = htmlspecialchars($installerUrl, ENT_QUOTES, 'UTF-8');
|
||||
|
||||
// Toutes les strings utilisateur sont passées par $tr() qui interpole
|
||||
// {version}/{owner} dans le template localisé pour la $locale demandée.
|
||||
$titleStr = $tr('title');
|
||||
$subtitleStr = $tr('subtitle');
|
||||
$greetingStr = $tr('greeting');
|
||||
$introStr = $tr('intro');
|
||||
$instructionsStr = $tr('instructions');
|
||||
$fiTitleStr = $tr('firstInstallTitle');
|
||||
$fiBodyStr = $tr('firstInstallBody');
|
||||
$downloadBtnStr = $tr('downloadButton');
|
||||
$directLinkStr = $tr('directLink');
|
||||
$supportStr = $tr('support');
|
||||
$closingStr = $tr('closing');
|
||||
$signatureStr = $tr('signature');
|
||||
$footerStr = $tr('footer');
|
||||
|
||||
return <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html{$dirAttr}>
|
||||
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"></head>
|
||||
<body style="margin:0; padding:24px 12px; background-color:#F3F4F6; font-family:'Segoe UI',Arial,sans-serif; color:#1F2937;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" align="center" width="640"
|
||||
style="max-width:640px; margin:0 auto; border-collapse:collapse;">
|
||||
style="max-width:640px; margin:0 auto; border-collapse:collapse;"{$dirAttr}>
|
||||
<tr>
|
||||
<td bgcolor="#0F172A" align="center"
|
||||
style="background-color:#0F172A; padding:32px 24px; color:#FFFFFF; border-radius:8px 8px 0 0;">
|
||||
<div style="margin-bottom:16px;">{$logoBlock}</div>
|
||||
<h1 style="margin:0; font-size:24px; color:#FFFFFF; font-weight:600;">
|
||||
PROSERVE v{$versionEsc} is available!
|
||||
{$titleStr}
|
||||
</h1>
|
||||
<p style="margin:8px 0 0; color:#94A3B8; font-size:14px;">
|
||||
A new version is ready to install via your PROSERVE Launcher.
|
||||
{$subtitleStr}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td bgcolor="#FFFFFF"
|
||||
style="background-color:#FFFFFF; padding:28px 28px; border:1px solid #E5E7EB; border-top:none; border-radius:0 0 8px 8px;">
|
||||
<p style="margin:0 0 14px;">Hi {$ownerEsc},</p>
|
||||
<p style="margin:0 0 14px;">{$greetingStr}</p>
|
||||
<p style="margin:0 0 14px;">
|
||||
We're happy to let you know that
|
||||
<strong>PROSERVE version {$versionEsc}</strong>
|
||||
has just been published and is now available for download.
|
||||
{$introStr}
|
||||
</p>
|
||||
<p style="margin:0 0 14px;">
|
||||
If you already have the <strong>PROSERVE Launcher</strong> installed,
|
||||
simply open it on any of your machines, click
|
||||
<em>« Check for updates »</em>, and install the new version with one click.
|
||||
{$instructionsStr}
|
||||
</p>
|
||||
|
||||
<!-- Section « Don't have the launcher yet ? » : bouton de téléchargement
|
||||
@@ -200,13 +391,10 @@ function renderReleaseAnnounceEmail(string $version, string $ownerName, string $
|
||||
<tr>
|
||||
<td style="padding:18px 0;">
|
||||
<p style="margin:0 0 12px; font-weight:600; color:#1F2937;">
|
||||
First install or new workstation?
|
||||
{$fiTitleStr}
|
||||
</p>
|
||||
<p style="margin:0 0 16px; color:#4B5563;">
|
||||
If this is your first time, or you're setting up a new PC,
|
||||
download the PROSERVE Launcher installer below.
|
||||
<strong>You'll need to run it on EACH workstation</strong>
|
||||
where PROSERVE will be used.
|
||||
{$fiBodyStr}
|
||||
</p>
|
||||
<!-- Bouton stylé via table + bgcolor (vs CSS button) pour
|
||||
compat Outlook qui ne supporte ni border-radius CSS sur
|
||||
@@ -222,13 +410,13 @@ function renderReleaseAnnounceEmail(string $version, string $ownerName, string $
|
||||
color:#FFFFFF; text-decoration:none;
|
||||
font-family:'Segoe UI',Arial,sans-serif;
|
||||
font-size:14px; font-weight:600;">
|
||||
⬇ Download PROSERVE Launcher (Windows)
|
||||
{$downloadBtnStr}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p style="margin:12px 0 0; color:#9CA3AF; font-size:11px; text-align:center;">
|
||||
Direct link:
|
||||
{$directLinkStr}
|
||||
<a href="{$installerUrlEsc}" style="color:#3B82F6; text-decoration:underline; word-break:break-all;">{$installerUrlEsc}</a>
|
||||
</p>
|
||||
</td>
|
||||
@@ -237,19 +425,17 @@ function renderReleaseAnnounceEmail(string $version, string $ownerName, string $
|
||||
|
||||
{$notesBlock}
|
||||
<p style="margin:24px 0 14px;">
|
||||
As always, if you run into any issue (download failure, install error,
|
||||
anything weird in-game), don't hesitate to reach out — we're here to help.
|
||||
{$supportStr}
|
||||
</p>
|
||||
<p style="margin:0;">
|
||||
Happy training!<br/>
|
||||
<strong>The ASTERION VR team</strong>
|
||||
{$closingStr}<br/>
|
||||
<strong>{$signatureStr}</strong>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="padding:16px 8px; color:#9CA3AF; font-size:11px; line-height:1.5;">
|
||||
This automated notification was sent because your contact email is registered with one of your PROSERVE licenses.<br/>
|
||||
To stop receiving these, ask your ASTERION VR account manager to remove your address from the license contacts.
|
||||
{$footerStr}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -547,10 +733,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$notesFile = "$notesDir/{$entryId}.md";
|
||||
$notesBody = is_file($notesFile) ? file_get_contents($notesFile) : '';
|
||||
|
||||
// Récupère les licenses éligibles
|
||||
// Récupère les licenses éligibles. On inclut maintenant `language`
|
||||
// pour pouvoir localiser l'email — fallback 'en' si NULL ou inconnu.
|
||||
$db = Db::get($config);
|
||||
$stmt = $db->prepare(
|
||||
'SELECT id, owner_name, channel, can_see_betas, contact_emails, download_entitlement_until
|
||||
'SELECT id, owner_name, channel, can_see_betas, contact_emails, language, download_entitlement_until
|
||||
FROM licenses
|
||||
WHERE revoked_at IS NULL
|
||||
AND download_entitlement_until >= ?
|
||||
@@ -603,7 +790,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (empty($emails)) continue;
|
||||
$licsTouched++;
|
||||
|
||||
$subject = "PROSERVE v{$version} is now available";
|
||||
// Locale par license : whitelist contre les langues supportées,
|
||||
// fallback 'en' pour les licenses sans préférence définie.
|
||||
$locale = trim((string)($lic['language'] ?? ''));
|
||||
if (!in_array($locale, ['fr', 'en', 'zh', 'th', 'ar'], true)) {
|
||||
$locale = 'en';
|
||||
}
|
||||
// Sujet localisé via le même dictionnaire que le corps.
|
||||
$subjectTemplate = getEmailStrings($locale)['subject'] ?? 'PROSERVE v{version} is now available';
|
||||
$subject = str_replace('{version}', $version, $subjectTemplate);
|
||||
// Logo via CID inline si fichier disponible sur disque, sinon
|
||||
// chaîne vide → le template tombera sur le fallback texte
|
||||
// "ASTERION VR" en blanc sur le bandeau dark navy. On NE
|
||||
@@ -617,7 +812,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$lic['owner_name'] ?? '',
|
||||
$notesBody,
|
||||
$config['base_url'] ?? '',
|
||||
$logoSrc
|
||||
$logoSrc,
|
||||
$locale
|
||||
);
|
||||
|
||||
foreach ($emails as $email) {
|
||||
|
||||
11
server/migrations/005_license_language.sql
Normal file
11
server/migrations/005_license_language.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- PS_Launcher schema v5
|
||||
-- Ajoute language sur la table licenses : code de langue 2-letter (fr/en/zh/th/ar)
|
||||
-- utilisé pour localiser les emails de notification de release. NULL = English
|
||||
-- (defaut conservateur pour les licenses existantes qui n'ont pas été éditées).
|
||||
-- Le contenu de la release note elle-même reste TOUJOURS en anglais — seul le
|
||||
-- texte d'accompagnement (greeting, instructions, CTAs) est localisé.
|
||||
--
|
||||
-- À jouer après 004_license_contact_emails.sql.
|
||||
-- migrate.php attrape "Duplicate column" comme idempotent.
|
||||
|
||||
ALTER TABLE licenses ADD COLUMN language VARCHAR(8) NULL AFTER contact_emails;
|
||||
@@ -18,9 +18,9 @@
|
||||
<Product>PROSERVE Launcher</Product>
|
||||
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
||||
<RootNamespace>PSLauncher.App</RootNamespace>
|
||||
<Version>0.29.10</Version>
|
||||
<AssemblyVersion>0.29.10.0</AssemblyVersion>
|
||||
<FileVersion>0.29.10.0</FileVersion>
|
||||
<Version>1.0.0</Version>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<FileVersion>1.0.0.0</FileVersion>
|
||||
|
||||
<!-- Single-file self-contained publish profile (used by `dotnet publish`) -->
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
|
||||
@@ -8,7 +8,11 @@ namespace PSLauncher.Core.Localization;
|
||||
/// 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.
|
||||
/// Langues supportées :
|
||||
/// • fr (par défaut), en, zh (Simplified), th, ar : traduction complète historique
|
||||
/// • es, de : ajoutées en v0.29.11, traduction progressive — les strings non encore
|
||||
/// traduites tombent automatiquement sur l'anglais via T() (cf. fallback dans
|
||||
/// <see cref="T(string,string,string,string,string,string?,string?)"/>).
|
||||
/// Pour Arabic, FlowDirection doit aussi être basculé en RTL côté Window.
|
||||
/// </summary>
|
||||
public static class Strings
|
||||
@@ -27,6 +31,8 @@ public static class Strings
|
||||
("auto", "Automatique (langue du système)"),
|
||||
("fr", "Français"),
|
||||
("en", "English"),
|
||||
("es", "Español"),
|
||||
("de", "Deutsch"),
|
||||
("zh", "中文"),
|
||||
("th", "ไทย"),
|
||||
("ar", "العربية"),
|
||||
@@ -39,7 +45,7 @@ public static class Strings
|
||||
/// </summary>
|
||||
public static void Init(string? configValue)
|
||||
{
|
||||
var supported = new[] { "fr", "en", "zh", "th", "ar" };
|
||||
var supported = new[] { "fr", "en", "es", "de", "zh", "th", "ar" };
|
||||
var v = configValue?.Trim().ToLowerInvariant() ?? "auto";
|
||||
|
||||
if (v == "auto" || string.IsNullOrEmpty(v) || Array.IndexOf(supported, v) < 0)
|
||||
@@ -62,9 +68,21 @@ public static class Strings
|
||||
catch { /* culture inconnue, on garde le défaut */ }
|
||||
}
|
||||
|
||||
private static string T(string fr, string en, string zh, string th, string ar) => _lang switch
|
||||
/// <summary>
|
||||
/// Helper de localisation. Les 5 premiers paramètres (fr/en/zh/th/ar) sont
|
||||
/// requis = chaque string DOIT exister dans ces langues (traduction historique
|
||||
/// complète). Les 2 derniers (es, de) sont OPTIONNELS — si non fournis, on
|
||||
/// retombe sur l'anglais. Stratégie progressive : on traduit en es/de
|
||||
/// uniquement les strings les plus visibles (top bar, actions principales,
|
||||
/// états licenses, errors common) ; le reste reste en anglais pour les
|
||||
/// utilisateurs es/de tant qu'on n'a pas traduit. Mieux qu'un crash ou
|
||||
/// une chaîne vide, et l'anglais est globalement lisible par ces locuteurs.
|
||||
/// </summary>
|
||||
private static string T(string fr, string en, string zh, string th, string ar, string? es = null, string? de = null) => _lang switch
|
||||
{
|
||||
"en" => en,
|
||||
"es" => es ?? en, // fallback English si pas encore traduit
|
||||
"de" => de ?? en, // idem
|
||||
"zh" => zh,
|
||||
"th" => th,
|
||||
"ar" => ar,
|
||||
@@ -72,34 +90,36 @@ public static class Strings
|
||||
};
|
||||
|
||||
// ==================== TOP BAR ====================
|
||||
public static string TopBarCheckUpdates => T("🔄 Vérifier les MAJ", "🔄 Check for updates", "🔄 检查更新", "🔄 ตรวจสอบการอัปเดต", "🔄 التحقق من التحديثات");
|
||||
public static string TopBarSettings => T("⚙ Paramètres", "⚙ Settings", "⚙ 设置", "⚙ ตั้งค่า", "⚙ الإعدادات");
|
||||
public static string TopBarCheckUpdates => T("🔄 Vérifier les MAJ", "🔄 Check for updates", "🔄 检查更新", "🔄 ตรวจสอบการอัปเดต", "🔄 التحقق من التحديثات", "🔄 Buscar actualizaciones", "🔄 Updates suchen");
|
||||
public static string TopBarSettings => T("⚙ Paramètres", "⚙ Settings", "⚙ 设置", "⚙ ตั้งค่า", "⚙ الإعدادات", "⚙ Ajustes", "⚙ Einstellungen");
|
||||
|
||||
// ==================== BODY ====================
|
||||
public static string BodyVersions => T("Versions", "Versions", "版本", "เวอร์ชัน", "النسخ");
|
||||
public static string BodyVersions => T("Versions", "Versions", "版本", "เวอร์ชัน", "النسخ", "Versiones", "Versionen");
|
||||
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.",
|
||||
"启动、安装或删除版本。已安装的版本与服务器上可用的版本可以共存。",
|
||||
"เปิด ติดตั้ง หรือลบเวอร์ชัน เวอร์ชันที่ติดตั้งและที่มีในเซิร์ฟเวอร์อยู่ร่วมกันได้",
|
||||
"تشغيل أو تثبيت أو إزالة نسخة. النسخ المثبتة والمتاحة على الخادم يمكن أن تتعايش."
|
||||
"تشغيل أو تثبيت أو إزالة نسخة. النسخ المثبتة والمتاحة على الخادم يمكن أن تتعايش.",
|
||||
"Inicia, instala o elimina una versión. Las versiones instaladas y las disponibles en el servidor pueden coexistir.",
|
||||
"Starte, installiere oder entferne eine Version. Installierte und verfügbare Versionen können koexistieren."
|
||||
);
|
||||
|
||||
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 ", "发布于 ", "เผยแพร่เมื่อ ", "صدر في ");
|
||||
public static string FeaturedCurrent => T("VERSION COURANTE", "CURRENT VERSION", "当前版本", "เวอร์ชันปัจจุบัน", "النسخة الحالية", "VERSIÓN ACTUAL", "AKTUELLE VERSION");
|
||||
public static string SectionOther => T("AUTRES VERSIONS", "OTHER VERSIONS", "其他版本", "เวอร์ชันอื่นๆ", "نسخ أخرى", "OTRAS VERSIONES", "ANDERE VERSIONEN");
|
||||
public static string ReleasedOn => T("Sortie le ", "Released ", "发布于 ", "เผยแพร่เมื่อ ", "صدر في ", "Publicado el ", "Veröffentlicht am ");
|
||||
|
||||
// ==================== 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…", "🗑 卸载中…", "🗑 กำลังถอนการติดตั้ง…", "🗑 جارٍ الإزالة…");
|
||||
public static string StatusVerifying => T("🔍 Vérification…", "🔍 Verifying…", "🔍 校验中…", "🔍 กำลังตรวจสอบ…", "🔍 جارٍ التحقق…");
|
||||
public static string StatusInstalled => T("● Installée", "● Installed", "● 已安装", "● ติดตั้งแล้ว", "● مثبتة", "● Instalada", "● Installiert");
|
||||
public static string StatusAvailable => T("○ Disponible", "○ Available", "○ 可用", "○ พร้อมใช้งาน", "○ متاحة", "○ Disponible", "○ Verfügbar");
|
||||
public static string StatusDownloading => T("⬇ Téléchargement…", "⬇ Downloading…", "⬇ 下载中…", "⬇ กำลังดาวน์โหลด…", "⬇ جارٍ التنزيل…", "⬇ Descargando…", "⬇ Wird heruntergeladen…");
|
||||
public static string StatusInstalling => T("📦 Installation…", "📦 Installing…", "📦 安装中…", "📦 กำลังติดตั้ง…", "📦 جارٍ التثبيت…", "📦 Instalando…", "📦 Wird installiert…");
|
||||
public static string StatusUninstalling => T("🗑 Suppression…", "🗑 Uninstalling…", "🗑 卸载中…", "🗑 กำลังถอนการติดตั้ง…", "🗑 جارٍ الإزالة…", "🗑 Eliminando…", "🗑 Wird entfernt…");
|
||||
public static string StatusVerifying => T("🔍 Vérification…", "🔍 Verifying…", "🔍 校验中…", "🔍 กำลังตรวจสอบ…", "🔍 جارٍ التحقق…", "🔍 Verificando…", "🔍 Wird überprüft…");
|
||||
|
||||
// ==================== BETA BADGE ====================
|
||||
/// <summary>Texte affiché dans la pill orange à côté de la version. Court et localisé.</summary>
|
||||
public static string BetaBadgeLabel => T("BÊTA", "BETA", "测试版", "เบต้า", "تجريبي");
|
||||
public static string BetaBadgeLabel => T("BÊTA", "BETA", "测试版", "เบต้า", "تجريبي", "BETA", "BETA");
|
||||
|
||||
/// <summary>Tooltip par défaut quand la version est tag BÊTA mais sans note des testeurs.</summary>
|
||||
public static string BetaBadgeTooltipDefault => T(
|
||||
@@ -120,17 +140,17 @@ public static class Strings
|
||||
);
|
||||
|
||||
// ==================== 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 ActionOk => T("OK", "OK", "确定", "ตกลง", "موافق");
|
||||
public static string ActionYes => T("Oui", "Yes", "是", "ใช่", "نعم");
|
||||
public static string ActionNo => T("Non", "No", "否", "ไม่", "لا");
|
||||
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 ActionLaunch => T("▶ Lancer", "▶ Launch", "▶ 启动", "▶ เปิด", "▶ تشغيل", "▶ Iniciar", "▶ Starten");
|
||||
public static string ActionLaunchBig => T("▶ LANCER", "▶ LAUNCH", "▶ 启动", "▶ เปิด", "▶ تشغيل", "▶ INICIAR", "▶ STARTEN");
|
||||
public static string ActionInstall => T("⬇ Installer", "⬇ Install", "⬇ 安装", "⬇ ติดตั้ง", "⬇ تثبيت", "⬇ Instalar", "⬇ Installieren");
|
||||
public static string ActionInstallBig => T("⬇ INSTALLER", "⬇ INSTALL", "⬇ 安装", "⬇ ติดตั้ง", "⬇ تثبيت", "⬇ INSTALAR", "⬇ INSTALLIEREN");
|
||||
public static string ActionCancel => T("Annuler", "Cancel", "取消", "ยกเลิก", "إلغاء", "Cancelar", "Abbrechen");
|
||||
public static string ActionOk => T("OK", "OK", "确定", "ตกลง", "موافق", "OK", "OK");
|
||||
public static string ActionYes => T("Oui", "Yes", "是", "ใช่", "نعم", "Sí", "Ja");
|
||||
public static string ActionNo => T("Non", "No", "否", "ไม่", "لا", "No", "Nein");
|
||||
public static string ActionLater => T("Plus tard", "Later", "稍后", "ภายหลัง", "لاحقاً", "Más tarde", "Später");
|
||||
public static string ActionSave => T("Enregistrer", "Save", "保存", "บันทึก", "حفظ", "Guardar", "Speichern");
|
||||
public static string ActionClose => T("Fermer", "Close", "关闭", "ปิด", "إغلاق", "Cerrar", "Schließen");
|
||||
// Affiché sur le bouton install d'une version dont le minLicenseDate dépasse l'entitlement de la license.
|
||||
// Concrètement : ta license expire le 2024-12-31 mais cette version a été sortie le 2025-03-15 → tu ne peux pas la télécharger.
|
||||
// Tu peux toujours installer/lancer une version antérieure couverte par ta période de license.
|
||||
@@ -270,13 +290,13 @@ public static class Strings
|
||||
public static string LauncherUpdateNow => T("⬇ Mettre à jour", "⬇ Update now", "⬇ 立即更新", "⬇ อัปเดตเดี๋ยวนี้", "⬇ تحديث الآن");
|
||||
|
||||
// ==================== MESSAGEBOX TITLES ====================
|
||||
public static string MsgBoxError => T("Erreur", "Error", "错误", "ข้อผิดพลาด", "خطأ");
|
||||
public static string MsgBoxLaunchError => T("Erreur de lancement", "Launch error", "启动错误", "เปิดใช้งานล้มเหลว", "خطأ في التشغيل");
|
||||
public static string MsgBoxConfirm => T("Confirmer", "Confirm", "确认", "ยืนยัน", "تأكيد");
|
||||
public static string MsgBoxInfo => T("Information", "Information", "信息", "ข้อมูล", "معلومة");
|
||||
public static string MsgBoxPatience => T("Patience", "Please wait", "请稍候", "โปรดรอ", "يرجى الانتظار");
|
||||
public static string MsgBoxLanguageChange => T("Changement de langue", "Language change", "更改语言", "เปลี่ยนภาษา", "تغيير اللغة");
|
||||
public static string MsgBoxReleaseNotes => T("Release notes", "Release notes", "版本说明", "บันทึกการเปลี่ยนแปลง", "ملاحظات الإصدار");
|
||||
public static string MsgBoxError => T("Erreur", "Error", "错误", "ข้อผิดพลาด", "خطأ", "Error", "Fehler");
|
||||
public static string MsgBoxLaunchError => T("Erreur de lancement", "Launch error", "启动错误", "เปิดใช้งานล้มเหลว", "خطأ في التشغيل", "Error al iniciar", "Startfehler");
|
||||
public static string MsgBoxConfirm => T("Confirmer", "Confirm", "确认", "ยืนยัน", "تأكيد", "Confirmar", "Bestätigen");
|
||||
public static string MsgBoxInfo => T("Information", "Information", "信息", "ข้อมูล", "معلومة", "Información", "Information");
|
||||
public static string MsgBoxPatience => T("Patience", "Please wait", "请稍候", "โปรดรอ", "يرجى الانتظار", "Por favor espera", "Bitte warten");
|
||||
public static string MsgBoxLanguageChange => T("Changement de langue", "Language change", "更改语言", "เปลี่ยนภาษา", "تغيير اللغة", "Cambio de idioma", "Sprachänderung");
|
||||
public static string MsgBoxReleaseNotes => T("Release notes", "Release notes", "版本说明", "บันทึกการเปลี่ยนแปลง", "ملاحظات الإصدار", "Notas de versión", "Versionshinweise");
|
||||
|
||||
// ==================== MESSAGEBOX MESSAGES ====================
|
||||
public static string MsgBusy => T(
|
||||
@@ -860,7 +880,7 @@ public static class Strings
|
||||
);
|
||||
|
||||
// ---- Mode auto (auto-launch + auto-relaunch après exit) ----
|
||||
public static string ActionAuto => T("AUTO", "AUTO", "自动", "อัตโนมัติ", "تلقائي");
|
||||
public static string ActionAuto => T("AUTO", "AUTO", "自动", "อัตโนมัติ", "تلقائي", "AUTO", "AUTO");
|
||||
public static string AutoModeTooltipActive => T(
|
||||
"Cette version est désignée pour le lancement automatique. Clique pour désactiver.",
|
||||
"This version is set for auto-launch. Click to disable.",
|
||||
|
||||
Reference in New Issue
Block a user