Auxiliary tools internationalization¶
Beyond the Spring Boot backend and the React frontend, PerfShop integrates several auxiliary tools that each have their own translation dictionaries. These tools follow a common, deliberately simple and framework-free pattern: flat JSON files, a vanilla loader, a declarative HTML data-i18n attribute, and a global _t() function.
This page groups the reference for the five tools involved: chaos-admin, monitoring, scripts-ui, jmeter-ui, and welcome.
Overview¶
| Tool | Path | Loader | Key count (order of magnitude) |
|---|---|---|---|
| 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 |
The first four tools use exactly the same pattern — an asynchronous vanilla JavaScript loader. The welcome page uses a variant: injection is done by an entrypoint.sh shell script that adds window.__I18N__ = {...} at the top of welcome.js.
The i18n.js vanilla loader¶
The chaos-admin/public/js/i18n.js file is the reference loader. It is duplicated (or adapted) for the other tools. Its code fits in about a hundred lines and has no external dependencies.
Global variables¶
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 = {};
The language is read from window.__CONFIG__.LANG, which is injected by the container via a dynamically served config.js script. If absent, the default is fr.
A mapping table exposes a consistent DATE_LOCALE for each supported language, including anticipated languages (Portuguese, Chinese, Hindi, Arabic) even if their dictionaries are not yet written.
The I18N dictionary is initially empty; it is populated asynchronously by loadI18n().
_t() function — translation with numbered placeholders¶
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;
}
The fallback is the key itself — just like on the React side. Placeholders are numbered ({0}, {1}, {2}) and not named, to match the MessageFormat style of the Spring backend.
Typical usage:
showToast(_t('toast.licence.activated'));
badge.textContent = _t('badge.level.active', levelName);
status.textContent = _t('student.license.valid_until', expireDate);
applyI18n() function — declarative DOM application¶
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;
}
Four declarative attributes are supported:
| Attribute | Target | Usage |
|---|---|---|
data-i18n |
textContent (or document.title for <title>) |
Default case, safe (no HTML) |
data-i18n-html |
innerHTML |
When the translation intentionally contains inline markup (<strong>, <code>, <br>) |
data-i18n-placeholder |
placeholder |
For <input> and <textarea> fields |
data-i18n-title |
title (HTML attribute) |
For native tooltips |
data-i18n vs data-i18n-html
The first is the default, safe path. The second is reserved for strings that intentionally contain HTML markup (for example <strong>Chaos Scripting</strong> increases complexity...). In the second case, the dictionary is static on the server side and contains no user input — innerHTML is therefore safe in this context.
HTML usage examples¶
<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">Reload</button>
<input type="email" id="new-email"
data-i18n-placeholder="gestion.create.email.ph"
placeholder="instructor@example.com">
<div class="scripting-intro" data-i18n-html="admin.scripting.intro">
🔐 <strong>Chaos Scripting</strong> increases complexity...
</div>
The text between the tags is the fallback value displayed before the i18n loader runs (in case of slow loading or a network error) — it is also the French reference value serving as inline documentation.
Asynchronous loading¶
function loadI18n() {
var lang = PERFSHOP_LANG;
return _fetchJson('/i18n/' + lang + '.json')
.catch(function (err) {
if (lang !== 'fr') {
console.warn('[i18n] ' + lang + '.json not found, fallback → fr.json');
return _fetchJson('/i18n/fr.json');
}
console.error('[i18n] fr.json not found:', 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();
The fallback chain is:
fetch /i18n/<lang>.json→ success →I18N = dict- If failure and
lang !== 'fr'→fetch /i18n/fr.json→ success →I18N = dict FR - If everything fails →
I18N = {}→_t()returns the raw key
At the end of loading, applyI18n() is called — either immediately if the DOM is already ready, or after the DOMContentLoaded event.
The window._i18nReady promise is exposed globally. Business scripts that call _t() at startup (before DOMContentLoaded) must wait for this promise:
window._i18nReady.then(function () {
// _t() is now available
_buildClassicLabels();
Tabs.init();
loadStatus();
});
That's exactly what student-main.js does in the student chaos page — see Student chaos page.
Dictionary format¶
Each <lang>.json file is a flat JSON object identical in structure to frontend/src/i18n/fr.json:
{
"admin.page.title": "PerfShop — Chaos Engineering",
"admin.title": "Chaos Engineering",
"admin.subtitle": "perfshop — backend & frontend anomaly injection",
"admin.btn.reload": "Reload",
"admin.btn.reset": "Reset all",
"admin.btn.logout": "Log out",
"admin.student.label": "Student page",
"admin.student.active": "Active",
"admin.student.blocked": "Blocked"
}
Keys follow the dotted hierarchical convention, in English. The prefix designates the section or module of the panel (admin.*, gestion.*, login.*, lic.*, etc.).
Loading in script order¶
The loading order is critical. In each HTML page of the auxiliary tools, the sequence is:
<!-- 1. Public config injected by the container -->
<script src="/config.js"></script>
<!-- 2. i18n loader — exposes window._i18nReady -->
<script src="/js/i18n.js"></script>
<!-- 3. Business scripts — can call _t() after _i18nReady -->
<script src="/admin/chaos-sections.js"></script>
<script src="/admin/js/gestion.js"></script>
Each business script must either wait on window._i18nReady.then(...), or only consume _t() inside event handlers (clicks, polls) that are always triggered after initial loading.
Special case: the welcome page¶
The welcome page (see Welcome page) uses a variant of the pattern, dictated by the fact that its JavaScript runs in an Nginx context without dynamic config.
On container startup, entrypoint.sh reads the /usr/share/nginx/html/i18n/<lang>.json file, adds the internal key __lang__ to it to reflect the active language, and injects the content at the beginning of welcome.js as a global declaration:
I18N_JSON=$(cat "${I18N_FILE}")
I18N_JSON=$(printf '%s' "${I18N_JSON}" | sed 's/^{/{ "__lang__": "'"${LANG}"'",/')
TMPFILE=$(mktemp)
printf '// i18n — injected by 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"
The final chmod 644 is critical: mktemp creates the file in 600 root-only, which would prevent the Nginx worker from reading it and would cause a 403.
In the browser, welcome.js finds window.__I18N__ already available before any execution and can access it directly without fetch. The translation function is simpler because it has no async loading to handle:
If the requested i18n file does not exist, entrypoint.sh falls back to fr.json with a warning in the container logs.
Volumetry by tool¶
| Tool | Approximate keys | Main role |
|---|---|---|
| chaos-admin | ~400 | Full instructor panel + student page (7 tabs, sliders, modals, toasts, license) |
| scripts-ui | ~200 | Web editor for Robot Framework / pytest scripts — tree, menus, messages |
| jmeter-ui | ~180 | JMeter test launcher — configuration, execution, results |
| monitoring | ~150 | Real-time HTML dashboard — labels, tooltips, state messages |
| welcome | ~30 | Minimalist landing page — title, tagline, table headers, service descriptions |
FR/EN symmetry¶
Like the backend and frontend dictionaries, the rule is absolute: every key in FR must exist in EN and vice versa. A typical shell script:
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 " ✓ symmetric"
done
At the time of writing, all five tools are in perfect FR/EN symmetry.
Adding a new language to a tool¶
The process is trivial and requires no code modification:
- Copy
public/i18n/fr.jsontopublic/i18n/<lang>.json - Translate the values (leave the keys as-is)
- Redeploy with
PERFSHOP_LANG=<lang>
The i18n.js loader will automatically detect the new file via the /i18n/${lang}.json pattern and use it. No registry entry, no import to add.
For the welcome page, the principle is identical — create welcome/i18n/<lang>.json, redeploy with PERFSHOP_LANG=<lang>. The entrypoint.sh script will detect the file at startup and inject it.
Adding a new key¶
- Add the key in FR in
public/i18n/fr.json(orwelcome/i18n/fr.json) - Add the same key in EN in the corresponding file
- Use the key in HTML via
data-i18n="my.key"or in JS via_t('my.key') - Redeploy (no build needed — JSON files are served statically)