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.
95 lines
3.3 KiB
JavaScript
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);
|