HormoneTrack-web/scripts/i18n-check.mjs
Siphonight 491efef8eb v1.4.10 — portage initial de l'app Android v1.4.10
Portage navigateur complet de l'app Android HormoneTrack v1.4.10 :
- moteur PK fidèle (Estrannaise tables ODS, Transfem Science V3C,
  WHSAH fit Mona, Bateman) — paramètres identiques, invariants Android
  préservés (fixes #19-#23, #35, #52-#62), 121 tests Node épinglés
- calibration par période d'ester et par modèle affiché (#60/#61)
- UI 6 écrans + éditeur, CurveChart Canvas (zoom/pan fractionnaire,
  prévision, pics/creux, fuseau configurable), rappels web
  (Notification API), seuils d'alerte, i18n FR/EN
- sauvegarde JSON v2 compatible Android bidirectionnelle (rétrocompat v1)
- 100 % local : localStorage, aucun serveur applicatif, aucune télémétrie
- processus : scripts/check.sh (syntaxe, i18n, tests, E2E navigateur,
  smoke HTTP), docs séparées (README + DEVELOPPEMENT + CHANGELOG)
- dépôt GIT SÉPARÉ de l'Android : historique 100 % propre, versions
  alignées sur l'Android porté, changelogs indépendants

Non porté (impossible dans un navigateur, documenté §12) : agenda
récurrent, notifications onglet fermé, montre.
2026-09-08 14:34:58 +02:00

95 lines
3.3 KiB
JavaScript

#!/usr/bin/env node
/**
* i18n-check.mjs — cohérence des dictionnaires FR/EN (utilisé par check.sh).
*
* Vérifie :
* 1. chaque clé de EN existe dans FR et inversement (UI mi-traduite sinon) ;
* 2. chaque clé utilisée via t('…') dans js/ existe dans les deux dicts
* (leçon Android §12 : une référence manquante = texte brut à l'écran).
*
* Sortie : silencieuse si tout va bien, liste les problèmes sinon (exit 1).
* Aucune dépendance : parse le module i18n.js par regex (les dicts sont des
* littéraux plats) et grep les usages dans les sources.
*/
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const i18nPath = join(root, 'js/util/i18n.js');
const src = readFileSync(i18nPath, 'utf8');
function extractDict(name) {
const m = src.match(new RegExp(`const ${name} = \\{([\\s\\S]*?)\\n\\};`));
if (!m) throw new Error(`dictionnaire ${name} introuvable`);
const keys = [];
// Les clés suivent la convention identifiant_underscore (a-z0-9_)
for (const km of m[1].matchAll(/^\s{2}([a-z0-9_]+):/gm)) keys.push(km[1]);
return new Set(keys);
}
const EN = extractDict('EN');
const FR = extractDict('FR');
// Les presets vivent dans des dicts dédiés (même convention de clés)
for (const k of extractDict('EN_PRESETS')) EN.add(k);
for (const k of extractDict('FR_PRESETS')) FR.add(k);
let failed = false;
// 1. EN ↔ FR
for (const k of EN) {
if (!FR.has(k)) {
console.error(`✗ clé EN absente de FR : ${k}`);
failed = true;
}
}
for (const k of FR) {
if (!EN.has(k)) {
console.error(`✗ clé FR absente de EN : ${k}`);
failed = true;
}
}
// 2. Clés utilisées dans les sources (t('key') et trKey('key'))
function walk(dir, out = []) {
for (const entry of readdirSync(dir)) {
const p = join(dir, entry);
if (statSync(p).isDirectory()) walk(p, out);
else if (p.endsWith('.js')) out.push(p);
}
return out;
}
const files = walk(join(root, 'js')).filter((f) => !f.endsWith('i18n.js'));
const used = new Set();
const usageRe = /\b(?:t|trKey)\('([a-z0-9_]+)'/g;
for (const f of files) {
const content = readFileSync(f, 'utf8');
for (const m of content.matchAll(usageRe)) used.add(m[1]);
}
// Clés de presets (résolues dynamiquement via nameKey — hors t('…') littéral)
const presetsSrc = readFileSync(join(root, 'js/pk/presets.js'), 'utf8');
for (const m of presetsSrc.matchAll(/'([a-z0-9_]+)'/g)) {
if (m[1].startsWith('preset_')) used.add(m[1]);
}
// Clés de modèles/labels résolues dynamiquement (modelLabelKey, routeLabelKey…)
for (const k of ['model_ese', 'model_tfs', 'model_whs', 'route_oral', 'route_gel', 'route_patch',
'route_injection_im', 'route_injection_sc', 'route_other', 'type_estradiol', 'type_antiandrogen',
'type_progestogen', 'type_other']) {
used.add(k);
}
for (const k of used) {
if (!EN.has(k)) {
console.error(`✗ clé utilisée absente de EN : ${k}`);
failed = true;
}
if (!FR.has(k)) {
console.error(`✗ clé utilisée absente de FR : ${k}`);
failed = true;
}
}
console.error(` i18n : ${EN.size} clés EN / ${FR.size} clés FR / ${used.size} clés utilisées`);
if (!failed) console.error(' i18n cohérent');
process.exit(failed ? 1 : 0);