test(i18n): fail npm test on a key present in one language only (#453) #468
No reviewers
Labels
No labels
autopilot:pending-human
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-clarification
status:needs-fix
status:ready
status:review
status:triage
type:bug
type:feature
type:infra
type:refactor
type:schema
type:security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: maximus/Simpl-Resultat#468
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-453-i18n-keys-guard"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Resolves #453
Generated autonomously by /autopilot run of 2026-10-18
Link 1 of 8 of the milestone
planned-2026-10-16-lot-v018(#453 → #449 → #372 → #429 → #393 → #437 → #384 → #446). Base:main(6e8e157). Every later link is stacked on this one, so its own PR runs the guard.What it does
npm testnow fails whensrc/i18n/locales/fr.jsonandsrc/i18n/locales/en.jsondo not declare the same keys. Before, a key written in one file only passed the build and the whole suite (mutations 1 and 2 below: the guard is the only failing test of the 2305), then showed raw to an English user (key missing fromen.json) or in English to a French user (key missing fromfr.json, English being the fallback ofsrc/i18n/config.ts).docs.dashboard.features.6), plural suffixes too (import.sources.fileCount_one).common.cancel,dashboard.help.tips,dashboard.help.tips.0): an unread or empty file would make the comparison pass on nothing.Run on the base first (
6e8e157, before any change): 1712 leaves on each side, no gap. Nothing added by #369, #419 or #421 is missing, so no translation is added and there is no CHANGELOG entry (no behaviour change).Files
src/i18n/locales.keys.test.ts(new): the guard, 8 tests.REVIEW.md:t»: the sentence « Jusqu'à #453, aucune garde ne détecte une clé présente dans une seule langue : comparerfr.jsoneten.json. » is replaced by the guard, named, with its two blind spots: a value left in the other language, and a key the code calls that neither file declares (« Note du 2026-10-18 » of the issue).#453added to its sources), as the migration and release zones name their tests.python3 scripts/review-levels.py --check→ok: 37 patterns, identical to the Haut table of REVIEW.md.Decisions (details in the run's decisions log)
DocsContent.locales.test.ts), dotted paths with indices, one failing test per language, values not compared: the issue's text.feedback.toast.error.429is a real object key made of digits). Rejected: leaf sets only; listing containers as missing too (mutation 2 would listzz,zz.onlyandzz.only.fr); one combined assertion (the failing count would not tell the language). Accepted limit: an empty object or array holds no key, so one present in a single file is not reported (none exists).docs/architecture.mdandCLAUDE.mdleft alone (not in the issue's file list; the CLAUDE.md docs rule covers services, hooks, commands, pages and tables, not tests)._oneand_otheronly. French CLDR also hasmany(Intl.PluralRules("fr"): one, many, other), which English lacks: a French-only_manyform would fail the guard and would have to be written in both files.Proofs
Implementation committed (
8a250b9) before the first mutation. Each mutation applied alone: locale files rewritten byjson.dumps(..., ensure_ascii=False, indent=2), which reproduces both files byte for byte, so the mutation diff is the mutation only; restored bygit checkout -- <file>,git status --shortempty after each.Mandatory mutations (acceptance criteria), guard file alone and full suite:
locales.keys.test.ts)npm test)en.jsonimport.config.sourceNameHint+ "import.config.sourceNameHint"fr.jsonzz.only.fr({"zz": {"only": {"fr": "…"}}})+ "zz.only.fr"en.jsondocs.dashboard.features(array of 7) → one stringdocs.dashboard.features.0to.6; kinddocs.dashboard.features: array in fr.json, leaf in en.jsonDocsContent.locales.test.tsExtra mutations (guard file alone):
fr.jsondocs.dashboard.features→ object{"0": …, "6": …}(same indices)docs.dashboard.features: object in fr.json, array in en.json, nothing reported missingen.jsoncommon.zzOnlyEn+ "common.zzOnlyEn"en.jsoncommon.cancelchanged, keys untouchedlocales.keys.test.tskindGapsfilter →() => falselocales.keys.test.tslocales.keys.test.tsTests
25a1fde: 138 files / 2305 tests, green (main: the same minus this file, 137 / 2297).npm run build(tsc + vite) green.src/zz_tmp_probe.test.tswithexpect(1).toBe(2)→npx vitest run src/zz_tmp_probe.test.tsexit 1 (sonde rc=1), file deleted.For the reviewer
expected [ 'docs.dashboard.features.0', …(6) ] to deeply equal []), but the diff under it lists every key (mutation 3).🤖 Generated with Claude Code
Niveau : normal
Verdict : APPROVE
Auto-revue de l'auteur (worker
/autopilotdu 2026-10-18), pas une revue indépendante : l'issue reste enstatus:reviewet la PR porteautopilot:pending-human, en attente d'un/pr-review.Niveau (Step 0bis,
REVIEW.mddemain)src/i18n/locales.keys.test.ts(ajouté) : la ligne Haut « code livré :src/**» ne vaut que pour un appel réseau ajouté, un envoi changé, une URL qui porte une donnée de l'utilisateur ou un consentement contourné. Le fichier importe deux JSON en statique, rien de cela. Il n'est ni à la racine ni directement danssrc-tauri/. Niveau normal (tests).REVIEW.md: le diff touche « Ce qui compte par zone » (i18n et documentation) et « Pièges de revue », pas « Niveaux ». Niveau normal, jamais bas (liste « Restent au moins normaux »).python3 scripts/review-levels.py --check:ok: 37 patterns, identical to the Haut table of REVIEW.md.Ce qui a été vérifié
Intention : la garde compare les ensembles de clés des deux fichiers, indices de tableau compris. Elle liste à part les clés manquantes de chaque langue (un test par langue) et signale dans un troisième test un tableau contre un objet, ou une feuille contre un objet ou un tableau. Elle ne compare pas les valeurs. C'est le « Travail à faire » de #453.
Base : garde lancée sur
6e8e157avant tout changement, 1712 feuilles de chaque côté, aucun écart. D'où ni traduction ajoutée ni entrée de CHANGELOG (critère de l'issue).Mutations de l'issue, toutes rouges :
import.config.sourceNameHintretirée d'en.json) : 1 test sur 8, exit 1, la clé est nommée manquante dansen.json;zz.only.frajoutée au seulfr.json) : 1 sur 8, exit 1,zz.only.frest nommée ;docs.dashboard.featuresremplacé par une chaîne dansen.json) : 2 sur 8, exit 1.Sur la suite complète, M1 et M2 ne font tomber que la garde (1 sur 2305) : avant cette PR, elles passaient toute la suite.
Mutations en plus :
en.json: 1 sur 8 ;nodesOfn'enregistre plus les conteneurs) : 3 sur 8, exit 1.Suite : sonde rc=1,
npm run buildvert, vitest vert sur25a1fde(138 fichiers, 2305 tests). Rust non touché.Zones de
REVIEW.mdtouchées :architecture.md;common.cancel,dashboard.help.tips,dashboard.help.tips.0), choisi hors des clés des mutations.CI :
check-frontendse déclenche (fichier soussrc/). Le run 571 était en attente au moment de cette revue ; il n'a pas été attendu ici.Fermeture : la timeline de #453 porte un
pull_refclosesvenant de #468. Le corps n'a aucun autre mot-clé de fermeture.Dépôt public : rien de personnel dans le diff (fixtures génériques).
Constats
Aucun bloquant.
Suggestions (non bloquantes)
src/i18n/locales.keys.test.ts:48: un objet ou un tableau vide présent dans un seul fichier n'est pas signalé, puisqu'un conteneur vide ne porte aucune clé. Il n'en existe aucun aujourd'hui ; la limite est écrite dans le corps de la PR.src/i18n/locales.keys.test.ts:11: les suffixes de pluriel font partie de la clé. Le français a une catégoriemanyque l'anglais n'a pas : une forme_manypropre au français ferait rougir la garde, et il faudrait l'écrire dans les deux fichiers (l'anglais ne la lirait jamais). Aujourd'hui, les deux fichiers n'ont que_oneet_other.src/i18n/locales.keys.test.ts:80: sur une longue liste, vitest tronque la ligne de résumé (…(6)), mais le diff affiché dessous nomme chaque clé (vu sur M3). Rien à faire.CI lue par l'orchestrateur du run
/autopilotdu 2026-10-18 :check-frontendvert sur la tête25a1fde(run 571).v24.22.0,vite buildréussi.src/i18n/locales.keys.test.tsexécuté (8 tests).export faileddu journal est la sortie attendue desrc/services/logService.test.ts, pas un échec.check-rustne s'applique pas à ce maillon (aucun chemin Rust touché).Niveau : normal
Verdict : APPROVE
Résumé
src/i18n/locales.keys.test.tscompare les arbres de clés defr.jsoneten.json(chemins pointés, indices de tableau et suffixes de pluriel compris, sorte de chaque nœud) et nomme, langue par langue, ce qui manque ;REVIEW.mdremplace la phrase « Jusqu'à #453… » par la garde et ses angles morts. Conforme au « Travail à faire » et aux critères de #453, rejoués ici sur une copie hors dépôt, avec des mutations du relecteur.Niveau (Step 0bis,
REVIEW.mddemain)src/i18n/locales.keys.test.ts(ajouté) : désigné par « code livré :src/**», condition non remplie (ni appel réseau, ni envoi, ni URL, ni consentement) ; ni à la racine, ni directement danssrc-tauri/. Normal.REVIEW.md: hunks aux lignes 141 (« i18n et documentation ») et 186 (« Pièges de revue ») ; la section « Niveaux » (lignes 7 à 74) est intacte. Normal, jamais bas.python3 scripts/review-levels.py --checksur la tête :ok: 37 patterns.Ce qui a été exécuté
Copie hors dépôt : archive Forgejo de la tête
25a1fde, identique à la base6e8e157à deux fichiers près (diff -rq),npm cipropre (Node 22 en local).expect(1).toBe(2)fait sortir vitest en rc=1.npx vitest run: 138 fichiers, 2305 tests, verts.npm run build(tsc + vite) vert.check-frontendrun 571 vert sur25a1fde, journal lu : Nodev24.22.0,✓ src/i18n/locales.keys.test.ts (8 tests), 138 fichiers et 2305 tests._oneet_other(10 de chaque par fichier) ;Intl.PluralRules("fr")donnemany, one, other(select(1e6)=many),endonneone, other. Le « 1412 feuilles » du corps de #453 est une coquille :mainau 2026-10-16 (cb7f847) en comptait déjà 1712..ou:, donc l'aplatissement en chemins pointés ne confond rien ; aucun conteneur vide, aucune clé en double, aucune feuille autre qu'une chaîne.Mutations du relecteur
Une à la fois, sur la copie, restaurée depuis un exemplaire vierge et comparée octet par octet après chacune. Garde seule (8 tests), sauf mention.
en.jsonimport.config.sourceNameHintretirée (critère 1)en.jsonREVIEW.md, donc surmainla même mutation passait les 2297 testsfr.jsonzz.only.frajoutée (critère 2)zz.only.frnommée comme manquante dansen.jsonfr.jsondocs.import.tips(tableau de 11) remplacé par une chaîne (critère 3, autre tableau et autre langue que l'auteur)docs.import.tips: leaf in fr.json, array in en.json, lisible dans le diff sous la ligne tronquée[ Array(1) ])fr.jsonimport.help.tipsretiréimport.help.tips.4en.jsonimport.sources.fileCount_onerenomméefileCount_manyen.jsondocs.import.subsections.2.steps(tableau dans un tableau d'objets)docs.import.subsections.2.steps.4en.jsonsourceNameHint: { text })import.config.sourceNameHint.textet la sorteen.jsonzzTop.a.bpropre à l'anglaisen.jsonmonths(objet) changé en tableau des mêmes valeursfr.jsoncommon.zzEmpty: {}propre au françaiskindGapsignore une feuille contre un conteneurmissingKeysliste aussi les conteneursimport en from "./locales/fr.json"fr.jsonà lui-mêmemissingKeys(FR, EN))T4 et T5 touchent le câblage du test lui-même, visible à la lecture d'un diff ; T1 ne change que le message. Aucun ne cache un écart réel des fichiers tant que le test reste tel qu'il est.
Constats
Aucun bloquant.
Suggestions (non bloquantes)
src/i18n/locales.keys.test.ts:70-77: le canari prouve que chaque fichier est lu en entier, pas que les deux imports désignent deux fichiers (T4). Une ligne, vérifiée sur la copie :expect(en, "en.json and fr.json are two modules").not.toBe(fr);en tête du canari. T4 tombe alors (1 sur 8), la garde reste verte sur la tête,tscaussi.src/i18n/locales.keys.test.ts:93-98: la règle « seules les feuilles sont listées », retenue dans le journal de décisions, n'est pas épinglée (T1). Une ligne dans la première fixture, vérifiée :expect(missingKeys(nodesOf({ zz: { only: "x" } }), nodesOf({}))).toEqual(["zz.only"]);fait tomber T1 (1 sur 8). Cosmétique.fr.jsonécrit{{count}}eten.json{{n}}passe et affiche la variable brute. Mesuré sur la tête : aucun écart de variables sur les 1712 clés, donc une telle garde serait verte d'emblée ; aujourd'hui, seuls des tests ciblés le vérifient pour leurs phrases (onboardingPath.locales.test.ts,csvAutoDetect.test.ts,bankSignatures.test.ts,importFormat.test.ts).Checklist
test:,docs:), titretest(i18n):.check-rustne s'applique pas.REVIEW.md: le fait devenu faux (« Jusqu'à #453, aucune garde… ») est corrigé dans la même PR, comme le demande « Règles de revue » ; les deux angles morts nommés sont exacts.pull_refcloses, venant de #468 ; #470 et #475 la citent sans la fermer. Corps et commit25a1fde:Resolves #453.Actions faites
/tmp/pr468-reviewsupprimée en fin de revue.status:reviewremplacé parstatus:approvedaprès ce commentaire.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.