test(i18n): fail npm test on a key present in one language only (#453) #468

Open
maximus wants to merge 2 commits from issue-453-i18n-keys-guard into main
Owner

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 test now fails when src/i18n/locales/fr.json and src/i18n/locales/en.json do 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 from en.json) or in English to a French user (key missing from fr.json, English being the fallback of src/i18n/config.ts).

  • A key is the dotted path of a leaf, array indices included (docs.dashboard.features.6), plural suffixes too (import.sources.fileCount_one).
  • One test per language lists, sorted, the keys the other file lacks: « keys of fr.json missing from en.json » and « keys of en.json missing from fr.json ».
  • A third test lists the paths both files hold with another kind: an array against an object (even with the same indices), a leaf against an object or an array. Such a path is reported there only, not also as missing.
  • Values are never compared (extra mutation 6: a changed value stays green).
  • A canary checks that both files were read whole, array items included (common.cancel, dashboard.help.tips, dashboard.help.tips.0): an unread or empty file would make the comparison pass on nothing.
  • Four tests on small fixtures pin the rules (array indices, array against object, leaf against object or array, values ignored).

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:
    • « Pièges de revue », entry « Faux t »: the sentence « Jusqu'à #453, aucune garde ne détecte une clé présente dans une seule langue : comparer fr.json et en.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).
    • « i18n et documentation »: the first rule names the guard (one parenthesis, #453 added to its sources), as the migration and release zones name their tests.
    • Section « Niveaux » untouched: 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)

  • [HIGH] File name, static imports (as DocsContent.locales.test.ts), dotted paths with indices, one failing test per language, values not compared: the issue's text.
  • [HIGH] Guard green on the base: no translation to add, no CHANGELOG entry.
  • [HIGH] REVIEW.md pitfall sentence replaced and the test named (« Note du 2026-10-18 »).
  • [HIGH] Level normal (see the self-review comment).
  • [MEDIUM] Every node is collected with its kind (object, array, leaf). Comparing leaf sets alone cannot tell an array from an object with the same numeric keys (feedback.toast.error.429 is a real object key made of digits). Rejected: leaf sets only; listing containers as missing too (mutation 2 would list zz, zz.only and zz.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).
  • [MEDIUM] Canary and fixture tests added beyond the issue's list: REVIEW.md « CI et gardes » asks a guard to read a canary, and the three mandatory mutations never exercise the array-against-object rule. Canary keys chosen apart from the mutation keys, so each mandatory mutation's count reflects the parity tests alone.
  • [MEDIUM] REVIEW.md zone mention kept to one parenthesis; docs/architecture.md and CLAUDE.md left alone (not in the issue's file list; the CLAUDE.md docs rule covers services, hooks, commands, pages and tables, not tests).
  • [MEDIUM] Plural suffixes are part of the key, no exception (the guard stays strict). Both files use _one and _other only. French CLDR also has many (Intl.PluralRules("fr"): one, many, other), which English lacks: a French-only _many form 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 by json.dumps(..., ensure_ascii=False, indent=2), which reproduces both files byte for byte, so the mutation diff is the mutation only; restored by git checkout -- <file>, git status --short empty after each.

Mandatory mutations (acceptance criteria), guard file alone and full suite:

# File Mutation Guard (locales.keys.test.ts) Exit Full suite (npm test)
1 en.json remove import.config.sourceNameHint 1 failed / 8: « en.json declares every key of fr.json », diff + "import.config.sourceNameHint" 1 1 failed / 2305, the guard only
2 fr.json add zz.only.fr ({"zz": {"only": {"fr": "…"}}}) 1 failed / 8: « en.json declares every key of fr.json », diff + "zz.only.fr" 1 1 failed / 2305, the guard only
3 en.json docs.dashboard.features (array of 7) → one string 2 failed / 8: missing from en.json docs.dashboard.features.0 to .6; kind docs.dashboard.features: array in fr.json, leaf in en.json 1 4 failed / 2305: the guard's 2, plus 2 of DocsContent.locales.test.ts

Extra mutations (guard file alone):

# File Mutation Failing tests Exit
4 fr.json docs.dashboard.features → object {"0": …, "6": …} (same indices) 1 / 8: kind docs.dashboard.features: object in fr.json, array in en.json, nothing reported missing 1
5 en.json add common.zzOnlyEn 1 / 8: « fr.json declares every key of en.json », diff + "common.zzOnlyEn" 1
6 en.json value of common.cancel changed, keys untouched 0 / 8 (values not compared) 0
h1 locales.keys.test.ts kindGaps filter → () => false 2 / 8 (array against object, leaf against object or array) 1
h2 locales.keys.test.ts an array taken for a leaf 4 / 8 (canary and three fixture tests) 1
h3 locales.keys.test.ts a leaf held with another kind also reported missing 1 / 8 (leaf against object or array) 1

Tests

  • vitest on the tip 25a1fde: 138 files / 2305 tests, green (main: the same minus this file, 137 / 2297).
  • npm run build (tsc + vite) green.
  • Probe: src/zz_tmp_probe.test.ts with expect(1).toBe(2) → npx vitest run src/zz_tmp_probe.test.ts exit 1 (sonde rc=1), file deleted.
  • Rust: not touched, not run.

For the reviewer

  • On a long list, vitest truncates the summary line (expected [ 'docs.dashboard.features.0', …(6) ] to deeply equal []), but the diff under it lists every key (mutation 3).
  • No manual check: a test and review rules only, nothing visible in the app.

🤖 Generated with Claude Code

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 test` now fails when `src/i18n/locales/fr.json` and `src/i18n/locales/en.json` do 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 from `en.json`) or in English to a French user (key missing from `fr.json`, English being the fallback of `src/i18n/config.ts`). - A key is the dotted path of a leaf, array indices included (`docs.dashboard.features.6`), plural suffixes too (`import.sources.fileCount_one`). - One test per language lists, sorted, the keys the other file lacks: « keys of fr.json missing from en.json » and « keys of en.json missing from fr.json ». - A third test lists the paths both files hold with another kind: an array against an object (even with the same indices), a leaf against an object or an array. Such a path is reported there only, not also as missing. - Values are never compared (extra mutation 6: a changed value stays green). - A canary checks that both files were read whole, array items included (`common.cancel`, `dashboard.help.tips`, `dashboard.help.tips.0`): an unread or empty file would make the comparison pass on nothing. - Four tests on small fixtures pin the rules (array indices, array against object, leaf against object or array, values ignored). **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`: - « Pièges de revue », entry « Faux `t` »: the sentence « Jusqu'à #453, aucune garde ne détecte une clé présente dans une seule langue : comparer `fr.json` et `en.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). - « i18n et documentation »: the first rule names the guard (one parenthesis, `#453` added to its sources), as the migration and release zones name their tests. - Section « Niveaux » untouched: `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) - [HIGH] File name, static imports (as `DocsContent.locales.test.ts`), dotted paths with indices, one failing test per language, values not compared: the issue's text. - [HIGH] Guard green on the base: no translation to add, no CHANGELOG entry. - [HIGH] REVIEW.md pitfall sentence replaced and the test named (« Note du 2026-10-18 »). - [HIGH] Level normal (see the self-review comment). - [MEDIUM] Every node is collected with its kind (object, array, leaf). Comparing leaf sets alone cannot tell an array from an object with the same numeric keys (`feedback.toast.error.429` is a real object key made of digits). Rejected: leaf sets only; listing containers as missing too (mutation 2 would list `zz`, `zz.only` and `zz.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). - [MEDIUM] Canary and fixture tests added beyond the issue's list: REVIEW.md « CI et gardes » asks a guard to read a canary, and the three mandatory mutations never exercise the array-against-object rule. Canary keys chosen apart from the mutation keys, so each mandatory mutation's count reflects the parity tests alone. - [MEDIUM] REVIEW.md zone mention kept to one parenthesis; `docs/architecture.md` and `CLAUDE.md` left alone (not in the issue's file list; the CLAUDE.md docs rule covers services, hooks, commands, pages and tables, not tests). - [MEDIUM] Plural suffixes are part of the key, no exception (the guard stays strict). Both files use `_one` and `_other` only. French CLDR also has `many` (`Intl.PluralRules("fr")`: one, many, other), which English lacks: a French-only `_many` form 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 by `json.dumps(..., ensure_ascii=False, indent=2)`, which reproduces both files byte for byte, so the mutation diff is the mutation only; restored by `git checkout -- <file>`, `git status --short` empty after each. Mandatory mutations (acceptance criteria), guard file alone and full suite: | # | File | Mutation | Guard (`locales.keys.test.ts`) | Exit | Full suite (`npm test`) | |---|---|---|---|---|---| | 1 | `en.json` | remove `import.config.sourceNameHint` | 1 failed / 8: « en.json declares every key of fr.json », diff `+ "import.config.sourceNameHint"` | 1 | 1 failed / 2305, the guard only | | 2 | `fr.json` | add `zz.only.fr` (`{"zz": {"only": {"fr": "…"}}}`) | 1 failed / 8: « en.json declares every key of fr.json », diff `+ "zz.only.fr"` | 1 | 1 failed / 2305, the guard only | | 3 | `en.json` | `docs.dashboard.features` (array of 7) → one string | 2 failed / 8: missing from en.json `docs.dashboard.features.0` to `.6`; kind `docs.dashboard.features: array in fr.json, leaf in en.json` | 1 | 4 failed / 2305: the guard's 2, plus 2 of `DocsContent.locales.test.ts` | Extra mutations (guard file alone): | # | File | Mutation | Failing tests | Exit | |---|---|---|---|---| | 4 | `fr.json` | `docs.dashboard.features` → object `{"0": …, "6": …}` (same indices) | 1 / 8: kind `docs.dashboard.features: object in fr.json, array in en.json`, nothing reported missing | 1 | | 5 | `en.json` | add `common.zzOnlyEn` | 1 / 8: « fr.json declares every key of en.json », diff `+ "common.zzOnlyEn"` | 1 | | 6 | `en.json` | value of `common.cancel` changed, keys untouched | 0 / 8 (values not compared) | 0 | | h1 | `locales.keys.test.ts` | `kindGaps` filter → `() => false` | 2 / 8 (array against object, leaf against object or array) | 1 | | h2 | `locales.keys.test.ts` | an array taken for a leaf | 4 / 8 (canary and three fixture tests) | 1 | | h3 | `locales.keys.test.ts` | a leaf held with another kind also reported missing | 1 / 8 (leaf against object or array) | 1 | ## Tests - vitest on the tip `25a1fde`: 138 files / 2305 tests, green (`main`: the same minus this file, 137 / 2297). - `npm run build` (tsc + vite) green. - Probe: `src/zz_tmp_probe.test.ts` with `expect(1).toBe(2)` → `npx vitest run src/zz_tmp_probe.test.ts` exit 1 (sonde rc=1), file deleted. - Rust: not touched, not run. ## For the reviewer - On a long list, vitest truncates the summary line (`expected [ 'docs.dashboard.features.0', …(6) ] to deeply equal []`), but the diff under it lists every key (mutation 3). - No manual check: a test and review rules only, nothing visible in the app. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
maximus added 2 commits 2026-10-18 21:18:52 +00:00
No test compared the key trees of the two locale files: a key added to one
language only passed the build and the suite, then showed raw to an English
user (missing from en.json) or in English to a French user (missing from
fr.json, English being the fallback).

src/i18n/locales.keys.test.ts imports both files statically, flattens every
node to a dotted path, array indices included (docs.dashboard.features.6),
and fails with the keys missing from each language in a test of its own,
plus a third test for a path held with another kind in each file (an array
against an object, a leaf against an object or an array). Values are never
compared. A canary checks both files were read whole, array items included,
and four tests on small fixtures pin the rules of the comparison.

Run on the base (6e8e157): 1712 leaves on each side, no gap, so no
translation to add.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: name the i18n key guard in REVIEW.md (#453)
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m9s
25a1fdea84
The "Faux t" review pitfall said that until #453 no guard caught a key
present in one language only. It now names src/i18n/locales.keys.test.ts
and what the guard does not see: a value left in the other language, and a
key the code calls that neither file declares. The first rule of the "i18n
et documentation" zone names the guard too, as the migration and release
zones name theirs. The "Niveaux" section is untouched
(scripts/review-levels.py --check still passes).

Resolves #453

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
maximus added the
autopilot:pending-human
label 2026-10-18 21:19:05 +00:00
Author
Owner

Niveau : normal
Verdict : APPROVE

Auto-revue de l'auteur (worker /autopilot du 2026-10-18), pas une revue indépendante : l'issue reste en status:review et la PR porte autopilot:pending-human, en attente d'un /pr-review.

Niveau (Step 0bis, REVIEW.md de main)

  • 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 dans src-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 6e8e157 avant 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 :

    • M1 (import.config.sourceNameHint retirée d'en.json) : 1 test sur 8, exit 1, la clé est nommée manquante dans en.json ;
    • M2 (zz.only.fr ajoutée au seul fr.json) : 1 sur 8, exit 1, zz.only.fr est nommée ;
    • M3 (docs.dashboard.features remplacé par une chaîne dans en.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 :

    • un tableau contre un objet aux mêmes indices : 1 sur 8 ;
    • une clé seulement dans en.json : 1 sur 8 ;
    • une valeur seule changée : 0 sur 8, exit 0 (voulu) ;
    • trois mutations des fonctions de comparaison : 2, 4 et 1 sur 8 ;
    • une mutation de relecture (nodesOf n'enregistre plus les conteneurs) : 3 sur 8, exit 1.
  • Suite : sonde rc=1, npm run build vert, vitest vert sur 25a1fde (138 fichiers, 2305 tests). Rust non touché.

  • Zones de REVIEW.md touchées :

    • « i18n et documentation » : rien de visible, donc ni CHANGELOG, ni guide, ni architecture.md ;
    • « Règles de revue » : le fait devenu faux (la phrase « Jusqu'à #453… ») est corrigé dans la même PR ;
    • « CI et gardes » : la garde lit un canari (common.cancel, dashboard.help.tips, dashboard.help.tips.0), choisi hors des clés des mutations.
  • CI : check-frontend se déclenche (fichier sous src/). 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_ref closes venant 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)

  1. 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.
  2. src/i18n/locales.keys.test.ts:11 : les suffixes de pluriel font partie de la clé. Le français a une catégorie many que l'anglais n'a pas : une forme _many propre 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 _one et _other.
  3. 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.
Niveau : normal Verdict : APPROVE Auto-revue de l'auteur (worker `/autopilot` du 2026-10-18), pas une revue indépendante : l'issue reste en `status:review` et la PR porte `autopilot:pending-human`, en attente d'un `/pr-review`. ## Niveau (Step 0bis, `REVIEW.md` de `main`) - `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 dans `src-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 `6e8e157` avant 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 : - M1 (`import.config.sourceNameHint` retirée d'`en.json`) : 1 test sur 8, exit 1, la clé est nommée manquante dans `en.json` ; - M2 (`zz.only.fr` ajoutée au seul `fr.json`) : 1 sur 8, exit 1, `zz.only.fr` est nommée ; - M3 (`docs.dashboard.features` remplacé par une chaîne dans `en.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** : - un tableau contre un objet aux mêmes indices : 1 sur 8 ; - une clé seulement dans `en.json` : 1 sur 8 ; - une valeur seule changée : 0 sur 8, exit 0 (voulu) ; - trois mutations des fonctions de comparaison : 2, 4 et 1 sur 8 ; - une mutation de relecture (`nodesOf` n'enregistre plus les conteneurs) : 3 sur 8, exit 1. - **Suite** : sonde rc=1, `npm run build` vert, vitest vert sur `25a1fde` (138 fichiers, 2305 tests). Rust non touché. - **Zones de `REVIEW.md` touchées** : - « i18n et documentation » : rien de visible, donc ni CHANGELOG, ni guide, ni `architecture.md` ; - « Règles de revue » : le fait devenu faux (la phrase « Jusqu'à #453… ») est corrigé dans la même PR ; - « CI et gardes » : la garde lit un canari (`common.cancel`, `dashboard.help.tips`, `dashboard.help.tips.0`), choisi hors des clés des mutations. - **CI** : `check-frontend` se déclenche (fichier sous `src/`). 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_ref` `closes` venant 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) 1. `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. 2. `src/i18n/locales.keys.test.ts:11` : les suffixes de pluriel font partie de la clé. Le français a une catégorie `many` que l'anglais n'a pas : une forme `_many` propre 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 `_one` et `_other`. 3. `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.
Author
Owner

CI lue par l'orchestrateur du run /autopilot du 2026-10-18 : check-frontend vert sur la tête 25a1fde (run 571).

  • Node v24.22.0, vite build réussi.
  • vitest : 138 fichiers, 2305 tests, tous verts ; src/i18n/locales.keys.test.ts exécuté (8 tests).
  • La ligne export failed du journal est la sortie attendue de src/services/logService.test.ts, pas un échec.
  • check-rust ne s'applique pas à ce maillon (aucun chemin Rust touché).
CI lue par l'orchestrateur du run `/autopilot` du 2026-10-18 : `check-frontend` **vert** sur la tête `25a1fde` (run 571). - Node `v24.22.0`, `vite build` réussi. - vitest : 138 fichiers, 2305 tests, tous verts ; `src/i18n/locales.keys.test.ts` exécuté (8 tests). - La ligne `export failed` du journal est la sortie attendue de `src/services/logService.test.ts`, pas un échec. - `check-rust` ne s'applique pas à ce maillon (aucun chemin Rust touché).
Author
Owner

Niveau : normal

Verdict : APPROVE

Résumé

src/i18n/locales.keys.test.ts compare les arbres de clés de fr.json et en.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.md remplace 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.md de main)

  • 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 dans src-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 --check sur 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 base 6e8e157 à deux fichiers près (diff -rq), npm ci propre (Node 22 en local).

  • Sonde : un test expect(1).toBe(2) fait sortir vitest en rc=1.
  • npx vitest run : 138 fichiers, 2305 tests, verts. npm run build (tsc + vite) vert.
  • CI : check-frontend run 571 vert sur 25a1fde, journal lu : Node v24.22.0, ✓ src/i18n/locales.keys.test.ts (8 tests), 138 fichiers et 2305 tests.
  • Faits du corps : 1712 feuilles de chaque côté, aucun écart ; seuls _one et _other (10 de chaque par fichier) ; Intl.PluralRules("fr") donne many, one, other (select(1e6) = many), en donne one, other. Le « 1412 feuilles » du corps de #453 est une coquille : main au 2026-10-16 (cb7f847) en comptait déjà 1712.
  • Contenu des deux fichiers : aucune clé qui contient . 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.

# Fichier Mutation Tests qui tombent rc
D1 en.json import.config.sourceNameHint retirée (critère 1) 1 : « en.json declares every key of fr.json », clé nommée 1
D1 en.json idem, suite complète 1 sur 2305, la garde seule ; aucun autre test ne lit REVIEW.md, donc sur main la même mutation passait les 2297 tests 1
D2 fr.json zz.only.fr ajoutée (critère 2) 1, zz.only.fr nommée comme manquante dans en.json 1
D3 fr.json docs.import.tips (tableau de 11) remplacé par une chaîne (critère 3, autre tableau et autre langue que l'auteur) 2 : les 11 clés manquantes en français, et la sorte (docs.import.tips: leaf in fr.json, array in en.json, lisible dans le diff sous la ligne tronquée [ Array(1) ]) 1
D4 fr.json dernier élément de import.help.tips retiré 1, import.help.tips.4 1
D5 en.json import.sources.fileCount_one renommée fileCount_many 2, une par langue 1
D6 en.json une étape retirée de docs.import.subsections.2.steps (tableau dans un tableau d'objets) 1, docs.import.subsections.2.steps.4 1
D7 en.json feuille changée en objet (sourceNameHint: { text }) 2 : import.config.sourceNameHint.text et la sorte 1
D8 en.json racine zzTop.a.b propre à l'anglais 1 1
D10 en.json months (objet) changé en tableau des mêmes valeurs 3 1
D9 fr.json common.zzEmpty: {} propre au français 0 : limite écrite dans le corps, aucun conteneur vide aujourd'hui 0
T2 garde un tableau classé objet 3 (canari et deux fixtures) 1
T3 garde aucune descente dans les tableaux 3 1
T6 garde kindGaps ignore une feuille contre un conteneur 1 (fixture) 1
T1 garde missingKeys liste aussi les conteneurs 0 : survit, seul le message change (la garde rougit toujours sur un écart) 0
T4 garde import en from "./locales/fr.json" 0 : survit, et avec D1 en plus, toujours 0 : la garde compare fr.json à lui-même 0
T5 garde second test câblé dans le sens du premier (missingKeys(FR, EN)) 0 ; avec D8 en plus, toujours 0 0

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)

  1. 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, tsc aussi.
  2. 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.
  3. Hors périmètre de #453, suivi possible : la garde ne regarde pas les variables d'interpolation. Une clé où fr.json écrit {{count}} et en.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

  • Sécurité : aucun secret, aucune donnée de tiers (fixtures génériques), aucun appel réseau.
  • Commits conventionnels (test:, docs:), titre test(i18n):.
  • Pas d'entrée de CHANGELOG, à raison : rien de visible (critère de #453).
  • Aucune migration ; Rust non touché, check-rust ne 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.
  • Fermeture : la timeline de #453 porte un seul pull_ref closes, venant de #468 ; #470 et #475 la citent sans la fermer. Corps et commit 25a1fde : Resolves #453.
  • Pile : #469 à #475 sont empilées sur cette branche, chaque maillon suivant lance donc la garde dans sa propre CI.

Actions faites

  • Copie hors dépôt /tmp/pr468-review supprimée en fin de revue.
  • Label de #453 : status:review remplacé par status:approved après ce commentaire.
Niveau : normal **Verdict : APPROVE** ## Résumé `src/i18n/locales.keys.test.ts` compare les arbres de clés de `fr.json` et `en.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.md` remplace 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.md` de `main`) - `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 dans `src-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 --check` sur 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 base `6e8e157` à deux fichiers près (`diff -rq`), `npm ci` propre (Node 22 en local). - Sonde : un test `expect(1).toBe(2)` fait sortir vitest en rc=1. - `npx vitest run` : 138 fichiers, 2305 tests, verts. `npm run build` (tsc + vite) vert. - CI : `check-frontend` run 571 vert sur `25a1fde`, journal lu : Node `v24.22.0`, `✓ src/i18n/locales.keys.test.ts (8 tests)`, 138 fichiers et 2305 tests. - Faits du corps : 1712 feuilles de chaque côté, aucun écart ; seuls `_one` et `_other` (10 de chaque par fichier) ; `Intl.PluralRules("fr")` donne `many, one, other` (`select(1e6)` = `many`), `en` donne `one, other`. Le « 1412 feuilles » du corps de #453 est une coquille : `main` au 2026-10-16 (`cb7f847`) en comptait déjà 1712. - Contenu des deux fichiers : aucune clé qui contient `.` 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. | # | Fichier | Mutation | Tests qui tombent | rc | |---|---|---|---|---| | D1 | `en.json` | `import.config.sourceNameHint` retirée (critère 1) | 1 : « en.json declares every key of fr.json », clé nommée | 1 | | D1 | `en.json` | idem, suite complète | 1 sur 2305, la garde seule ; aucun autre test ne lit `REVIEW.md`, donc sur `main` la même mutation passait les 2297 tests | 1 | | D2 | `fr.json` | `zz.only.fr` ajoutée (critère 2) | 1, `zz.only.fr` nommée comme manquante dans `en.json` | 1 | | D3 | `fr.json` | `docs.import.tips` (tableau de 11) remplacé par une chaîne (critère 3, autre tableau et autre langue que l'auteur) | 2 : les 11 clés manquantes en français, et la sorte (`docs.import.tips: leaf in fr.json, array in en.json`, lisible dans le diff sous la ligne tronquée `[ Array(1) ]`) | 1 | | D4 | `fr.json` | dernier élément de `import.help.tips` retiré | 1, `import.help.tips.4` | 1 | | D5 | `en.json` | `import.sources.fileCount_one` renommée `fileCount_many` | 2, une par langue | 1 | | D6 | `en.json` | une étape retirée de `docs.import.subsections.2.steps` (tableau dans un tableau d'objets) | 1, `docs.import.subsections.2.steps.4` | 1 | | D7 | `en.json` | feuille changée en objet (`sourceNameHint: { text }`) | 2 : `import.config.sourceNameHint.text` et la sorte | 1 | | D8 | `en.json` | racine `zzTop.a.b` propre à l'anglais | 1 | 1 | | D10 | `en.json` | `months` (objet) changé en tableau des mêmes valeurs | 3 | 1 | | D9 | `fr.json` | `common.zzEmpty: {}` propre au français | 0 : limite écrite dans le corps, aucun conteneur vide aujourd'hui | 0 | | T2 | garde | un tableau classé objet | 3 (canari et deux fixtures) | 1 | | T3 | garde | aucune descente dans les tableaux | 3 | 1 | | T6 | garde | `kindGaps` ignore une feuille contre un conteneur | 1 (fixture) | 1 | | T1 | garde | `missingKeys` liste aussi les conteneurs | **0** : survit, seul le message change (la garde rougit toujours sur un écart) | 0 | | T4 | garde | `import en from "./locales/fr.json"` | **0** : survit, et avec D1 en plus, toujours 0 : la garde compare `fr.json` à lui-même | 0 | | T5 | garde | second test câblé dans le sens du premier (`missingKeys(FR, EN)`) | **0** ; avec D8 en plus, toujours 0 | 0 | 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) 1. `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, `tsc` aussi. 2. `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. 3. Hors périmètre de #453, suivi possible : la garde ne regarde pas les variables d'interpolation. Une clé où `fr.json` écrit `{{count}}` et `en.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 - Sécurité : aucun secret, aucune donnée de tiers (fixtures génériques), aucun appel réseau. - Commits conventionnels (`test:`, `docs:`), titre `test(i18n):`. - Pas d'entrée de CHANGELOG, à raison : rien de visible (critère de #453). - Aucune migration ; Rust non touché, `check-rust` ne 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. - Fermeture : la timeline de #453 porte un seul `pull_ref` `closes`, venant de #468 ; #470 et #475 la citent sans la fermer. Corps et commit `25a1fde` : `Resolves #453`. - Pile : #469 à #475 sont empilées sur cette branche, chaque maillon suivant lance donc la garde dans sa propre CI. ## Actions faites - Copie hors dépôt `/tmp/pr468-review` supprimée en fin de revue. - Label de #453 : `status:review` remplacé par `status:approved` après ce commentaire.
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m9s
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin issue-453-i18n-keys-guard:issue-453-i18n-keys-guard
git checkout issue-453-i18n-keys-guard
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/Simpl-Resultat#468
No description provided.