From f1f57758a1a456d5b684590ad366e1a04aa03b31 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Sat, 7 Mar 2026 02:59:07 -0500 Subject: [PATCH] [IMP] i18n script todo --- CLAUDE.md | 25 ++- env_var.sh | 1 + script/todo/todo.json | 16 +- script/todo/todo.py | 281 +++++++++++++----------- script/todo/todo_i18n.py | 451 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 639 insertions(+), 135 deletions(-) create mode 100644 script/todo/todo_i18n.py diff --git a/CLAUDE.md b/CLAUDE.md index 6c520dc..b98adc8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -223,6 +223,29 @@ make doc_markdown # Regénérer toute la doc multilingue - `script/*/` : database, deployment, fork_github_repo, nginx, restful, selenium (2), todo, odoo/migration - `.github/ISSUE_TEMPLATE/` : bug_report, feature_request +## Internationalisation du CLI TODO (i18n) + +Le CLI interactif `script/todo/todo.py` supporte le français et l'anglais. + +### Architecture +- **`script/todo/todo_i18n.py`** — Module de traduction (dictionnaire `TRANSLATIONS`, fonctions `t()`, `get_lang()`, `set_lang()`) +- Les chaînes traduisibles utilisent `t("clé")` au lieu de texte en dur +- Les entrées de `todo.json` peuvent avoir un champ `prompt_description_key` résolu via `t()` (fallback sur `prompt_description`) + +### Résolution de la langue (priorité) +1. Variable d'environnement `EL_LANG` (définie dans `env_var.sh`, défaut `"fr"`) +2. Défaut : `"fr"` + +### Comportement +- Première exécution : prompt bilingue demande à l'utilisateur de choisir sa langue +- Le choix est persisté dans `env_var.sh` +- Changement de langue possible via le menu Execute > Langue/Language + +### Ajouter une traduction +1. Ajouter la clé dans `TRANSLATIONS` de `todo_i18n.py` avec les valeurs `"fr"` et `"en"` +2. Remplacer la chaîne en dur par `t("ma_clé")` dans `todo.py` +3. Pour les entrées JSON : ajouter `"prompt_description_key": "ma_clé"` dans `todo.json` + ## Déploiement - **Docker** : `docker-compose.yml` (PostgreSQL 18 + PostGIS 3.6) @@ -231,7 +254,7 @@ make doc_markdown # Regénérer toute la doc multilingue - **SSL** : Certbot pour les certificats - **DNS** : `script/deployment/update_dns_cloudflare.py` -Plateformes supportées : Ubuntu 20.04-25.04, Debian 12, Arch Linux, macOS (pyenv), Windows (WSL/Docker). +Plateformes supportées : Ubuntu 20.04-25.04, Linux Mint 22.3, Debian 12, Arch Linux, macOS (pyenv), Windows (WSL/Docker). ## Points d'attention pour Claude diff --git a/env_var.sh b/env_var.sh index 9f6bf06..b13134c 100755 --- a/env_var.sh +++ b/env_var.sh @@ -32,3 +32,4 @@ EL_WEBSITE_NAME="localhost" EL_GITHUB_TOKEN="" EL_MANIFEST_PROD="./default.xml" EL_MANIFEST_DEV="./manifest/git_manifest_erplibre.xml" +EL_LANG="fr" diff --git a/script/todo/todo.json b/script/todo/todo.json index b96e68a..ad9b89b 100644 --- a/script/todo/todo.json +++ b/script/todo/todo.json @@ -10,45 +10,45 @@ }, "instance": [ { - "prompt_description": "Test - Instance de base minimale", + "prompt_description_key": "json_instance_test", "makefile_cmd": "db_restore_erplibre_base_db_test", "database": "test" }, { - "prompt_description": "Ouvrir RobotLibre \uD83E\uDD16 minimal", + "prompt_description_key": "json_robot_minimal", "makefile_cmd": "robot_libre", "database": "robotlibre" }, { - "prompt_description": "Ouvrir RobotLibre \uD83E\uDD16 en activant la recherche", + "prompt_description_key": "json_robot_search", "makefile_cmd": "robot_libre_all", "database": "robotlibre" } ], "function": [ { - "prompt_description": "Ouvrir ERPLibre avec TODO \uD83E\uDD16", + "prompt_description_key": "json_open_erplibre_todo", "command": "./.venv.erplibre/bin/python ./script/selenium/web_login.py" } ], "update_from_makefile": [ { - "prompt_description": "Update all erplibre_base on database test", + "prompt_description_key": "json_update_erplibre_base_test", "makefile_cmd": "db_erplibre_base_db_test_update_all", "database": "test" } ], "code_from_makefile": [ { - "prompt_description": "Show code status", + "prompt_description_key": "json_show_code_status", "makefile_cmd": "repo_show_status" }, { - "prompt_description": "Stash all code", + "prompt_description_key": "json_stash_all_code", "makefile_cmd": "repo_do_stash" }, { - "prompt_description": "Format modified code", + "prompt_description_key": "json_format_modified_code", "makefile_cmd": "format" } ] diff --git a/script/todo/todo.py b/script/todo/todo.py index 54fb9cd..e04c86f 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -22,6 +22,7 @@ sys.path.append(new_path) from script.config import config_file from script.execute import execute +from script.todo.todo_i18n import get_lang, lang_is_configured, set_lang, t file_error_path = ".erplibre.error.txt" cst_venv_erplibre = ".venv.erplibre" @@ -40,7 +41,6 @@ STRINGS_FILE = os.path.join( ) GRADLE_FILE = os.path.join(MOBILE_HOME_PATH, ANDROID_DIR, "app/build.gradle") -from script.execute import execute try: import tkinter as tk @@ -63,7 +63,7 @@ except ModuleNotFoundError as e: CRASH_E = e if not ENABLE_CRASH: - print("Importation success!") + print(t("Importation success!")) logging.basicConfig( format=( @@ -88,17 +88,49 @@ class TODO: self.config_file = config_file.ConfigFile() self.execute = execute.Execute() + def _ask_language(self): + if not lang_is_configured(): + print() + print("Choisir la langue / Choose language:") + print("[1] Francais") + print("[2] English") + choice = "" + while choice not in ("1", "2"): + choice = input("Select / Choisir : ").strip() + if choice == "1": + set_lang("fr") + else: + set_lang("en") + + def _change_language(self): + print() + print(t("lang_prompt") + ":") + print(f"[1] {t('lang_french')}") + print(f"[2] {t('lang_english')}") + print(f"[0] {t('back')}") + choice = "" + while choice not in ("0", "1", "2"): + choice = input(t("selection")).strip() + if choice == "0": + return False + elif choice == "1": + set_lang("fr") + else: + set_lang("en") + print(t("lang_changed")) + def run(self): with open(self.config_file.get_logo_ascii_file_path()) as my_file: print(my_file.read()) - print("Ouverture de TODO en cours ...") - print("🤖 => Entre tes directives par son chiffre et fait Entrée!") - help_info = """Commande : -[1] Execute -[2] Install -[3] Question -[4] Fork - Ouvre TODO 🤖 dans une nouvelle tabulation -[0] Quitter + self._ask_language() + print(t("opening")) + print(f"🤖 {t('enter_directives')}") + help_info = f"""{t("command")} +[1] {t("menu_execute")} +[2] {t("menu_install")} +[3] {t("menu_question")} +[4] {t("menu_fork")} +[0] {t("menu_quit")} """ while True: try: @@ -132,7 +164,7 @@ class TODO: # elif status == "3" or status == "install": # print("install") else: - print("Commande non trouvée 🤖!") + print(t("cmd_not_found")) print(status) # manipuler() @@ -161,9 +193,7 @@ class TODO: ["kdbx", "password"] ) if not mot_de_passe_kdbx: - mot_de_passe_kdbx = getpass.getpass( - prompt="Entrez votre mot de passe : " - ) + mot_de_passe_kdbx = getpass.getpass(prompt=t("enter_password")) kp = PyKeePass(chemin_fichier_kdbx, password=mot_de_passe_kdbx) @@ -173,9 +203,9 @@ class TODO: def execute_prompt_ia(self): while True: - help_info = """Commande : -[0] Retour -Écrit moi ta question """ + help_info = f"""{t("command")} +[0] {t("back")} +{t("ia_prompt")}""" status = click.prompt(help_info) print() if status == "0": @@ -199,18 +229,19 @@ class TODO: print() def prompt_execute(self): - help_info = """Commande : -[1] Run - Exécuter et installer une instance -[2] Automatisation - Démonstration des fonctions développées -[3] Mise à jour - Update all developed staging source code -[4] Code - Outil pour développeur -[5] Doc - Recherche de documentation -[6] Database - Outils sur les bases de données -[7] Process - Outils sur les exécutions -[8] Config - Traitement du fichier de configuration -[9] Réseau - Outil réseautique -[10] Sécurité - Audit de sécurité des dépendances -[0] Retour + help_info = f"""{t("command")} +[1] {t("menu_run")} +[2] {t("menu_automation")} +[3] {t("menu_update")} +[4] {t("menu_code")} +[5] {t("menu_doc")} +[6] {t("menu_database")} +[7] {t("menu_process")} +[8] {t("menu_config")} +[9] {t("menu_network")} +[10] {t("menu_security")} +[11] {t("menu_lang")} +[0] {t("back")} """ while True: status = click.prompt(help_info) @@ -257,8 +288,12 @@ class TODO: status = self.prompt_execute_security() if status is not False: return + elif status == "11": + status = self._change_language() + if status is not False: + return else: - print("Commande non trouvée 🤖!") + print(t("cmd_not_found")) def prompt_install(self): print("Detect first installation from code source.") @@ -338,7 +373,7 @@ class TODO: ), "0": ( "0", - "0: Quitter", + f"0: {t('menu_quit')}", ), } dct_final_cmd_intern = {} @@ -374,11 +409,11 @@ class TODO: odoo_version_input = "" while odoo_version_input not in dct_cmd_intern.keys(): if odoo_version_input: - print(f"Error, cannot understand value '{odoo_version_input}'") + print(f"{t('error_value')} '{odoo_version_input}'") str_input_dyn_odoo_version = ( - "💬 Choose a version:\n\t" + f"💬 {t('choose_version')}\n\t" + "\n\t".join([a[1] for a in dct_cmd_intern.values()]) - + "\nSelect : " + + f"\n{t('selection')}" ) odoo_version_input = ( input(str_input_dyn_odoo_version).strip().lower() @@ -388,7 +423,7 @@ class TODO: return cmd_intern = dct_cmd_intern.get(odoo_version_input)[2] - print(f"Will execute :\n{cmd_intern}") + print(f"{t('will_execute')}\n{cmd_intern}") # TODO use external script to detect terminal to use on system # TODO check script open_terminal_code_generator.sh @@ -398,9 +433,7 @@ class TODO: cmd_intern, shell=True, executable="/bin/bash", check=True ) except subprocess.CalledProcessError as e: - print( - f"Le script Bash «{cmd_intern}» a échoué avec le code de retour {e.returncode}." - ) + print(f"{t('script_failed')} {e.returncode}.") print("Wait after installation and open projects by terminal.") print("make open_terminal") self.restart_script(str(e)) @@ -453,12 +486,15 @@ class TODO: callback(dct_instance) def fill_help_info(self, lst_choice): - help_info = "Commande :\n" - help_end = "[0] Retour\n" + help_info = t("command") + "\n" + help_end = f"[0] {t('back')}\n" for i, dct_instance in enumerate(lst_choice): - help_info += ( - f"[{i + 1}] " + dct_instance["prompt_description"] + "\n" - ) + desc_key = dct_instance.get("prompt_description_key") + if desc_key: + desc = t(desc_key) + else: + desc = dct_instance["prompt_description"] + help_info += f"[{i + 1}] " + desc + "\n" help_info += help_end return help_info @@ -472,14 +508,14 @@ class TODO: # Support mobile ERPLibre if os.path.exists(MOBILE_HOME_PATH): dct_upgrade_odoo_database = { - "prompt_description": "Mobile - Compile and run software", + "prompt_description": t("mobile_compile_run"), "callback": self.callback_make_mobile_home, } lst_choice.append(dct_upgrade_odoo_database) # Support custom database to execute dct_upgrade_odoo_database = { - "prompt_description": "Choisir sa base de données", + "prompt_description": t("choose_database"), "callback": self.callback_execute_custom_database, } lst_choice.insert(0, dct_upgrade_odoo_database) @@ -496,9 +532,7 @@ class TODO: int_cmd = int(status) if 1 < int_cmd <= init_len: cmd_no_found = False - status = click.confirm( - "Voulez-vous une nouvelle instance?" - ) + status = click.confirm(t("new_instance_confirm")) dct_instance = lst_choice[int_cmd - 1] self.execute_from_configuration( dct_instance, @@ -515,7 +549,7 @@ class TODO: except ValueError: pass if cmd_no_found: - print("Commande non trouvée 🤖!") + print(t("cmd_not_found")) def prompt_execute_fonction(self): lst_choice = self.config_file.get_config("function") @@ -537,11 +571,11 @@ class TODO: except ValueError: pass if cmd_no_found: - print("Commande non trouvée 🤖!") + print(t("cmd_not_found")) def prompt_execute_update(self): # self.execute.exec_command_live(f"make {makefile_cmd}") - print("🤖 Mise à jour du développement") + print(f"🤖 {t('update_dev')}") # TODO détecter les modules en modification pour faire la mise à jour en cours # TODO demander sur quel BD faire la mise à jour # TODO proposer les modules manuelles selon la configuration à mettre à jour @@ -552,11 +586,11 @@ class TODO: lst_choice = self.config_file.get_config("update_from_makefile") dct_upgrade_odoo_database = { - "prompt_description": "Upgrade Odoo - Migration Database", + "prompt_description": t("upgrade_odoo_migration"), } lst_choice.append(dct_upgrade_odoo_database) dct_upgrade_poetry = { - "prompt_description": "Upgrade Poetry - Dependency of Odoo", + "prompt_description": t("upgrade_poetry_dependency"), } lst_choice.append(dct_upgrade_poetry) help_info = self.fill_help_info(lst_choice) @@ -582,10 +616,10 @@ class TODO: except ValueError: pass if cmd_no_found: - print("Commande non trouvée 🤖!") + print(t("cmd_not_found")) def prompt_execute_code(self): - print("🤖 Qu'avez-vous de besoin pour développer?") + print(f"🤖 {t('code_need')}") # help_info = """Commande : # [1] Status Git local et distant # [2] Démarrer le générateur de code @@ -601,18 +635,18 @@ class TODO: lst_choice = self.config_file.get_config("code_from_makefile") dct_upgrade_odoo_database = { - "prompt_description": "Open SHELL", + "prompt_description": t("open_shell"), } lst_choice.append(dct_upgrade_odoo_database) dct_upgrade_odoo_database = { - "prompt_description": "Upgrade Module", + "prompt_description": t("upgrade_module"), } lst_choice.append(dct_upgrade_odoo_database) lst_choice.append( { - "prompt_description": "Debug", + "prompt_description": t("debug"), } ) @@ -640,15 +674,15 @@ class TODO: except ValueError: pass if cmd_no_found: - print("Commande non trouvée 🤖!") + print(t("cmd_not_found")) def prompt_execute_doc(self): - print("🤖 Vous cherchez de la documentation?") + print(f"🤖 {t('doc_search')}") lst_choice = [ - {"prompt_description": "Migration module coverage"}, - {"prompt_description": "What change between version"}, - {"prompt_description": "OCA guidelines"}, - {"prompt_description": "OCA migration Odoo 19 milestone"}, + {"prompt_description": t("migration_module_coverage")}, + {"prompt_description": t("what_change_between_version")}, + {"prompt_description": t("oca_guidelines")}, + {"prompt_description": t("oca_migration_odoo_19")}, ] help_info = self.fill_help_info(lst_choice) @@ -689,16 +723,14 @@ class TODO: elif status == "4": print("https://github.com/OCA/maintainer-tools/issues/658") else: - print("Commande non trouvée 🤖!") + print(t("cmd_not_found")) def prompt_execute_database(self): - print("🤖 Faites des modifications sur les bases de données!") + print(f"🤖 {t('db_modify')}") lst_choice = [ - { - "prompt_description": "Download database to create backup (.zip)" - }, - {"prompt_description": "Restore from backup (.zip)"}, - {"prompt_description": "Create backup (.zip)"}, + {"prompt_description": t("download_db_backup")}, + {"prompt_description": t("restore_from_backup")}, + {"prompt_description": t("create_backup")}, ] help_info = self.fill_help_info(lst_choice) @@ -714,12 +746,12 @@ class TODO: elif status == "3": self.create_backup_from_database() else: - print("Commande non trouvée 🤖!") + print(t("cmd_not_found")) def prompt_execute_process(self): - print("🤖 Manipuler les processus d'exécution!") + print(f"🤖 {t('process_manage')}") lst_choice = [ - {"prompt_description": "Kill process from actual port"}, + {"prompt_description": t("kill_process_port")}, ] help_info = self.fill_help_info(lst_choice) @@ -731,16 +763,16 @@ class TODO: elif status == "1": self.process_kill_from_port() else: - print("Commande non trouvée 🤖!") + print(t("cmd_not_found")) def prompt_execute_config(self): - print("🤖 Manipuler la configuration ERPLibre et Odoo!") + print(f"🤖 {t('config_manage')}") lst_choice = [ - {"prompt_description": "Generate all configuration"}, - {"prompt_description": "Generate from pre-configuration"}, - {"prompt_description": "Generate from backup file"}, - {"prompt_description": "Generate from database"}, - {"prompt_description": "Setup queue job for parallelism"}, + {"prompt_description": t("generate_all_config")}, + {"prompt_description": t("generate_from_preconfig")}, + {"prompt_description": t("generate_from_backup")}, + {"prompt_description": t("generate_from_database")}, + {"prompt_description": t("setup_queue_job_for_parallelism")}, ] help_info = self.fill_help_info(lst_choice) @@ -760,13 +792,17 @@ class TODO: elif status == "5": self.generate_config_queue_job() else: - print("Commande non trouvée 🤖!") + print(t("cmd_not_found")) def prompt_execute_network(self): - print("🤖 Outil réseautique!") + print(f"🤖 {t('network_tools')}") lst_choice = [ - {"prompt_description": "SSH port-forwarding"}, - {"prompt_description": "Network performance request per second"}, + {"prompt_description": t("ssh_port_forwarding")}, + { + "prompt_description": t( + "network_performance_request_per_second" + ) + }, ] help_info = self.fill_help_info(lst_choice) @@ -780,7 +816,7 @@ class TODO: elif status == "2": self.generate_network_performance_test() else: - print("Commande non trouvée 🤖!") + print(t("cmd_not_found")) def generate_network_port_forwarding(self, add_arg=None): # ssh -L local_port:localhost:remote_port SSH_connection @@ -807,11 +843,9 @@ class TODO: ) def prompt_execute_security(self): - print("🤖 Audit de sécurité des dépendances!") + print(f"🤖 {t('security_audit')}") lst_choice = [ - { - "prompt_description": "pip-audit - Vérifier les vulnérabilités sur les environnements Python" - }, + {"prompt_description": t("pip_audit_desc")}, ] help_info = self.fill_help_info(lst_choice) @@ -823,7 +857,7 @@ class TODO: elif status == "1": self.execute_pip_audit() else: - print("Commande non trouvée 🤖!") + print(t("cmd_not_found")) def execute_pip_audit(self): lst_version, lst_version_installed, odoo_installed_version = ( @@ -846,9 +880,9 @@ class TODO: key_s = str(key_i) label = f"{key_s}: {erplibre_version}" if odoo_version == odoo_installed_version: - label += " - Actuel" + label += f" - {t('current')}" if dct_version.get("default"): - label += " - Défaut" + label += f" - {t('default')}" dct_env[key_s] = { "label": label, @@ -858,26 +892,20 @@ class TODO: } if not dct_env: - print( - "Aucun environnement installé trouvé. Installez" - " d'abord une version d'Odoo." - ) + print(t("no_env_installed")) return # Show selection menu str_input = ( - "💬 Choisir un environnement pour l'audit :\n\t" + f"💬 {t('choose_env_audit')}\n\t" + "\n\t".join([v["label"] for v in dct_env.values()]) - + "\n\t0: Retour" - + "\nSélection : " + + f"\n\t0: {t('back')}" + + f"\n{t('selection')}" ) env_input = "" while env_input not in dct_env.keys() and env_input != "0": if env_input: - print( - f"Erreur, impossible de comprendre la valeur" - f" '{env_input}'" - ) + print(f"{t('error_value')}" f" '{env_input}'") env_input = input(str_input).strip() if env_input == "0": @@ -888,12 +916,12 @@ class TODO: req_path = selected["req_path"] if not os.path.isfile(req_path): - print(f"Fichier de dépendances introuvable : {req_path}") + print(f"{t('dep_file_not_found')}{req_path}") return # TODO support bash from parameter if open gnome-terminal cmd = f"pip-audit -r {req_path} -l;bash" - print(f"Exécution : {cmd}") + print(f"{t('execution')}{cmd}") self.execute.exec_command_live( cmd, source_erplibre=True, @@ -920,10 +948,10 @@ class TODO: def generate_config_from_preconfiguration(self): lst_choice = [ - {"prompt_description": "base"}, - {"prompt_description": "base + code_generator"}, - {"prompt_description": "base + image_db"}, - {"prompt_description": "all"}, + {"prompt_description": t("preconfig_base")}, + {"prompt_description": t("preconfig_base_code_generator")}, + {"prompt_description": t("preconfig_base_image_db")}, + {"prompt_description": t("preconfig_all")}, # {"prompt_description": "base + migration"}, ] help_info = self.fill_help_info(lst_choice) @@ -952,11 +980,11 @@ class TODO: # str_group = f"--group {group}" # self.generate_config(add_arg=str_group) else: - print("Commande non trouvée 🤖!") + print(t("cmd_not_found")) def debug_ide(self): lst_choice = [ - {"prompt_description": "Debug todo.py"}, + {"prompt_description": t("debug_todo_py")}, ] help_info = self.fill_help_info(lst_choice) @@ -971,7 +999,7 @@ class TODO: os.path.join(os.getcwd(), "script/todo/todo.py"), ) else: - print("Commande non trouvée 🤖!") + print(t("cmd_not_found")) def generate_config_from_backup(self): file_name = self.open_file_image_db() @@ -1016,7 +1044,7 @@ class TODO: print(database_name) return database_name else: - print("Commande non trouvée 🤖!") + print(t("cmd_not_found")) def get_odoo_version(self): with open(VERSION_DATA_FILE) as txt: @@ -1175,14 +1203,15 @@ class TODO: def open_shell_on_database(self): database = self.select_database() - cmd_server = f"./odoo_bin.sh shell -d {database}" - status, lst_database = self.execute.exec_command_live( - cmd_server, - return_status_and_output=True, - source_erplibre=False, - single_source_erplibre=True, - new_window=True, - ) + if database: + cmd_server = f"./odoo_bin.sh shell -d {database}" + status, lst_database = self.execute.exec_command_live( + cmd_server, + return_status_and_output=True, + source_erplibre=False, + single_source_erplibre=True, + new_window=True, + ) def open_pycharm_file(self, folder, filename): cmd = "~/.local/share/JetBrains/Toolbox/scripts/pycharm" @@ -1411,7 +1440,7 @@ class TODO: return status, output_path, database_name def restart_script(self, last_error): - print("Reboot TODO 🤖...") + print(f"🤖 {t('reboot_todo')}") # os.execv(sys.executable, ['python'] + sys.argv) # TODO mettre check que le répertoire est créé, s'il existe, auto-loop à corriger if os.path.exists(cst_venv_erplibre) and not os.path.exists( @@ -1579,13 +1608,13 @@ if __name__ == "__main__": todo.crash_diagnostic(CRASH_E) todo.run() except KeyboardInterrupt: - print("Keyboard interrupt") + print(t("keyboard_interrupt")) finally: end_time = time.time() duration_sec = end_time - start_time if humanize: duration_delta = datetime.timedelta(seconds=duration_sec) humain_time = humanize.precisedelta(duration_delta) - print(f"\nTODO execution time {humain_time}\n") + print(f"\n{t('execution_time')} {humain_time}\n") else: - print(f"\nTODO execution time {duration_sec:.2f} sec.\n") + print(f"\n{t('execution_time')} {duration_sec:.2f} sec.\n") diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py new file mode 100644 index 0000000..8a84067 --- /dev/null +++ b/script/todo/todo_i18n.py @@ -0,0 +1,451 @@ +#!/usr/bin/env python3 +# © 2021-2025 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import os +import re + +CONFIG_OVERRIDE_PRIVATE_FILE = "./env_var.sh" + +_current_lang = None + +TRANSLATIONS = { + # Main menu + "Importation success!": { + "fr": "L'importation est un succès!", + "en": "Importation success!", + }, + "opening": { + "fr": "Ouverture de TODO en cours ...", + "en": "Opening TODO ...", + }, + "enter_directives": { + "fr": "=> Entre tes directives par son chiffre et fait Entrée!", + "en": "=> Enter your choice by number and press Enter!", + }, + "command": { + "fr": "Commande :", + "en": "Command:", + }, + "menu_execute": { + "fr": "Exécution", + "en": "Execute", + }, + "menu_install": { + "fr": "Installation", + "en": "Install", + }, + "menu_question": { + "fr": "Question", + "en": "Question", + }, + "menu_fork": { + "fr": "Fork - Ouvre TODO dans une nouvelle tabulation", + "en": "Fork - Open TODO in a new tab", + }, + "menu_quit": { + "fr": "Quitter", + "en": "Quit", + }, + "cmd_not_found": { + "fr": "Commande non trouvée !", + "en": "Command not found !", + }, + "back": { + "fr": "Retour", + "en": "Back", + }, + # Execute submenu + "menu_run": { + "fr": "Run - Exécuter et installer une instance", + "en": "Run - Execute and install an instance", + }, + "menu_automation": { + "fr": "Automatisation - Demonstration des fonctions développées", + "en": "Automation - Demonstration of developed features", + }, + "menu_update": { + "fr": "Mise à jour - Update all developed staging source code", + "en": "Update - Update all developed staging source code", + }, + "menu_code": { + "fr": "Code - Outil pour développeur", + "en": "Code - Developer tools", + }, + "menu_doc": { + "fr": "Doc - Recherche de documentation", + "en": "Doc - Documentation search", + }, + "menu_database": { + "fr": "Database - Outils sur les bases de données", + "en": "Database - Database tools", + }, + "menu_process": { + "fr": "Process - Outils sur les executions", + "en": "Process - Execution tools", + }, + "menu_config": { + "fr": "Config - Traitement du fichier de configuration", + "en": "Config - Configuration file management", + }, + "menu_network": { + "fr": "Réseau - Outil réseautique", + "en": "Network - Network tools", + }, + "menu_security": { + "fr": "Sécurité - Audit de sécurité des dépendances", + "en": "Security - Dependency security audit", + }, + "menu_lang": { + "fr": "Langue - Changer la langue / Change language", + "en": "Language - Change language / Changer la langue", + }, + # Prompts and messages + "enter_password": { + "fr": "Entrez votre mot de passe : ", + "en": "Enter your password: ", + }, + "ia_prompt": { + "fr": "Écrit moi ta question ", + "en": "Write your question ", + }, + "new_instance_confirm": { + "fr": "Voulez-vous une nouvelle instance?", + "en": "Do you want a new instance?", + }, + "ssh_port_forwarding": { + "fr": "SSH port-forwarding", + "en": "SSH port-forwarding", + }, + "network_performance_request_per_second": { + "fr": "Performance réseau en requêtes par seconde", + "en": "Network performance request per second", + }, + "setup_queue_job_for_parallelism": { + "fr": "Configurer la file d'attente pour l'exécution parallèle", + "en": "Setup queue job for parallelism", + }, + "choose_database": { + "fr": "Choisir sa base de données", + "en": "Choose your database", + }, + "update_dev": { + "fr": "Mise à jour du développement", + "en": "Development update", + }, + "code_need": { + "fr": "Qu'avez-vous de besoin pour développer?", + "en": "What do you need for development?", + }, + "doc_search": { + "fr": "Vous cherchez de la documentation?", + "en": "Looking for documentation?", + }, + "db_modify": { + "fr": "Faites des modifications sur les bases de données!", + "en": "Make changes to databases!", + }, + "process_manage": { + "fr": "Manipuler les processus d'exécution!", + "en": "Manage execution processes!", + }, + "config_manage": { + "fr": "Manipuler la configuration ERPLibre et Odoo!", + "en": "Manage ERPLibre and Odoo configuration!", + }, + "network_tools": { + "fr": "Outil réseautique!", + "en": "Network tools!", + }, + "security_audit": { + "fr": "Audit de securite des dépendances!", + "en": "Dependency security audit!", + }, + "script_failed": { + "fr": "Le script Bash a échoué avec le code de retour", + "en": "The Bash script failed with return code", + }, + "no_env_installed": { + "fr": "Aucun environnement installe trouve. Installez d'abord une version d'Odoo.", + "en": "No installed environment found. Install an Odoo version first.", + }, + "choose_env_audit": { + "fr": "Choisir un environnement pour l'audit :", + "en": "Choose an environment for the audit:", + }, + "selection": { + "fr": "Sélection : ", + "en": "Select: ", + }, + "error_value": { + "fr": "Erreur, impossible de comprendre la valeur", + "en": "Error, cannot understand value", + }, + "dep_file_not_found": { + "fr": "Fichier de dépendances introuvable : ", + "en": "Dependencies file not found: ", + }, + "execution": { + "fr": "Execution : ", + "en": "Execution: ", + }, + "current": { + "fr": "Actuel", + "en": "Current", + }, + "default": { + "fr": "Défaut", + "en": "Default", + }, + "reboot_todo": { + "fr": "Reboot TODO ...", + "en": "Reboot TODO ...", + }, + "pip_audit_desc": { + "fr": "pip-audit - Verifier les vulnérabilités sur les environnements Python", + "en": "pip-audit - Check vulnerabilities on Python environments", + }, + "will_execute": { + "fr": "Va exécuter :", + "en": "Will execute:", + }, + "choose_version": { + "fr": "Choisir une version :", + "en": "Choose a version:", + }, + "error_cannot_understand": { + "fr": "Erreur, impossible de comprendre la valeur", + "en": "Error, cannot understand value", + }, + # todo.json translatable prompt_descriptions + "json_instance_test": { + "fr": "Test - Instance de base minimale", + "en": "Test - Minimal base instance", + }, + "json_robot_minimal": { + "fr": "Ouvrir RobotLibre 🤖 minimal", + "en": "Open RobotLibre 🤖 minimal", + }, + "json_robot_search": { + "fr": "Ouvrir RobotLibre 🤖 en activant la recherche", + "en": "Open RobotLibre 🤖 with search enabled", + }, + "json_open_erplibre_todo": { + "fr": "Ouvrir ERPLibre avec TODO 🤖", + "en": "Open ERPLibre with TODO 🤖", + }, + "json_update_erplibre_base_test": { + "fr": "Mise à jour de tous les erplibre_base sur la base de données test", + "en": "Update all erplibre_base on database test", + }, + "json_show_code_status": { + "fr": "Afficher le statut du code", + "en": "Show code status", + }, + "json_stash_all_code": { + "fr": "Remiser tout le code", + "en": "Stash all code", + }, + "json_format_modified_code": { + "fr": "Formater le code modifié", + "en": "Format modified code", + }, + # todo.py hardcoded prompt_descriptions + "mobile_compile_run": { + "fr": "Mobile - Compiler et exécuter le logiciel", + "en": "Mobile - Compile and run software", + }, + "upgrade_odoo_migration": { + "fr": "Mise à jour Odoo - Migration de base de données", + "en": "Upgrade Odoo - Migration Database", + }, + "upgrade_poetry_dependency": { + "fr": "Mise à jour Poetry - Dépendances d'Odoo", + "en": "Upgrade Poetry - Dependency of Odoo", + }, + "open_shell": { + "fr": "Ouvrir le SHELL", + "en": "Open SHELL", + }, + "upgrade_module": { + "fr": "Mise à jour de module", + "en": "Upgrade Module", + }, + "debug": { + "fr": "Débogage", + "en": "Debug", + }, + "migration_module_coverage": { + "fr": "Couverture de migration des modules", + "en": "Migration module coverage", + }, + "what_change_between_version": { + "fr": "Quels changements entre les versions", + "en": "What change between version", + }, + "oca_guidelines": { + "fr": "Directives OCA", + "en": "OCA guidelines", + }, + "oca_migration_odoo_19": { + "fr": "Migration OCA Odoo 19 - Jalons", + "en": "OCA migration Odoo 19 milestone", + }, + "download_db_backup": { + "fr": "Télécharger une base de données pour créer une sauvegarde (.zip)", + "en": "Download database to create backup (.zip)", + }, + "restore_from_backup": { + "fr": "Restaurer a partir d'une sauvegarde (.zip)", + "en": "Restore from backup (.zip)", + }, + "create_backup": { + "fr": "Créer une sauvegarde (.zip)", + "en": "Create backup (.zip)", + }, + "kill_process_port": { + "fr": "Terminer le processus du port actuel", + "en": "Kill process from actual port", + }, + "generate_all_config": { + "fr": "Générer toute la configuration", + "en": "Generate all configuration", + }, + "generate_from_preconfig": { + "fr": "Générer a partir de la pre-configuration", + "en": "Generate from pre-configuration", + }, + "generate_from_backup": { + "fr": "Générer a partir d'un fichier de sauvegarde", + "en": "Generate from backup file", + }, + "generate_from_database": { + "fr": "Générer a partir de la base de données", + "en": "Generate from database", + }, + "preconfig_base": { + "fr": "base", + "en": "base", + }, + "preconfig_base_code_generator": { + "fr": "base + code_generator", + "en": "base + code_generator", + }, + "preconfig_base_image_db": { + "fr": "base + image_db", + "en": "base + image_db", + }, + "preconfig_all": { + "fr": "tout", + "en": "all", + }, + "debug_todo_py": { + "fr": "Débogage todo.py", + "en": "Debug todo.py", + }, + # Language selection + "lang_prompt": { + "fr": "Choisir la langue / Choose language", + "en": "Choose language / Choisir la langue", + }, + "lang_french": { + "fr": "Francais", + "en": "French", + }, + "lang_english": { + "fr": "Anglais", + "en": "English", + }, + "lang_changed": { + "fr": "Langue changée pour : Francais", + "en": "Language changed to: English", + }, + "execution_time": { + "fr": "Temps d'exécution TODO", + "en": "TODO execution time", + }, + "keyboard_interrupt": { + "fr": "Interruption clavier", + "en": "Keyboard interrupt", + }, +} + + +def get_lang(): + global _current_lang + if _current_lang is not None: + return _current_lang + + # 1. Check env_var.sh file + if os.path.exists(CONFIG_OVERRIDE_PRIVATE_FILE): + try: + with open(CONFIG_OVERRIDE_PRIVATE_FILE) as f: + content = f.read() + match = re.search( + r'^EL_LANG=["\']?(\w+)["\']?', content, re.MULTILINE + ) + if match: + lang = match.group(1) + if lang in ("fr", "en"): + _current_lang = lang + return _current_lang + except OSError: + pass + + # 2. Check env var + env_lang = os.environ.get("EL_LANG") + if env_lang in ("fr", "en"): + _current_lang = env_lang + return _current_lang + + # 3. Default + _current_lang = "fr" + return _current_lang + + +def set_lang(lang): + global _current_lang + _current_lang = lang + + # Persist to env_var.sh + if os.path.exists(CONFIG_OVERRIDE_PRIVATE_FILE): + try: + with open(CONFIG_OVERRIDE_PRIVATE_FILE) as f: + content = f.read() + except OSError: + return + + new_line = f'EL_LANG="{lang}"' + if re.search(r"^EL_LANG=", content, re.MULTILINE): + content = re.sub( + r'^EL_LANG=["\']?\w*["\']?', + new_line, + content, + count=1, + flags=re.MULTILINE, + ) + else: + content = content.rstrip("\n") + "\n" + new_line + "\n" + + with open(CONFIG_OVERRIDE_PRIVATE_FILE, "w") as f: + f.write(content) + + +def lang_is_configured(): + """Check if a language has been explicitly set.""" + if os.path.exists(CONFIG_OVERRIDE_PRIVATE_FILE): + try: + with open(CONFIG_OVERRIDE_PRIVATE_FILE) as f: + content = f.read() + return bool(re.search(r"^EL_LANG=", content, re.MULTILINE)) + except OSError: + pass + return False + + +def t(key): + entry = TRANSLATIONS.get(key) + if entry is None: + return key + lang = get_lang() + return entry.get(lang, entry.get("fr", key))