diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index ccc1d47..406d2fb 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -1255,6 +1255,9 @@ TRANSLATIONS = { "fr": "Ajouter un 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": { "fr": "Maintenance", "en": "Maintenance", @@ -1638,10 +1641,6 @@ TRANSLATIONS = { "fr": "Interface", "en": "Interface", }, - "Maintenance": { - "fr": "Maintenance", - "en": "Maintenance", - }, "Language / Langue": { "fr": "🌐 Langue / Language", "en": "🌐 Language / Langue", @@ -5779,10 +5778,6 @@ TRANSLATIONS = { "fr": "Fuseau inconnu, on garde", "en": "Unknown timezone, keeping", }, - "none": { - "fr": "aucune", - "en": "none", - }, "No SSH key found. Set a password instead? (Y/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): ", @@ -7596,10 +7591,6 @@ TRANSLATIONS = { "fr": "secondes.", "en": "seconds.", }, - "pass": { - "fr": "passe", - "en": "pass", - }, "The cleanup was still running after": { "fr": "Le nettoyage tournait encore après", "en": "The cleanup was still running after", diff --git a/test/test_assistant_menu.py b/test/test_assistant_menu.py index 337287e..b8b356c 100644 --- a/test/test_assistant_menu.py +++ b/test/test_assistant_menu.py @@ -161,32 +161,6 @@ class ClesDeTraduction(unittest.TestCase): self.assertIn(nom, COMMANDS) 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): """La conversation ne doit pas être coupée par des lignes de journal. diff --git a/test/test_todo_i18n.py b/test/test_todo_i18n.py index 00b428d..db29c9f 100644 --- a/test/test_todo_i18n.py +++ b/test/test_todo_i18n.py @@ -2,6 +2,8 @@ # © 2026 TechnoLibre (http://www.technolibre.ca) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +import ast +import collections import os import tempfile import unittest @@ -232,5 +234,63 @@ class TestLangIsConfigured(unittest.TestCase): 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__": unittest.main()