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:
2026-05-28 18:01:50 +02:00
parent 500f7d12e6
commit 73d084a703
6 changed files with 353 additions and 89 deletions

View File

@@ -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"