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 :
fetch /i18n/<lang>.json→ succès →I18N = dict- Si échec et
lang !== 'fr'→fetch /i18n/fr.json→ succès →I18N = dict FR - 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 :
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 :
- Copier
public/i18n/fr.jsonverspublic/i18n/<lang>.json - Traduire les valeurs (laisser les clés telles quelles)
- 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é¶
- Ajouter la clé en FR dans
public/i18n/fr.json(ouwelcome/i18n/fr.json) - Ajouter la même clé en EN dans le fichier correspondant
- Utiliser la clé dans le HTML via
data-i18n="ma.cle"ou dans le JS via_t('ma.cle') - Redéployer (pas de build à faire — les fichiers JSON sont servis statiquement)