test_runtime_traduire_libelles.py
Rama main · réplica de lectura del repositorio original.
"""Issue GitHub #6 (2026-09-13) : les messages imprimés par les commandes
(download, login, merge...) restaient tout en français, contrairement à la
page web qui suit déjà la langue choisie. `runtime.traduire()` leur donne le
même mécanisme bilingue que LIBELLES dans tray.py, indexé par
`runtime.lire_langue()` (le fichier `blink_langue.txt` laissé par le dernier
chargement de la page). blink_auth.py, blink_cli.py (en entier), blink_models.py,
blink_engine.py, merge_daily.py, maj.py et autostart.py sont les modules
convertis jusqu'ici ; tous sauf blink_auth.py et autostart.py (aucune
collision avec `_`, alias `_()` gardé) nomment leur alias local `msg()`
plutôt que `_()` puisqu'ils utilisent déjà `_` comme variable jetable
ailleurs (`for _, p in lances`, etc.)."""
from __future__ import annotations
import contextlib
import io
import os
import sys
import tempfile
import unittest
import zipfile
from pathlib import Path
from unittest import mock
import runtime
import blink_auth
import blink_cli
import blink_models
import blink_engine
import merge_daily
import maj
import autostart
import smoketest
import watch
import serve
class TestTraduireLibelles(unittest.TestCase):
def setUp(self):
self._dossier = tempfile.TemporaryDirectory(prefix="blink_i18n_test_")
self._ancien_home = os.environ.get("BLINK_HOME")
os.environ["BLINK_HOME"] = self._dossier.name
def tearDown(self):
if self._ancien_home is None:
os.environ.pop("BLINK_HOME", None)
else:
os.environ["BLINK_HOME"] = self._ancien_home
self._dossier.cleanup()
def _regler_langue(self, code: str) -> None:
(Path(self._dossier.name) / runtime.LANGUE).write_text(code, encoding="utf-8")
def test_defaut_francais_sans_fichier_langue(self):
self.assertEqual(runtime.lire_langue(), "fr")
self.assertEqual(blink_auth._("session_invalide"),
"La session enregistrée n'est plus valide.")
def test_bascule_en_anglais(self):
self._regler_langue("en")
self.assertEqual(blink_auth._("session_invalide"),
"The saved session is no longer valid.")
def test_formatage_avec_valeurs(self):
self._regler_langue("en")
self.assertEqual(blink_auth._("code_refuse", remaining=2),
"Code rejected. 2 attempt(s) left.")
self._regler_langue("fr")
self.assertEqual(blink_auth._("code_refuse", remaining=2),
"Code refusé. 2 tentative(s) restante(s).")
def test_toutes_les_cles_existent_dans_les_deux_langues(self):
self.assertEqual(set(blink_auth.LIBELLES["fr"]), set(blink_auth.LIBELLES["en"]))
def test_blink_cli_toutes_les_cles_existent_dans_les_deux_langues(self):
self.assertEqual(set(blink_cli.LIBELLES["fr"]), set(blink_cli.LIBELLES["en"]))
def test_blink_cli_msg_bascule_et_formate(self):
self._regler_langue("en")
self.assertEqual(
blink_cli.msg("sync_module_ligne", nom="Garage", sync_id=1, network_id=2),
"- Garage (ID 1, network 2)")
self._regler_langue("fr")
self.assertEqual(
blink_cli.msg("sync_module_ligne", nom="Garage", sync_id=1, network_id=2),
"- Garage (ID 1, réseau 2)")
def test_blink_cli_pluriel_arrete_verbe(self):
self._regler_langue("en")
self.assertEqual(blink_cli.msg("arrete_verbe_code", verbe="download", code=1),
"Stopped: download (code 1)")
self.assertEqual(blink_cli.msg("arrete_verbe_normal", verbe="download"),
"Stopped: download (normal exit)")
self._regler_langue("fr")
self.assertEqual(blink_cli.msg("arrete_verbe_normal", verbe="download"),
"Arrêté : download (fin normale)")
def test_blink_models_toutes_les_cles_existent_dans_les_deux_langues(self):
self.assertEqual(set(blink_models.LIBELLES["fr"]), set(blink_models.LIBELLES["en"]))
def test_blink_models_human_size_suit_la_langue(self):
self._regler_langue("fr")
self.assertEqual(blink_models.human_size(1024), "1.0 Kio")
self._regler_langue("en")
self.assertEqual(blink_models.human_size(1024), "1.0 KiB")
def test_blink_models_categorie_bascule(self):
self._regler_langue("en")
self.assertEqual(blink_models.msg("cat_reseau"), "network")
self._regler_langue("fr")
self.assertEqual(blink_models.msg("cat_reseau"), "réseau")
def test_blink_engine_toutes_les_cles_existent_dans_les_deux_langues(self):
self.assertEqual(set(blink_engine.LIBELLES["fr"]), set(blink_engine.LIBELLES["en"]))
def test_blink_engine_msg_bascule_et_formate(self):
self._regler_langue("en")
self.assertEqual(blink_engine.msg("nouveaux_clips", n=3), "\nNew clips: 3")
self._regler_langue("fr")
self.assertEqual(blink_engine.msg("nouveaux_clips", n=3), "\nNouveaux clips : 3")
def test_blink_engine_notification_singulier_pluriel(self):
self._regler_langue("en")
self.assertEqual(blink_engine.msg("notif_corps_singulier", n=1),
"1 new clip downloaded. Click to open.")
self.assertEqual(blink_engine.msg("notif_corps_pluriel", n=3),
"3 new clips downloaded. Click to open.")
def test_merge_daily_toutes_les_cles_existent_dans_les_deux_langues(self):
self.assertEqual(set(merge_daily.LIBELLES["fr"]), set(merge_daily.LIBELLES["en"]))
def test_merge_daily_msg_bascule_et_formate(self):
self._regler_langue("en")
self.assertEqual(merge_daily.msg("mp4_invalide"), "FFmpeg did not produce a valid MP4")
self.assertEqual(
merge_daily.msg("jour_deja_a_jour", nom="Garage_2026-09-01.mp4", clips=5),
"Already up to date: Garage_2026-09-01.mp4 (5 clip(s))")
self._regler_langue("fr")
self.assertEqual(merge_daily.msg("mp4_invalide"), "FFmpeg n'a pas produit un MP4 valide")
def test_merge_daily_labels_periode(self):
self._regler_langue("en")
self.assertEqual(merge_daily.msg("label_hebdomadaires"), "Weekly")
self.assertEqual(merge_daily.msg("label_mensuelles"), "Monthly")
self._regler_langue("fr")
self.assertEqual(merge_daily.msg("label_hebdomadaires"), "Hebdomadaires")
self.assertEqual(merge_daily.msg("label_mensuelles"), "Mensuelles")
def test_maj_toutes_les_cles_existent_dans_les_deux_langues(self):
self.assertEqual(set(maj.LIBELLES["fr"]), set(maj.LIBELLES["en"]))
def test_maj_msg_bascule_et_formate(self):
self._regler_langue("en")
# Les refus d'archive viennent du commun (clé, valeurs) et redeviennent,
# par maj._en_langue_courante, l'OSError au texte de la langue de la page.
with tempfile.TemporaryDirectory() as dossier:
archive = Path(dossier) / "mauvaise.zip"
with zipfile.ZipFile(archive, "w") as sortie:
sortie.writestr("../../etc/passwd", b"x")
with self.assertRaises(OSError) as erreur:
maj._extraire(archive, Path(dossier) / "contenu")
self.assertEqual(str(erreur.exception),
"Dangerous path in the archive: '../../etc/passwd'")
self._regler_langue("fr")
with self.assertRaises(OSError) as erreur:
maj._extraire(archive, Path(dossier) / "contenu2")
self.assertEqual(str(erreur.exception),
"Chemin dangereux dans l'archive : '../../etc/passwd'")
self._regler_langue("en")
self.assertEqual(maj.msg("deja_a_jour", version="0.12.20"),
"blink2video 0.12.20 is up to date.")
self._regler_langue("fr")
self.assertEqual(maj.msg("deja_a_jour", version="0.12.20"),
"blink2video 0.12.20 est à jour.")
def test_autostart_toutes_les_cles_existent_dans_les_deux_langues(self):
self.assertEqual(set(autostart.LIBELLES["fr"]), set(autostart.LIBELLES["en"]))
def test_autostart_bascule_et_formate(self):
self._regler_langue("en")
self.assertEqual(autostart._("demarrage_retire", cible="foo.lnk"),
"Autostart removed: foo.lnk")
self.assertEqual(autostart._("intitule_compte", intitule="Startup shortcuts", n=2),
"Startup shortcuts: 2")
self._regler_langue("fr")
self.assertEqual(autostart._("demarrage_deja_absent", cible="foo.lnk"),
"Démarrage automatique déjà absent : foo.lnk")
def test_smoketest_toutes_les_cles_existent_dans_les_deux_langues(self):
self.assertEqual(set(smoketest.LIBELLES["fr"]), set(smoketest.LIBELLES["en"]))
def test_smoketest_bascule_et_formate(self):
self._regler_langue("en")
self.assertEqual(smoketest._("clips_compte_detail", n=5, m=1), "5 clip(s), 1 excluded")
self.assertEqual(smoketest._("marque_echec"), "FAIL ")
self._regler_langue("fr")
self.assertEqual(smoketest._("clips_compte_detail", n=5, m=1), "5 clip(s) dont 1 écarté(s)")
self.assertEqual(smoketest._("marque_echec"), "ECHEC")
def test_watch_toutes_les_cles_existent_dans_les_deux_langues(self):
self.assertEqual(set(watch.MESSAGES["fr"]), set(watch.MESSAGES["en"]))
def test_watch_msg_deja_existant_continue_de_fonctionner(self):
# _msg() deleguait deja lui-meme a MESSAGES[lire_langue()] avant ce
# chantier ; verifie que le refactor vers runtime.traduire() ne casse
# pas les cles deja en place (notifications d'alerte reelles).
self._regler_langue("en")
self.assertEqual(watch._msg("camera_hors_ligne", nom="Garage"),
'Camera "Garage" offline.')
self._regler_langue("fr")
self.assertEqual(watch._msg("camera_hors_ligne", nom="Garage"),
"Caméra « Garage » hors ligne.")
def test_watch_nouvelles_cles_bascule_et_formate(self):
self._regler_langue("en")
self.assertEqual(watch._msg("alerte_ligne", ligne="Garage hors ligne."),
"ALERT Garage hors ligne.")
self.assertEqual(watch._msg("aucune"), "none")
self._regler_langue("fr")
self.assertEqual(watch._msg("cameras_en_sourdine"), "Caméras en sourdine :")
def test_serve_toutes_les_cles_existent_dans_les_deux_langues(self):
self.assertEqual(set(serve.LIBELLES["fr"]), set(serve.LIBELLES["en"]))
def test_serve_msg_bascule_et_formate(self):
self._regler_langue("en")
self.assertEqual(serve.msg("erreur_generique", erreur="ffmpeg introuvable"),
"Error: ffmpeg introuvable")
self.assertEqual(
serve.msg("echec_ecoute_port", port=8765, erreur="port deja utilise"),
"Could not listen on port 8765: port deja utilise")
self._regler_langue("fr")
self.assertEqual(serve.msg("erreur_generique", erreur="ffmpeg introuvable"),
"Erreur : ffmpeg introuvable")
def test_maj_restauration_incomplete_garde_le_message_traduit(self):
self._regler_langue("en")
try:
raise maj.RestaurationIncomplete(
maj.msg("maj_precedente_non_finalisee", marqueur="MARQUEUR"))
except maj.RestaurationIncomplete as erreur:
# Issue #95 : le message dit aussi quel fichier supprimer (le marqueur, nomme).
self.assertTrue(str(erreur).startswith(
"Previous update not finalized: backups and preparation kept."))
self.assertIn("delete the file MARQUEUR", str(erreur))
def test_issue_95_une_mise_a_jour_interrompue_est_reprise_sans_rien_supprimer(self):
# « I did a manual install of the latest version but now every time I try to update I
# get "Previous update not finalized". What do I need to delete? » : plus rien. Le
# nettoyage ne purge pas la sauvegarde tant que la permutation interrompue n'est pas
# reprise, et la permutation suivante la reprend sans toucher a la sauvegarde.
self._regler_langue("en")
with tempfile.TemporaryDirectory() as dossier:
racine = Path(dossier)
installe, neuf = racine / "installe", racine / "neuf"
installe.mkdir()
(neuf / "_internal").mkdir(parents=True)
(neuf / "blink2video.exe").write_bytes(b"neuf")
(neuf / "_internal" / "lib").write_bytes(b"lib-neuve")
(installe / maj.MARQUEUR_PERMUTATION).write_text("{}", encoding="utf-8")
(installe / "blink2video.exe.ancien").write_bytes(b"ancienne version")
(installe / "blink2video.exe").write_bytes(b"a moitie")
maj._nettoyer(installe) # aucun refus
self.assertEqual((installe / "blink2video.exe.ancien").read_bytes(), b"ancienne version")
with contextlib.redirect_stdout(io.StringIO()):
self.assertTrue(maj._permuter(neuf, installe))
self.assertEqual((installe / "blink2video.exe").read_bytes(), b"neuf")
self.assertFalse((installe / maj.MARQUEUR_PERMUTATION).exists())
maj._nettoyer(installe) # menage de la suivante
self.assertFalse((installe / "blink2video.exe.ancien").exists())
self.assertFalse((installe / "blink2video.exe.reprise").exists())
# Issue #16 : les messages propres à runtime.py (bootstrap, --loop) étaient
# restés hors traduction.
def test_runtime_toutes_les_cles_existent_dans_les_deux_langues(self):
self.assertEqual(set(runtime._LIBELLES_RUNTIME["fr"]),
set(runtime._LIBELLES_RUNTIME["en"]))
def test_runtime_bascule_et_formate(self):
self._regler_langue("en")
self.assertEqual(runtime._msg("repetition", minutes=5),
"Repeating every 5 min. Ctrl+C to stop.")
def test_aide_principale_liste_les_verbes_dans_la_langue_de_la_page(self):
# parse_args() lisait toujours Verbe.fr : l'aide principale restait en
# français même en anglais, alors que Verbe.en existait déjà.
self._regler_langue("en")
sortie = io.StringIO()
with mock.patch.object(sys, "argv", ["blink2video", "--help"]), \
contextlib.redirect_stdout(sortie), self.assertRaises(SystemExit):
blink_cli.parse_args()
aide = sortie.getvalue()
self.assertIn(runtime.VERBES["login"].en, aide)
self.assertNotIn(runtime.VERBES["login"].fr, aide)
self.assertIn("Getting started:", aide)
if __name__ == "__main__":
unittest.main()