Aller au contenu

Internationalisation des outils annexes

En plus du backend Spring Boot et du frontend React, PerfShop intègre plusieurs outils annexes qui disposent chacun de leurs propres dictionnaires de traduction. Ces outils suivent un pattern commun, volontairement simple et sans framework : des fichiers JSON plats, un loader vanilla, un attribut HTML déclaratif data-i18n et une fonction globale _t().

Cette page regroupe la référence pour les cinq outils concernés : chaos-admin, monitoring, scripts-ui, jmeter-ui, et welcome.

Vue d'ensemble

Outil Chemin Loader Nombre de clés (ordre de grandeur)
chaos-admin chaos-admin/public/i18n/{fr,en}.json chaos-admin/public/js/i18n.js ~400
monitoring monitoring/public/i18n/{fr,en}.json monitoring/public/js/i18n.js ~150
scripts-ui scripts-ui/public/i18n/{fr,en}.json scripts-ui/public/js/i18n.js ~200
jmeter-ui jmeter-ui/public/i18n/{fr,en}.json jmeter-ui/public/js/i18n.js ~180
welcome welcome/i18n/{fr,en}.json welcome/entrypoint.sh + welcome.js ~30

Les quatre premiers outils utilisent exactement le même pattern — un loader JavaScript vanilla asynchrone. La welcome page utilise une variante : l'injection se fait par un script shell entrypoint.sh qui ajoute window.__I18N__ = {...} en tête de welcome.js.

Le loader vanilla i18n.js

Le fichier chaos-admin/public/js/i18n.js est le loader de référence. Il est dupliqué (ou adapté) pour les autres outils. Son code tient en une centaine de lignes et n'a aucune dépendance externe.

Variables globales

var PERFSHOP_LANG = (window.__CONFIG__ && window.__CONFIG__.LANG) || 'fr';

var DATE_LOCALE = (function () {
  var map = {
    fr: 'fr-FR',
    en: 'en-GB',
    es: 'es-ES',
    pt: 'pt-BR',
    zh: 'zh-CN',
    hi: 'hi-IN',
    ar: 'ar-SA'
  };
  return map[PERFSHOP_LANG] || map.fr;
})();

var I18N = {};

La langue est lue depuis window.__CONFIG__.LANG, qui est injecté par le conteneur via un script config.js servi dynamiquement. En cas d'absence, le défaut est fr.

Une table de correspondance expose une DATE_LOCALE cohérente pour chaque langue supportée, y compris des langues anticipées (portugais, chinois, hindi, arabe) même si leurs dictionnaires ne sont pas encore écrits.

Le dictionnaire I18N est initialement vide ; il est rempli de façon asynchrone par loadI18n().

Fonction _t() — traduction avec placeholders numérotés

function _t(key) {
  var text = (key in I18N) ? I18N[key] : key;
  for (var i = 1; i < arguments.length; i++) {
    text = text.replace('{' + (i - 1) + '}', arguments[i]);
  }
  return text;
}

Le fallback est la clé elle-même — comme côté React. Les placeholders sont numérotés ({0}, {1}, {2}) et non nommés, pour coller au style MessageFormat du backend Spring.

Usage typique :

showToast(_t('toast.licence.activated'));
badge.textContent = _t('badge.level.active', levelName);
status.textContent = _t('student.license.valid_until', expireDate);

Fonction applyI18n() — application déclarative sur le DOM

function applyI18n() {
  document.querySelectorAll('[data-i18n]').forEach(function (el) {
    var key = el.getAttribute('data-i18n');
    if (!key) return;
    if (el.tagName === 'TITLE') {
      document.title = _t(key);
    } else {
      el.textContent = _t(key);
    }
  });
  document.querySelectorAll('[data-i18n-html]').forEach(function (el) {
    var key = el.getAttribute('data-i18n-html');
    if (key) el.innerHTML = _t(key);
  });
  document.querySelectorAll('[data-i18n-placeholder]').forEach(function (el) {
    var key = el.getAttribute('data-i18n-placeholder');
    if (key) el.placeholder = _t(key);
  });
  document.querySelectorAll('[data-i18n-title]').forEach(function (el) {
    var key = el.getAttribute('data-i18n-title');
    if (key) el.title = _t(key);
  });
  var html = document.documentElement;
  if (html) html.lang = PERFSHOP_LANG;
}

Quatre attributs déclaratifs sont supportés :

Attribut Cible Usage
data-i18n textContent (ou document.title pour <title>) Cas par défaut, sûr (pas d'HTML)
data-i18n-html innerHTML Quand la traduction contient du balisage inline volontaire (<strong>, <code>, <br>)
data-i18n-placeholder placeholder Pour les champs <input> et <textarea>
data-i18n-title title (attribut HTML) Pour les infobulles natives

data-i18n vs data-i18n-html

Le premier est la voie par défaut et sûre. Le second est réservé aux chaînes qui contiennent volontairement du balisage HTML (par exemple <strong>Chaos Scripting</strong> augmente la complexité...). Dans le second cas, le dictionnaire est statique côté serveur et ne contient aucun input utilisateur — innerHTML est donc sûr dans ce contexte.

Exemples d'utilisation dans le HTML

<title data-i18n="admin.page.title">PerfShop — Chaos Engineering</title>

<h1><span data-i18n="admin.title">Chaos Engineering</span></h1>

<button data-i18n="admin.btn.reload">Recharger</button>

<input type="email" id="new-email"
       data-i18n-placeholder="gestion.create.email.ph"
       placeholder="formateur@example.com">

<div class="scripting-intro" data-i18n-html="admin.scripting.intro">
  🔐 <strong>Chaos Scripting</strong> augmente la complexité...
</div>

Le texte entre les balises est la valeur de fallback affichée avant l'exécution du loader i18n (en cas de chargement lent ou d'erreur réseau) — c'est aussi la valeur de référence française servant de documentation inline.

Chargement asynchrone

function loadI18n() {
  var lang = PERFSHOP_LANG;
  return _fetchJson('/i18n/' + lang + '.json')
    .catch(function (err) {
      if (lang !== 'fr') {
        console.warn('[i18n] ' + lang + '.json introuvable, fallback → fr.json');
        return _fetchJson('/i18n/fr.json');
      }
      console.error('[i18n] fr.json introuvable :', err.message);
      return null;
    })
    .then(function (dict) {
      I18N = (dict != null && typeof dict === 'object') ? dict : {};
    })
    .then(function () {
      if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', applyI18n);
      } else {
        applyI18n();
      }
    });
}

window._i18nReady = loadI18n();

La chaîne de fallback est :

  1. fetch /i18n/<lang>.json → succès → I18N = dict
  2. Si échec et lang !== 'fr'fetch /i18n/fr.json → succès → I18N = dict FR
  3. Si tout échoue → I18N = {}_t() retourne la clé brute

À la fin du chargement, applyI18n() est appelé — soit immédiatement si le DOM est déjà prêt, soit après l'événement DOMContentLoaded.

La Promise window._i18nReady est exposée globalement. Les scripts métier qui appellent _t() au démarrage (avant le DOMContentLoaded) doivent attendre cette promesse :

window._i18nReady.then(function () {
  // _t() est maintenant disponible
  _buildClassicLabels();
  Tabs.init();
  loadStatus();
});

C'est exactement ce que fait student-main.js dans la page chaos étudiant — voir Page chaos étudiant.

Format des dictionnaires

Chaque fichier <lang>.json est un objet JSON plat identique en structure à frontend/src/i18n/fr.json :

{
  "admin.page.title": "PerfShop — Chaos Engineering",
  "admin.title": "Chaos Engineering",
  "admin.subtitle": "perfshop — injection d'anomalies backend & frontend",
  "admin.btn.reload": "Recharger",
  "admin.btn.reset": "Reset tout",
  "admin.btn.logout": "Déconnexion",
  "admin.student.label": "Page étudiant",
  "admin.student.active": "Active",
  "admin.student.blocked": "Bloquée",
  ...
}

Les clés suivent la convention hiérarchique par points, en anglais. Le préfixe désigne la section ou le module du panneau (admin.*, gestion.*, login.*, lic.*, etc.).

Chargement dans l'ordre des scripts

L'ordre de chargement est critique. Dans chaque page HTML des outils annexes, la séquence est la suivante :

<!-- 1. Config publique injectée par le conteneur -->
<script src="/config.js"></script>

<!-- 2. Loader i18n — expose window._i18nReady -->
<script src="/js/i18n.js"></script>

<!-- 3. Scripts métier — peuvent appeler _t() après _i18nReady -->
<script src="/admin/chaos-sections.js"></script>
<script src="/admin/js/gestion.js"></script>

Chaque script métier doit soit attendre window._i18nReady.then(...), soit ne consommer _t() que dans des handlers d'événements (clics, polls) qui sont toujours déclenchés après le chargement initial.

Cas particulier : la welcome page

La welcome page (voir Page d'accueil welcome) utilise une variante du pattern, dictée par le fait que son JavaScript s'exécute dans un contexte Nginx sans config dynamique.

Au démarrage du conteneur, entrypoint.sh lit le fichier /usr/share/nginx/html/i18n/<lang>.json, y ajoute la clé interne __lang__ pour refléter la langue active, et injecte le contenu au début de welcome.js comme une déclaration globale :

I18N_JSON=$(cat "${I18N_FILE}")
I18N_JSON=$(printf '%s' "${I18N_JSON}" | sed 's/^{/{  "__lang__": "'"${LANG}"'",/')

TMPFILE=$(mktemp)
printf '// i18n — injecté par entrypoint.sh\nwindow.__I18N__ = %s;\n\n' "${I18N_JSON}" > "${TMPFILE}"
cat "${WEBROOT}/welcome.js" >> "${TMPFILE}"
mv "${TMPFILE}" "${WEBROOT}/welcome.js"
chmod 644 "${WEBROOT}/welcome.js"

Le chmod 644 final est critique : mktemp crée le fichier en 600 root-only, ce qui empêcherait le worker Nginx de le lire et provoquerait un 403.

Dans le navigateur, welcome.js trouve window.__I18N__ déjà disponible avant toute exécution et peut y accéder directement sans fetch. La fonction de traduction est plus simple parce qu'elle n'a pas à gérer de chargement asynchrone :

function t(key) {
  return (window.__I18N__ && window.__I18N__[key]) || key;
}

Si le fichier i18n demandé n'existe pas, entrypoint.sh fait un fallback vers fr.json avec un warning dans les logs du conteneur.

Volumétrie par outil

Outil Clés approximatives Rôle principal
chaos-admin ~400 Panneau formateur complet + page étudiant (7 onglets, sliders, modales, toasts, licence)
scripts-ui ~200 Éditeur web de scripts Robot Framework / pytest — arborescence, menus, messages
jmeter-ui ~180 Lanceur de tests JMeter — configuration, exécution, résultats
monitoring ~150 Dashboard HTML temps réel — labels, tooltips, messages d'état
welcome ~30 Page d'accueil minimaliste — titre, tagline, en-têtes de tableau, descriptions de services

Symétrie FR/EN

Comme pour les dictionnaires backend et frontend, la règle est absolue : toute clé en FR doit exister en EN et réciproquement. Un script shell type :

for tool in chaos-admin/public monitoring/public scripts-ui/public jmeter-ui/public welcome; do
  echo "=== $tool ==="
  jq 'keys | sort' "$tool/i18n/fr.json" > /tmp/fr.keys
  jq 'keys | sort' "$tool/i18n/en.json" > /tmp/en.keys
  diff /tmp/fr.keys /tmp/en.keys && echo "  ✓ symétrique"
done

Au moment de cette rédaction, les cinq outils sont en parfaite symétrie FR/EN.

Ajouter une nouvelle langue à un outil

Le processus est trivial et ne nécessite aucune modification de code :

  1. Copier public/i18n/fr.json vers public/i18n/<lang>.json
  2. Traduire les valeurs (laisser les clés telles quelles)
  3. Redéployer avec PERFSHOP_LANG=<lang>

Le loader i18n.js détectera automatiquement le nouveau fichier via le pattern /i18n/${lang}.json et l'utilisera. Aucune inscription dans un registre, aucun import à ajouter.

Pour la welcome page, le principe est identique — créer welcome/i18n/<lang>.json, redéployer avec PERFSHOP_LANG=<lang>. Le script entrypoint.sh détectera le fichier au démarrage et l'injectera.

Ajouter une nouvelle clé

  1. Ajouter la clé en FR dans public/i18n/fr.json (ou welcome/i18n/fr.json)
  2. Ajouter la même clé en EN dans le fichier correspondant
  3. Utiliser la clé dans le HTML via data-i18n="ma.cle" ou dans le JS via _t('ma.cle')
  4. Redéployer (pas de build à faire — les fichiers JSON sont servis statiquement)

Voir aussi