[FIX] i18n : retirer trois clés déclarées deux fois, et l'interdire
Trois clés étaient déclarées deux fois dans un littéral de trois mille entrées. Rien n'avertit et rien ne lève : la seconde gagne, et le prix s'est déjà payé en étiquette de menu, la traduction lue n'étant pas celle qu'on venait d'écrire au premier endroit. Les trois portaient la même traduction des deux côtés, donc aucun écran ne change ; c'est le piège qui part. Le contrôle passe de « pas plus de trois » à « aucune », et il vit désormais avec les tests d'internationalisation plutôt qu'avec ceux d'un menu. Il lit l'ARBRE : une fois le dictionnaire construit, le doublon a déjà disparu et il n'y a plus rien à demander. --- EN --- Three keys were declared twice in a literal of three thousand entries. Nothing warns and nothing raises: the second wins, and the price was already paid on a menu label, the translation read not being the one just written at the first place. All three carried the same translation on both sides, so no screen changes; what goes is the trap. The check moves from "no more than three" to "none at all", and it now lives with the internationalisation tests rather than with a menu's. It reads the TREE: once the dictionary is built the duplicate is already gone, and there is nothing left to ask. Assisted-by: Claude Opus 5
This commit is contained in:
parent
314ab134d7
commit
d335293d85
3 changed files with 63 additions and 38 deletions
|
|
@ -1255,6 +1255,9 @@ TRANSLATIONS = {
|
||||||
"fr": "Ajouter un marketplace",
|
"fr": "Ajouter un marketplace",
|
||||||
"en": "Add a marketplace",
|
"en": "Add a marketplace",
|
||||||
},
|
},
|
||||||
|
# Sert la section « Maintenance » des deux menus qui en portent une :
|
||||||
|
# la traduction est la même, et une clé répétée écrase la précédente en
|
||||||
|
# silence.
|
||||||
"Maintenance": {
|
"Maintenance": {
|
||||||
"fr": "Maintenance",
|
"fr": "Maintenance",
|
||||||
"en": "Maintenance",
|
"en": "Maintenance",
|
||||||
|
|
@ -1638,10 +1641,6 @@ TRANSLATIONS = {
|
||||||
"fr": "Interface",
|
"fr": "Interface",
|
||||||
"en": "Interface",
|
"en": "Interface",
|
||||||
},
|
},
|
||||||
"Maintenance": {
|
|
||||||
"fr": "Maintenance",
|
|
||||||
"en": "Maintenance",
|
|
||||||
},
|
|
||||||
"Language / Langue": {
|
"Language / Langue": {
|
||||||
"fr": "🌐 Langue / Language",
|
"fr": "🌐 Langue / Language",
|
||||||
"en": "🌐 Language / Langue",
|
"en": "🌐 Language / Langue",
|
||||||
|
|
@ -5779,10 +5778,6 @@ TRANSLATIONS = {
|
||||||
"fr": "Fuseau inconnu, on garde",
|
"fr": "Fuseau inconnu, on garde",
|
||||||
"en": "Unknown timezone, keeping",
|
"en": "Unknown timezone, keeping",
|
||||||
},
|
},
|
||||||
"none": {
|
|
||||||
"fr": "aucune",
|
|
||||||
"en": "none",
|
|
||||||
},
|
|
||||||
"No SSH key found. Set a password instead? (Y/n): ": {
|
"No SSH key found. Set a password instead? (Y/n): ": {
|
||||||
"fr": "Aucune clé SSH trouvée. Définir un mot de passe ? (O/n) : ",
|
"fr": "Aucune clé SSH trouvée. Définir un mot de passe ? (O/n) : ",
|
||||||
"en": "No SSH key found. Set a password instead? (Y/n): ",
|
"en": "No SSH key found. Set a password instead? (Y/n): ",
|
||||||
|
|
@ -7596,10 +7591,6 @@ TRANSLATIONS = {
|
||||||
"fr": "secondes.",
|
"fr": "secondes.",
|
||||||
"en": "seconds.",
|
"en": "seconds.",
|
||||||
},
|
},
|
||||||
"pass": {
|
|
||||||
"fr": "passe",
|
|
||||||
"en": "pass",
|
|
||||||
},
|
|
||||||
"The cleanup was still running after": {
|
"The cleanup was still running after": {
|
||||||
"fr": "Le nettoyage tournait encore après",
|
"fr": "Le nettoyage tournait encore après",
|
||||||
"en": "The cleanup was still running after",
|
"en": "The cleanup was still running after",
|
||||||
|
|
|
||||||
|
|
@ -161,32 +161,6 @@ class ClesDeTraduction(unittest.TestCase):
|
||||||
self.assertIn(nom, COMMANDS)
|
self.assertIn(nom, COMMANDS)
|
||||||
self.assertIn(COMMANDS[nom], TRANSLATIONS)
|
self.assertIn(COMMANDS[nom], TRANSLATIONS)
|
||||||
|
|
||||||
def test_les_doublons_de_translations_restent_les_trois_connus(self):
|
|
||||||
"""Une clé répétée écrase la précédente en silence, et l'écrasement
|
|
||||||
s'est déjà payé d'une mauvaise étiquette de menu principal. Trois
|
|
||||||
doublons préexistent ; ce test refuse le quatrième sans exiger de
|
|
||||||
réparer les trois, qui sont hors du sujet de ce câblage."""
|
|
||||||
chemin = os.path.join(RACINE, "script", "todo", "todo_i18n.py")
|
|
||||||
with open(chemin) as fichier:
|
|
||||||
arbre = ast.parse(fichier.read())
|
|
||||||
litteral = None
|
|
||||||
for noeud in ast.walk(arbre):
|
|
||||||
if isinstance(noeud, ast.Assign) and any(
|
|
||||||
isinstance(c, ast.Name) and c.id == "TRANSLATIONS"
|
|
||||||
for c in noeud.targets
|
|
||||||
):
|
|
||||||
litteral = noeud.value
|
|
||||||
self.assertIsInstance(litteral, ast.Dict)
|
|
||||||
noms = [
|
|
||||||
c.value
|
|
||||||
for c in litteral.keys
|
|
||||||
if isinstance(c, ast.Constant) and isinstance(c.value, str)
|
|
||||||
]
|
|
||||||
self.assertTrue(noms)
|
|
||||||
compte = collections.Counter(noms)
|
|
||||||
doublons = sorted(k for k, n in compte.items() if n > 1)
|
|
||||||
self.assertEqual(doublons, ["Maintenance", "none", "pass"])
|
|
||||||
|
|
||||||
|
|
||||||
class JournalDuTransport(unittest.TestCase):
|
class JournalDuTransport(unittest.TestCase):
|
||||||
"""La conversation ne doit pas être coupée par des lignes de journal.
|
"""La conversation ne doit pas être coupée par des lignes de journal.
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import collections
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
@ -232,5 +234,63 @@ class TestLangIsConfigured(unittest.TestCase):
|
||||||
self.assertFalse(result)
|
self.assertFalse(result)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAucuneCleRepetee(unittest.TestCase):
|
||||||
|
"""Une clé répétée écrase la précédente EN SILENCE.
|
||||||
|
|
||||||
|
Le dictionnaire est un littéral Python de plusieurs milliers d'entrées :
|
||||||
|
rien n'avertit, rien ne lève, et la deuxième définition gagne. Le prix
|
||||||
|
s'est déjà payé une fois en étiquette de menu principal — la traduction
|
||||||
|
lue n'était pas celle qu'on venait d'écrire, et le fichier montrait la
|
||||||
|
bonne à qui la cherchait.
|
||||||
|
|
||||||
|
Le contrôle lit l'ARBRE et non le dictionnaire construit : une fois
|
||||||
|
construit, le doublon a déjà disparu, et il n'y a plus rien à voir. C'est
|
||||||
|
la seule façon de poser la question.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _cles(self):
|
||||||
|
chemin = os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||||
|
"script",
|
||||||
|
"todo",
|
||||||
|
"todo_i18n.py",
|
||||||
|
)
|
||||||
|
with open(chemin, encoding="utf-8") as fichier:
|
||||||
|
arbre = ast.parse(fichier.read())
|
||||||
|
litteral = None
|
||||||
|
for noeud in ast.walk(arbre):
|
||||||
|
if isinstance(noeud, ast.Assign) and any(
|
||||||
|
isinstance(c, ast.Name) and c.id == "TRANSLATIONS"
|
||||||
|
for c in noeud.targets
|
||||||
|
):
|
||||||
|
litteral = noeud.value
|
||||||
|
self.assertIsInstance(litteral, ast.Dict)
|
||||||
|
return [
|
||||||
|
c.value
|
||||||
|
for c in litteral.keys
|
||||||
|
if isinstance(c, ast.Constant) and isinstance(c.value, str)
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_the_literal_was_actually_read(self):
|
||||||
|
"""Sinon un dictionnaire vide passerait le test suivant."""
|
||||||
|
self.assertGreater(len(self._cles()), 2000)
|
||||||
|
|
||||||
|
def test_no_key_is_declared_twice(self):
|
||||||
|
compte = collections.Counter(self._cles())
|
||||||
|
doublons = sorted(k for k, n in compte.items() if n > 1)
|
||||||
|
self.assertEqual(
|
||||||
|
doublons,
|
||||||
|
[],
|
||||||
|
"clés déclarées deux fois : la seconde écrase la première",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_every_key_survives_the_build(self):
|
||||||
|
"""Le compte du littéral et celui du dictionnaire s'accordent.
|
||||||
|
|
||||||
|
C'est la même vérité dite autrement, et elle tombe d'elle-même le
|
||||||
|
jour où une clé se répète."""
|
||||||
|
self.assertEqual(len(self._cles()), len(todo_i18n.TRANSLATIONS))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue