#!/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);