erplibre/script/todo/todo_i18n.py

8124 lines
294 KiB
Python
Raw Normal View History

2026-03-07 02:59:07 -05:00
#!/usr/bin/env python3
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
2026-03-07 02:59:07 -05:00
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
import os
import re
ENV_VAR_FILE = "./env_var.sh"
2026-03-07 02:59:07 -05:00
_current_lang = None
TRANSLATIONS = {
# Main menu
"Importation success!": {
"fr": "L'importation est un succès!",
"en": "Importation success!",
},
"Opening TODO ...": {
2026-03-07 02:59:07 -05:00
"fr": "Ouverture de TODO en cours ...",
"en": "Opening TODO ...",
},
"=> Enter your choice by number and press Enter!": {
2026-03-07 02:59:07 -05:00
"fr": "=> Entre tes directives par son chiffre et fait Entrée!",
"en": "=> Enter your choice by number and press Enter!",
},
"Command:": {
2026-03-07 02:59:07 -05:00
"fr": "Commande :",
"en": "Command:",
},
"Execute": {
"fr": "🧰 Exécution",
"en": "🧰 Execute",
2026-03-07 02:59:07 -05:00
},
"Install": {
"fr": "📦 Installation",
"en": "📦 Install",
2026-03-07 02:59:07 -05:00
},
"Assistant": {
"fr": "🤖 Assistant",
"en": "🤖 Assistant",
},
"Fork - Open TODO in a new tab": {
"fr": "🔀 Fork - Ouvre TODO dans une nouvelle tabulation",
"en": "🔀 Fork - Open TODO in a new tab",
2026-03-07 02:59:07 -05:00
},
"Quit": {
2026-03-07 02:59:07 -05:00
"fr": "Quitter",
"en": "Quit",
},
"Command not found !": {
2026-03-07 02:59:07 -05:00
"fr": "Commande non trouvée !",
"en": "Command not found !",
},
"Back": {
"fr": "🔙 Retour",
"en": "🔙 Back",
},
# Execute submenu
"Run - Execute and install an instance": {
"fr": "🏃 Run - Exécuter et installer une instance",
"en": "🏃 Run - Execute and install an instance",
},
"Automation - Demonstration of developed features": {
"fr": "🦾 Automatisation - Demonstration des fonctions développées",
"en": "🦾 Automation - Demonstration of developed features",
},
"Update - Update all developed staging source code": {
"fr": "🔃 Mise à jour - Update all developed staging source code",
"en": "🔃 Update - Update all developed staging source code",
},
"Code - Developer tools": {
"fr": "💻 Code - Outil pour développeur",
"en": "💻 Code - Developer tools",
},
"Doc - Documentation search": {
"fr": "📖 Doc - Recherche de documentation",
"en": "📖 Doc - Documentation search",
},
"Database - Database tools": {
"fr": "💾 Database - Outils sur les bases de données",
"en": "💾 Database - Database tools",
},
"Process - Execution tools": {
"fr": "📟 Process - Outils sur les executions",
"en": "📟 Process - Execution tools",
},
"Config - Configuration file management": {
"fr": "🔧 Config - Traitement du fichier de configuration",
"en": "🔧 Config - Configuration file management",
},
"Network - Network tools": {
"fr": "📡 Réseau - Outil réseautique",
"en": "📡 Network - Network tools",
},
"Security - Dependency security audit": {
"fr": "🔒 Sécurité - Audit de sécurité des dépendances",
"en": "🔒 Security - Dependency security audit",
},
"RTK - CLI proxy to reduce LLM token consumption": {
"fr": "RTK - Proxy CLI pour réduire la consommation de tokens LLM",
"en": "RTK - CLI proxy to reduce LLM token consumption",
},
"Language - Change language / Changer la langue": {
"fr": "🌍 Langue - Changer la langue / Change language",
"en": "🌍 Language - Change language / Changer la langue",
},
# Deploy section
"Deploy - Deploy ERPLibre locally": {
"fr": "🚀 Déploiement - Déployer ERPLibre localement",
"en": "🚀 Deploy - Deploy ERPLibre locally",
},
"Deploy ERPLibre to a local directory!": {
"fr": "Déployer ERPLibre dans un répertoire local!",
"en": "Deploy ERPLibre to a local directory!",
},
"Clone ERPLibre locally (git clone)": {
"fr": "📥 Cloner ERPLibre localement (git clone)",
"en": "📥 Clone ERPLibre locally (git clone)",
},
"Target directory path (default: ~/erplibre): ": {
"fr": "Chemin du répertoire cible (défaut: ~/erplibre) : ",
"en": "Target directory path (default: ~/erplibre): ",
},
"Directory already exists: ": {
"fr": "Le répertoire existe déjà : ",
"en": "Directory already exists: ",
},
"Cloning ERPLibre...": {
"fr": "Clonage d'ERPLibre en cours...",
"en": "Cloning ERPLibre...",
},
"ERPLibre cloned successfully to: ": {
"fr": "ERPLibre cloné avec succès dans : ",
"en": "ERPLibre cloned successfully to: ",
},
"Error cloning ERPLibre: ": {
"fr": "Erreur lors du clonage d'ERPLibre : ",
"en": "Error cloning ERPLibre: ",
},
"Configure sshfs": {
"fr": "📁 Configurer sshfs",
"en": "📁 Configure sshfs",
},
"Local": {
"fr": "💻 Local",
"en": "💻 Local",
},
"SSH (remote host)": {
"fr": "SSH (hôte distant)",
"en": "SSH (remote host)",
},
"Remote & services": {
"fr": "🌐 Distant & services",
"en": "🌐 Remote & services",
},
"SSH (remote host)...": {
"fr": "🔐 SSH (hôte distant)…",
"en": "🔐 SSH (remote host)...",
},
"Deploy ERPLibre to a remote host over SSH!": {
"fr": "Déployer ERPLibre sur un hôte distant via SSH !",
"en": "Deploy ERPLibre to a remote host over SSH!",
},
"Virtualization & notifications": {
"fr": "Virtualisation & notifications",
"en": "Virtualization & notifications",
},
"Development": {
"fr": "🧰 Développement",
"en": "🧰 Development",
},
"Data": {
"fr": "📊 Données",
"en": "📊 Data",
},
"Sources & documentation": {
"fr": "📚 Sources & documentation",
"en": "📚 Sources & documentation",
},
"AI & automation": {
"fr": "🧠 IA & automatisation",
"en": "🧠 AI & automation",
},
"Deployment, network & security": {
"fr": "🌐 Déploiement, réseau & sécurité",
"en": "🌐 Deployment, network & security",
},
"Preferences": {
"fr": "🎨 Préférences",
"en": "🎨 Preferences",
},
"Deployment": {
"fr": "🚀 Déploiement",
"en": "🚀 Deployment",
},
"Manage": {
"fr": "🛠 Gérer",
"en": "🛠 Manage",
},
"Catalog": {
"fr": "📚 Catalogue",
"en": "📚 Catalog",
},
"Open the console on a VM": {
"fr": "🖥 Ouvrir la console d'une VM",
"en": "🖥 Open the console on a VM",
},
"Backup": {
"fr": "Sauvegarde",
"en": "Backup",
},
"Restore": {
"fr": "Restauration",
"en": "Restore",
},
"Danger zone": {
"fr": "Zone dangereuse",
"en": "Danger zone",
},
"Setup": {
"fr": "Installation",
"en": "Setup",
},
"Status": {
"fr": "Statut",
"en": "Status",
},
"Optimize": {
"fr": "Optimisation",
"en": "Optimize",
},
"Generate": {
"fr": "Génération",
"en": "Generate",
},
"Advanced": {
"fr": "Avancé",
"en": "Advanced",
},
"To leave the console, press Ctrl+] (then Enter).": {
"fr": "Pour quitter la console, appuyez sur Ctrl+] (puis Entrée).",
"en": "To leave the console, press Ctrl+] (then Enter).",
},
"Default login (if set at deploy): erplibre / erplibre": {
"fr": "Login par défaut (si défini au déploiement) : "
"erplibre / erplibre",
"en": "Default login (if set at deploy): erplibre / erplibre",
},
"Console password for 'erplibre' (default: erplibre): ": {
"fr": "Mot de passe console pour « erplibre » (défaut : erplibre) : ",
"en": "Console password for 'erplibre' (default: erplibre): ",
},
"Console/SSH login:": {
"fr": "Connexion console/SSH :",
"en": "Console/SSH login:",
},
"Default login:": {
"fr": "Login par défaut :",
"en": "Default login:",
},
"Add this VM to ~/.ssh/config? (y/N): ": {
"fr": "Ajouter cette VM à ~/.ssh/config ? (o/N, défaut : non) : ",
"en": "Add this VM to ~/.ssh/config? (y/N, default: no): ",
},
"Add each VM to ~/.ssh/config? (Y/n): ": {
"fr": "Ajouter chaque VM à ~/.ssh/config ? (O/n, défaut : oui) : ",
"en": "Add each VM to ~/.ssh/config? (Y/n, default: yes): ",
},
"Waiting for the VM IP (DHCP lease)...": {
"fr": "Attente de l'IP de la VM (bail DHCP)...",
"en": "Waiting for the VM IP (DHCP lease)...",
},
"No IP yet; add it later once the VM has booted.": {
"fr": "Pas encore d'IP ; à ajouter plus tard une fois la VM démarrée.",
"en": "No IP yet; add it later once the VM has booted.",
},
"Added to ~/.ssh/config:": {
"fr": "Ajouté à ~/.ssh/config :",
"en": "Added to ~/.ssh/config:",
},
"SSH address input method": {
"fr": "Méthode de saisie de l'adresse SSH",
"en": "SSH address input method",
},
"Manual entry": {
"fr": "Saisie manuelle",
"en": "Manual entry",
},
"From ~/.ssh/config": {
"fr": "Depuis ~/.ssh/config",
"en": "From ~/.ssh/config",
},
"Your choice (1/2): ": {
"fr": "Votre choix (1/2) : ",
"en": "Your choice (1/2): ",
},
"No SSH hosts found in ~/.ssh/config": {
"fr": "Aucun hôte SSH trouvé dans ~/.ssh/config",
"en": "No SSH hosts found in ~/.ssh/config",
},
"Select SSH host number: ": {
"fr": "Numéro de l'hôte SSH à sélectionner : ",
"en": "Select SSH host number: ",
},
"Invalid selection!": {
"fr": "Sélection invalide!",
"en": "Invalid selection!",
},
"SSH host (e.g.: user@192.168.1.100): ": {
"fr": "Hôte SSH (ex: user@192.168.1.100) : ",
"en": "SSH host (e.g.: user@192.168.1.100): ",
},
"Mounting sshfs on: ": {
"fr": "Montage sshfs sur : ",
"en": "Mounting sshfs on: ",
},
"Mounted on: ": {
"fr": "Monté sur : ",
"en": "Mounted on: ",
},
"To unmount: ": {
"fr": "Pour démonter : ",
"en": "To unmount: ",
},
"Error mounting sshfs: ": {
"fr": "Erreur lors du montage sshfs : ",
"en": "Error mounting sshfs: ",
},
# RTK (Rust Token Killer)
"Manage RTK (Rust Token Killer) for token optimization!": {
"fr": "Gérer RTK (Rust Token Killer) pour optimiser les tokens!",
"en": "Manage RTK (Rust Token Killer) for token optimization!",
},
"Install RTK": {
"fr": "Installer RTK",
"en": "Install RTK",
},
"Check RTK version": {
"fr": "Vérifier la version de RTK",
"en": "Check RTK version",
},
"Show cumulative token savings": {
"fr": "Afficher les économies de tokens cumulées",
"en": "Show cumulative token savings",
},
"Discover optimization opportunities": {
"fr": "Identifier les opportunités d'optimisation",
"en": "Discover optimization opportunities",
},
"Initialize global auto-rewrite hook": {
"fr": "Initialiser le hook auto-rewrite global",
"en": "Initialize global auto-rewrite hook",
},
"Check RTK status": {
"fr": "Vérifier le statut de RTK",
"en": "Check RTK status",
},
"RTK is not installed. Use option 1 to install it.": {
"fr": "RTK n'est pas installé. Utilisez l'option 1 pour l'installer.",
"en": "RTK is not installed. Use option 1 to install it.",
},
"RTK is installed, version: ": {
"fr": "RTK est installé, version : ",
"en": "RTK is installed, version: ",
},
"Global auto-rewrite hook: active": {
"fr": "Hook auto-rewrite global : actif",
"en": "Global auto-rewrite hook: active",
},
"Global auto-rewrite hook: inactive": {
"fr": "Hook auto-rewrite global : inactif",
"en": "Global auto-rewrite hook: inactive",
},
"Installation method:": {
"fr": "Méthode d'installation :",
"en": "Installation method:",
},
"curl - Automatic install script": {
"fr": "curl - Script d'installation automatique",
"en": "curl - Automatic install script",
},
"brew - Homebrew (macOS/Linux)": {
"fr": "brew - Homebrew (macOS/Linux)",
"en": "brew - Homebrew (macOS/Linux)",
},
"cargo - Build from source (Rust required)": {
"fr": "cargo - Compilation depuis les sources (Rust requis)",
"en": "cargo - Build from source (Rust required)",
},
2026-03-07 02:59:07 -05:00
# Prompts and messages
"Enter your password: ": {
2026-03-07 02:59:07 -05:00
"fr": "Entrez votre mot de passe : ",
"en": "Enter your password: ",
},
"Write your question ": {
2026-03-07 02:59:07 -05:00
"fr": "Écrit moi ta question ",
"en": "Write your question ",
},
"Do you want a new instance?": {
2026-03-07 02:59:07 -05:00
"fr": "Voulez-vous une nouvelle instance?",
"en": "Do you want a new instance?",
},
"SSH port-forwarding": {
2026-03-07 02:59:07 -05:00
"fr": "SSH port-forwarding",
"en": "SSH port-forwarding",
},
"Network performance request per second": {
2026-03-07 02:59:07 -05:00
"fr": "Performance réseau en requêtes par seconde",
"en": "Network performance request per second",
},
"Setup queue job for parallelism": {
2026-03-07 02:59:07 -05:00
"fr": "Configurer la file d'attente pour l'exécution parallèle",
"en": "Setup queue job for parallelism",
},
"Choose your database": {
2026-03-07 02:59:07 -05:00
"fr": "Choisir sa base de données",
"en": "Choose your database",
},
"Development update": {
2026-03-07 02:59:07 -05:00
"fr": "Mise à jour du développement",
"en": "Development update",
},
"What do you need for development?": {
2026-03-07 02:59:07 -05:00
"fr": "Qu'avez-vous de besoin pour développer?",
"en": "What do you need for development?",
},
"Looking for documentation?": {
2026-03-07 02:59:07 -05:00
"fr": "Vous cherchez de la documentation?",
"en": "Looking for documentation?",
},
"Make changes to databases!": {
2026-03-07 02:59:07 -05:00
"fr": "Faites des modifications sur les bases de données!",
"en": "Make changes to databases!",
},
# Database drop / erase
"Erase a database": {
"fr": "Effacer une base de données",
"en": "Erase a database",
},
"Erase a database — irreversible operation!": {
"fr": "Effacer une base de données — opération irréversible !",
"en": "Erase a database — irreversible operation!",
},
"Erase ALL databases (make db_drop_all)": {
"fr": "Effacer TOUTES les bases de données (make db_drop_all)",
"en": "Erase ALL databases (make db_drop_all)",
},
"Erase a single database": {
"fr": "Effacer une seule base de données",
"en": "Erase a single database",
},
"You are about to erase ALL databases. This cannot be undone.": {
"fr": (
"Tu es sur le point d'effacer TOUTES les bases de données."
" C'est irréversible."
),
"en": "You are about to erase ALL databases. This cannot be undone.",
},
"You are about to erase the database '{database}'. This cannot be undone.": {
"fr": (
"Tu es sur le point d'effacer la base de données « {database} »."
" C'est irréversible."
),
"en": (
"You are about to erase the database '{database}'."
" This cannot be undone."
),
},
"Type 'oui' to confirm (default: no): ": {
"fr": "Tape « oui » pour confirmer (défaut : non) : ",
"en": "Type 'oui'/'yes' to confirm (default: no): ",
},
"Database deletion cancelled.": {
"fr": "Effacement de la base de données annulé.",
"en": "Database deletion cancelled.",
},
"No database selected.": {
"fr": "Aucune base de données sélectionnée.",
"en": "No database selected.",
},
"Cannot list the databases (exit code): ": {
"fr": "Impossible de lister les bases (code de retour) : ",
"en": "Cannot list the databases (exit code): ",
},
"Is PostgreSQL running?": {
"fr": "PostgreSQL est-il démarré ?",
"en": "Is PostgreSQL running?",
},
"No database on this PostgreSQL server.": {
"fr": "Aucune base sur ce serveur PostgreSQL.",
"en": "No database on this PostgreSQL server.",
},
"Manage execution processes!": {
2026-03-07 02:59:07 -05:00
"fr": "Manipuler les processus d'exécution!",
"en": "Manage execution processes!",
},
"Manage ERPLibre and Odoo configuration!": {
2026-03-07 02:59:07 -05:00
"fr": "Manipuler la configuration ERPLibre et Odoo!",
"en": "Manage ERPLibre and Odoo configuration!",
},
"Network tools!": {
2026-03-07 02:59:07 -05:00
"fr": "Outil réseautique!",
"en": "Network tools!",
},
"Dependency security audit!": {
2026-03-07 02:59:07 -05:00
"fr": "Audit de securite des dépendances!",
"en": "Dependency security audit!",
},
"The Bash script failed with return code": {
2026-03-07 02:59:07 -05:00
"fr": "Le script Bash a échoué avec le code de retour",
"en": "The Bash script failed with return code",
},
"No installed environment found. Install an Odoo version first.": {
2026-03-07 02:59:07 -05:00
"fr": "Aucun environnement installe trouve. Installez d'abord une version d'Odoo.",
"en": "No installed environment found. Install an Odoo version first.",
},
"Choose an environment for the audit:": {
2026-03-07 02:59:07 -05:00
"fr": "Choisir un environnement pour l'audit :",
"en": "Choose an environment for the audit:",
},
"Select: ": {
2026-03-07 02:59:07 -05:00
"fr": "Sélection : ",
"en": "Select: ",
},
"Error, cannot understand value": {
2026-03-07 02:59:07 -05:00
"fr": "Erreur, impossible de comprendre la valeur",
"en": "Error, cannot understand value",
},
"Dependencies file not found: ": {
2026-03-07 02:59:07 -05:00
"fr": "Fichier de dépendances introuvable : ",
"en": "Dependencies file not found: ",
},
"Execution: ": {
2026-03-07 02:59:07 -05:00
"fr": "Execution : ",
"en": "Execution: ",
},
"Current": {
2026-03-07 02:59:07 -05:00
"fr": "Actuel",
"en": "Current",
},
"Default": {
2026-03-07 02:59:07 -05:00
"fr": "Défaut",
"en": "Default",
},
"Reboot TODO ...": {
2026-03-07 02:59:07 -05:00
"fr": "Reboot TODO ...",
"en": "Reboot TODO ...",
},
"pip-audit - Check vulnerabilities on Python environments": {
2026-03-07 02:59:07 -05:00
"fr": "pip-audit - Verifier les vulnérabilités sur les environnements Python",
"en": "pip-audit - Check vulnerabilities on Python environments",
},
"Will execute:": {
2026-03-07 02:59:07 -05:00
"fr": "Va exécuter :",
"en": "Will execute:",
},
"Choose a version:": {
2026-03-07 02:59:07 -05:00
"fr": "Choisir une version :",
"en": "Choose a version:",
},
"Install type:": {
"fr": "Type d'installation :",
"en": "Install type:",
},
"Standard install (without extra modules)": {
"fr": "Installation standard (sans modules extra)",
"en": "Standard install (without extra modules)",
},
"Install with extra modules (CybroOdoo - large, slow)": {
"fr": "Installation avec modules extra (CybroOdoo - gros, lent)",
"en": "Install with extra modules (CybroOdoo - large, slow)",
},
2026-03-07 02:59:07 -05:00
# todo.json translatable prompt_descriptions
"Test - Minimal base instance": {
2026-03-07 02:59:07 -05:00
"fr": "Test - Instance de base minimale",
"en": "Test - Minimal base instance",
},
"Open RobotLibre 🤖 minimal": {
2026-03-07 02:59:07 -05:00
"fr": "Ouvrir RobotLibre 🤖 minimal",
"en": "Open RobotLibre 🤖 minimal",
},
"Open RobotLibre 🤖 with search enabled": {
2026-03-07 02:59:07 -05:00
"fr": "Ouvrir RobotLibre 🤖 en activant la recherche",
"en": "Open RobotLibre 🤖 with search enabled",
},
"Open ERPLibre with TODO 🤖": {
2026-03-07 02:59:07 -05:00
"fr": "Ouvrir ERPLibre avec TODO 🤖",
"en": "Open ERPLibre with TODO 🤖",
},
"Update all erplibre_base on database test": {
"fr": "🔄 Mise à jour de tous les erplibre_base sur la base de données test",
"en": "🔄 Update all erplibre_base on database test",
},
"Show code status": {
"fr": "🔍 Afficher le statut du code",
"en": "🔍 Show code status",
},
"Stash all code": {
"fr": "📦 Remiser tout le code",
"en": "📦 Stash all code",
},
"Format modified code": {
"fr": "🎨 Formater le code modifié",
"en": "🎨 Format modified code",
},
2026-03-07 02:59:07 -05:00
# todo.py hardcoded prompt_descriptions
"Mobile - Compile and run software": {
2026-03-07 02:59:07 -05:00
"fr": "Mobile - Compiler et exécuter le logiciel",
"en": "Mobile - Compile and run software",
},
"Upgrade Odoo - Migration Database": {
"fr": "🚚 Mise à jour Odoo - Migration de base de données",
"en": "🚚 Upgrade Odoo - Migration Database",
},
"Upgrade Poetry - Dependency of Odoo": {
"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": {
2026-03-07 02:59:07 -05:00
"fr": "Couverture de migration des modules",
"en": "Migration module coverage",
},
"What change between version": {
2026-03-07 02:59:07 -05:00
"fr": "Quels changements entre les versions",
"en": "What change between version",
},
"OCA guidelines": {
2026-03-07 02:59:07 -05:00
"fr": "Directives OCA",
"en": "OCA guidelines",
},
"OCA migration Odoo 19 milestone": {
2026-03-07 02:59:07 -05:00
"fr": "Migration OCA Odoo 19 - Jalons",
"en": "OCA migration Odoo 19 milestone",
},
"Download database to create backup (.zip)": {
2026-03-07 02:59:07 -05:00
"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 (.zip)": {
2026-03-07 02:59:07 -05:00
"fr": "Restaurer a partir d'une sauvegarde (.zip)",
"en": "Restore from backup (.zip)",
},
"Create backup (.zip)": {
2026-03-07 02:59:07 -05:00
"fr": "Créer une sauvegarde (.zip)",
"en": "Create backup (.zip)",
},
"Kill Odoo process from actual port": {
"fr": "Terminer le processus Odoo du port actuel",
"en": "Kill Odoo process from actual port",
},
"Kill git daemon server process": {
"fr": "Terminer le processus du serveur git daemon",
"en": "Kill git daemon server process",
},
"Git daemon process killed.": {
"fr": "Processus git daemon terminé.",
"en": "Git daemon process killed.",
2026-03-07 02:59:07 -05:00
},
"Generate all configuration": {
2026-03-07 02:59:07 -05:00
"fr": "Générer toute la configuration",
"en": "Generate all configuration",
},
"Generate from pre-configuration": {
2026-03-07 02:59:07 -05:00
"fr": "Générer a partir de la pre-configuration",
"en": "Generate from pre-configuration",
},
"Generate from backup file": {
2026-03-07 02:59:07 -05:00
"fr": "Générer a partir d'un fichier de sauvegarde",
"en": "Generate from backup file",
},
"Generate from database": {
2026-03-07 02:59:07 -05:00
"fr": "Générer a partir de la base de données",
"en": "Generate from database",
},
"base": {
2026-03-07 02:59:07 -05:00
"fr": "base",
"en": "base",
},
"base + code_generator": {
2026-03-07 02:59:07 -05:00
"fr": "base + code_generator",
"en": "base + code_generator",
},
"base + image_db": {
2026-03-07 02:59:07 -05:00
"fr": "base + image_db",
"en": "base + image_db",
},
"all": {
2026-03-07 02:59:07 -05:00
"fr": "tout",
"en": "all",
},
"Debug todo.py": {
2026-03-07 02:59:07 -05:00
"fr": "Débogage todo.py",
"en": "Debug todo.py",
},
# Test section
"Test - Test an Odoo module": {
"fr": "🧪 Test - Tester un module Odoo",
"en": "🧪 Test - Test an Odoo module",
},
"Test an Odoo module on a temporary database!": {
2026-03-08 15:53:51 -04:00
"fr": "Tester un module Odoo sur une base de données temporaire!",
"en": "Test an Odoo module on a temporary database!",
},
"Test a module": {
2026-03-08 15:53:51 -04:00
"fr": "Tester un module",
"en": "Test a module",
},
"Test a module with code coverage": {
2026-03-08 15:53:51 -04:00
"fr": "Tester un module avec couverture de code",
"en": "Test a module with code coverage",
},
"ERPLibre unit tests": {
2026-03-10 01:44:10 -04:00
"fr": "Tests unitaires ERPLibre",
"en": "ERPLibre unit tests",
},
"Mail unit tests": {
"fr": "Tests unitaires courriel",
"en": "Mail unit tests",
},
"Analyse unit tests": {
"fr": "Tests unitaires analyse",
"en": "Analyse unit tests",
},
"Running unit tests": {
2026-03-10 01:44:10 -04:00
"fr": "Exécution des tests unitaires",
"en": "Running unit tests",
},
"All unit tests passed": {
2026-03-10 01:44:10 -04:00
"fr": "Tous les tests unitaires ont réussi",
"en": "All unit tests passed",
},
"Some unit tests failed, exit code": {
2026-03-10 01:44:10 -04:00
"fr": "Des tests unitaires ont échoué, code de sortie",
"en": "Some unit tests failed, exit code",
},
"Module name to test: ": {
2026-03-08 15:53:51 -04:00
"fr": "Nom du module à tester : ",
"en": "Module name to test: ",
},
"Temporary database name (default: test_todo_tmp): ": {
2026-03-08 15:53:51 -04:00
"fr": "Nom de la base de données temporaire (défaut: test_todo_tmp) : ",
"en": "Temporary database name (default: test_todo_tmp): ",
},
"Extra modules to install (comma-separated, empty for none): ": {
2026-03-08 15:53:51 -04:00
"fr": "Modules supplémentaires à installer (séparés par des virgules, vide pour aucun) : ",
"en": "Extra modules to install (comma-separated, empty for none): ",
},
"Log level (default: test): ": {
2026-03-08 15:53:51 -04:00
"fr": "Niveau de log (défaut: test) : ",
"en": "Log level (default: test): ",
},
"Creating temporary database": {
2026-03-08 15:53:51 -04:00
"fr": "Création de la base de données temporaire",
"en": "Creating temporary database",
},
"Installing modules": {
2026-03-08 15:53:51 -04:00
"fr": "Installation des modules",
"en": "Installing modules",
},
"Running tests": {
2026-03-08 15:53:51 -04:00
"fr": "Exécution des tests",
"en": "Running tests",
},
"Cleaning up temporary database": {
2026-03-08 15:53:51 -04:00
"fr": "Suppression de la base de données temporaire",
"en": "Cleaning up temporary database",
},
"Keep the temporary database? (y/N): ": {
"fr": "Conserver la base de données temporaire? (o/N, défaut : non) : ",
"en": "Keep the temporary database? (y/N, default: no): ",
},
"Database kept": {
2026-03-08 15:53:51 -04:00
"fr": "Base de données conservée",
"en": "Database kept",
},
"Tests completed successfully!": {
2026-03-08 15:53:51 -04:00
"fr": "Tests terminés avec succès!",
"en": "Tests completed successfully!",
},
"Tests failed with return code": {
2026-03-08 15:53:51 -04:00
"fr": "Les tests ont échoué avec le code de retour",
"en": "Tests failed with return code",
},
"Module name is required!": {
2026-03-08 15:53:51 -04:00
"fr": "Le nom du module est requis!",
"en": "Module name is required!",
},
# Git section
"Git - Git tools": {
"fr": "🌿 Git - Outils Git",
"en": "🌿 Git - Git tools",
},
"Git management tools!": {
"fr": "Outils de gestion Git!",
"en": "Git management tools!",
},
"Local git server": {
"fr": "Serveur git local",
"en": "Local git server",
},
"Manage local git repository server!": {
"fr": "Gérer le serveur de dépôts git local!",
"en": "Manage local git repository server!",
},
"Deploy a local git server (~/.git-server)": {
"fr": "Déployer un serveur git local (~/.git-server)",
"en": "Deploy a local git server (~/.git-server)",
},
"Deploy a production git server (/srv/git, root required)": {
"fr": "Déployer un serveur git production (/srv/git, root requis)",
"en": "Deploy a production git server (/srv/git, root required)",
},
"Starting git server deployment...": {
"fr": "Démarrage du déploiement du serveur git...",
"en": "Starting git server deployment...",
},
"Local mode (~/.git-server)": {
"fr": "Mode local (~/.git-server)",
"en": "Local mode (~/.git-server)",
},
"Production mode (/srv/git, root required)": {
"fr": "Mode production (/srv/git, root requis)",
"en": "Production mode (/srv/git, root required)",
},
"Run all (init + remote + push + serve)": {
"fr": "Tout exécuter (init + remote + push + serve)",
"en": "Run all (init + remote + push + serve)",
},
"Init - Create bare repos": {
"fr": "Init - Créer les bare repos",
"en": "Init - Create bare repos",
},
"Remote - Add local remotes": {
"fr": "Remote - Ajouter les remotes locaux",
"en": "Remote - Add local remotes",
},
"Push - Push to local server": {
"fr": "Push - Pousser vers le serveur local",
"en": "Push - Push to local server",
},
"Serve - Start git daemon": {
"fr": "Serve - Démarrer le daemon git",
"en": "Serve - Start git daemon",
},
# Git remote add
"Add a remote to a local repository": {
"fr": "Ajouter un remote vers un dépôt local",
"en": "Add a remote to a local repository",
},
"Remote name (default: localhost): ": {
"fr": "Nom du remote (défaut: localhost) : ",
"en": "Remote name (default: localhost): ",
},
"Repository address (e.g.: git://192.168.1.100/my-repo.git): ": {
"fr": "Adresse du dépôt (ex: git://192.168.1.100/mon-repo.git) : ",
"en": "Repository address (e.g.: git://192.168.1.100/my-repo.git): ",
},
"Repository address is required!": {
"fr": "L'adresse du dépôt est requise!",
"en": "Repository address is required!",
},
"Remote added successfully!": {
"fr": "Remote ajouté avec succès!",
"en": "Remote added successfully!",
},
"Error adding remote: ": {
"fr": "Erreur lors de l'ajout du remote : ",
"en": "Error adding remote: ",
},
# Git config vim
"Configure git local editor to vim": {
"fr": "Configuration git local par vim",
"en": "Configure git local editor to vim",
},
2026-03-22 22:21:16 -04:00
"Generate git patch to /tmp": {
"fr": "Générer une patch git dans /tmp",
"en": "Generate git patch to /tmp",
},
"Git editor configured to vim successfully!": {
"fr": "Éditeur git configuré sur vim avec succès!",
"en": "Git editor configured to vim successfully!",
},
"Error during configuration: ": {
"fr": "Erreur lors de la configuration : ",
"en": "Error during configuration: ",
},
# GPT code - Claude automation
"Add an automation with Claude in todo.py": {
"fr": "Ajouter une automatisation avec Claude dans todo.py",
"en": "Add an automation with Claude in todo.py",
},
"Description of the command to add: ": {
"fr": "Description de la commande à ajouter : ",
"en": "Description of the command to add: ",
},
"Bash command to execute: ": {
"fr": "Commande bash à exécuter : ",
"en": "Bash command to execute: ",
},
"Menu section (git/code/config/network/process): ": {
"fr": "Section du menu (git/code/config/network/process) : ",
"en": "Menu section (git/code/config/network/process): ",
},
"Automation added successfully in todo.json!": {
"fr": "Automatisation ajoutée avec succès dans todo.json!",
"en": "Automation added successfully in todo.json!",
},
"Error adding automation: ": {
"fr": "Erreur lors de l'ajout de l'automatisation : ",
"en": "Error adding automation: ",
},
# SSH Deploy section
"Deploy - Deploy ERPLibre via SSH": {
"fr": "Déploiement - Déployer ERPLibre via SSH",
"en": "Deploy - Deploy ERPLibre via SSH",
},
"SSH deployment tools!": {
"fr": "Outils de déploiement SSH!",
"en": "SSH deployment tools!",
},
"SSH - Check connection": {
"fr": "🔌 SSH - Vérifier la connexion",
"en": "🔌 SSH - Check connection",
},
"SSH - Sync files (rsync)": {
"fr": "🔄 SSH - Synchroniser les fichiers (rsync)",
"en": "🔄 SSH - Sync files (rsync)",
},
"SSH - Install ERPLibre": {
"fr": "📦 SSH - Installer ERPLibre",
"en": "📦 SSH - Install ERPLibre",
},
"SSH - Start Odoo": {
"fr": "🟢 SSH - Démarrer Odoo",
"en": "🟢 SSH - Start Odoo",
},
"SSH - Stop Odoo": {
"fr": "🔴 SSH - Arrêter Odoo",
"en": "🔴 SSH - Stop Odoo",
},
"SSH - Restart Odoo": {
"fr": "🔁 SSH - Redémarrer Odoo",
"en": "🔁 SSH - Restart Odoo",
},
"SSH - Service status": {
"fr": "📊 SSH - Statut du service",
"en": "📊 SSH - Service status",
},
"SSH - View logs": {
"fr": "📜 SSH - Voir les logs",
"en": "📜 SSH - View logs",
},
"SSH - Run make target": {
"fr": "🧰 SSH - Exécuter une cible make",
"en": "🧰 SSH - Run make target",
},
"SSH - Install systemd service": {
"fr": "🧩 SSH - Installer le service systemd",
"en": "🧩 SSH - Install systemd service",
},
"SSH - Configure nginx + SSL": {
"fr": "🔒 SSH - Configurer nginx + SSL",
"en": "🔒 SSH - Configure nginx + SSL",
},
"Remote host (user@hostname or hostname): ": {
"fr": "Hôte distant (user@hostname ou hostname) : ",
"en": "Remote host (user@hostname or hostname): ",
},
"SSH user (default: erplibre): ": {
"fr": "Utilisateur SSH (défaut: erplibre) : ",
"en": "SSH user (default: erplibre): ",
},
"SSH port (default: 22): ": {
"fr": "Port SSH (défaut: 22) : ",
"en": "SSH port (default: 22): ",
},
"SSH key path (default: ~/.ssh/id_rsa, empty for none): ": {
"fr": "Chemin de la clé SSH (défaut: ~/.ssh/id_rsa, vide pour aucune) : ",
"en": "SSH key path (default: ~/.ssh/id_rsa, empty for none): ",
},
"Remote path (default: ~/erplibre_deploy_2): ": {
"fr": "Chemin distant (défaut: ~/erplibre_deploy_2) : ",
"en": "Remote path (default: ~/erplibre_deploy_2): ",
},
"Make target to run remotely: ": {
"fr": "Cible make à exécuter à distance : ",
"en": "Make target to run remotely: ",
},
"Domain name (e.g.: example.com): ": {
"fr": "Nom de domaine (ex: example.com) : ",
"en": "Domain name (e.g.: example.com): ",
},
"Admin email for SSL certificate: ": {
"fr": "Email administrateur pour le certificat SSL : ",
"en": "Admin email for SSL certificate: ",
},
"SSH host is required!": {
"fr": "L'hôte SSH est requis!",
"en": "SSH host is required!",
},
2026-03-07 02:59:07 -05:00
# Language selection
"Choose language / Choisir la langue": {
2026-03-07 02:59:07 -05:00
"fr": "Choisir la langue / Choose language",
"en": "Choose language / Choisir la langue",
},
"French": {
2026-03-07 02:59:07 -05:00
"fr": "Francais",
"en": "French",
},
"English": {
2026-03-07 02:59:07 -05:00
"fr": "Anglais",
"en": "English",
},
"Language changed to: English": {
2026-03-07 02:59:07 -05:00
"fr": "Langue changée pour : Francais",
"en": "Language changed to: English",
},
"TODO execution time": {
2026-03-07 02:59:07 -05:00
"fr": "Temps d'exécution TODO",
"en": "TODO execution time",
},
"Keyboard interrupt": {
2026-03-07 02:59:07 -05:00
"fr": "Interruption clavier",
"en": "Keyboard interrupt",
},
# GPT code section
"GPT code - AI assistant tools": {
"fr": "🤖 GPT code - Outils d'assistant IA",
"en": "🤖 GPT code - AI assistant tools",
},
"AI assistant tools for development!": {
"fr": "Outils d'assistant IA pour le développement!",
"en": "AI assistant tools for development!",
},
"Configure Claude Code configurations": {
"fr": "Configurer les configurations Claude Code",
"en": "Configure Claude Code configurations",
},
"Commit - OCA/Odoo commit command": {
"fr": "Commit - Commande de commit OCA/Odoo",
"en": "Commit - OCA/Odoo commit command",
},
"Todo Add Command - Add a command to todo.py menu": {
"fr": "Todo Add Command - Ajouter une commande au menu todo.py",
"en": "Todo Add Command - Add a command to todo.py menu",
},
"Enter your full name: ": {
"fr": "Entrez votre nom complet : ",
"en": "Enter your full name: ",
},
"Enter your email: ": {
"fr": "Entrez votre courriel : ",
"en": "Enter your email: ",
},
"Deploy Claude Code commands!": {
"fr": "Déployer les commandes Claude Code!",
"en": "Deploy Claude Code commands!",
},
"Show installed custom commands": {
"fr": "Afficher les commandes personnalisées installées",
"en": "Show installed custom commands",
},
"No custom commands found in ~/.claude/commands/": {
"fr": "Aucune commande personnalisée trouvée dans ~/.claude/commands/",
"en": "No custom commands found in ~/.claude/commands/",
},
"Claude Code custom commands:": {
"fr": "Commandes personnalisées Claude Code :",
"en": "Claude Code custom commands:",
},
"File already exists: ": {
"fr": "Le fichier existe déjà : ",
"en": "File already exists: ",
},
"Do you want to overwrite the file? (y/Y): ": {
"fr": "Voulez-vous écraser le fichier? (y/Y) : ",
"en": "Do you want to overwrite the file? (y/Y): ",
},
"Nothing to do.": {
"fr": "Rien à faire.",
"en": "Nothing to do.",
},
"File created successfully: ": {
"fr": "Fichier créé avec succès : ",
"en": "File created successfully: ",
},
"Error creating file: ": {
"fr": "Erreur lors de la création du fichier : ",
"en": "Error creating file: ",
},
# NTFY section
"Deploy - Install NTFY notification server": {
"fr": "🔔 Déployer - Installer le serveur de notifications NTFY",
"en": "🔔 Deploy - Install NTFY notification server",
},
"Deploy a local NTFY push notification server (Ubuntu/Arch)": {
"fr": "Déployer un serveur local de notifications NTFY (Ubuntu/Arch)",
"en": "Deploy a local NTFY push notification server (Ubuntu/Arch)",
},
"NTFY server port (default: 8080): ": {
"fr": "Port du serveur NTFY (défaut : 8080) : ",
"en": "NTFY server port (default: 8080): ",
},
"NTFY base URL": {
"fr": "URL de base NTFY",
"en": "NTFY base URL",
},
"Installing NTFY server (requires sudo)...": {
"fr": "Installation du serveur NTFY (sudo requis)...",
"en": "Installing NTFY server (requires sudo)...",
},
"NTFY server installed and started successfully!": {
"fr": "Serveur NTFY installé et démarré avec succès!",
"en": "NTFY server installed and started successfully!",
},
"Error installing NTFY server: ": {
"fr": "Erreur lors de l'installation du serveur NTFY : ",
"en": "Error installing NTFY server: ",
},
"NTFY install script not found: ": {
"fr": "Script d'installation NTFY introuvable : ",
"en": "NTFY install script not found: ",
},
# QEMU / KVM (libvirt) VM deployment
"QEMU/KVM - Deploy an Ubuntu VM (libvirt)": {
"fr": "💻 QEMU/KVM - Déployer une VM Ubuntu (libvirt)",
"en": "💻 QEMU/KVM - Deploy an Ubuntu VM (libvirt)",
},
"Deploy a QEMU/KVM virtual machine (libvirt)!": {
"fr": "Déployer une machine virtuelle QEMU/KVM (libvirt) !",
"en": "Deploy a QEMU/KVM virtual machine (libvirt)!",
},
"QEMU deploy script not found: ": {
"fr": "Script de déploiement QEMU introuvable : ",
"en": "QEMU deploy script not found: ",
},
"Deploy a new VM": {
"fr": "Déployer une nouvelle VM",
"en": "Deploy a new VM",
},
"Preview a deployment (dry-run, no sudo)": {
"fr": "🔍 Prévisualiser un déploiement (dry-run, sans sudo)",
"en": "🔍 Preview a deployment (dry-run, no sudo)",
},
"Download a cloud image only": {
"fr": "⬇ Télécharger seulement une image cloud",
"en": "⬇ Download a cloud image only",
},
"List VMs (virsh list --all)": {
"fr": "📋 Lister les VM (virsh list --all)",
"en": "📋 List VMs (virsh list --all)",
},
"Show a VM IP address": {
"fr": "🌐 Afficher l'adresse IP d'une VM",
"en": "🌐 Show a VM IP address",
},
"List available images and specs": {
"fr": "🗂 Lister les images disponibles et leurs specs",
"en": "🗂 List available images and specs",
},
"Distribution:": {
"fr": "Distribution :",
"en": "Distribution:",
},
"Choice (number or name, default: ubuntu): ": {
"fr": "Choix (numéro ou nom, défaut : ubuntu) : ",
"en": "Choice (number or name, default: ubuntu): ",
},
"Invalid selection, using ubuntu": {
"fr": "Sélection invalide, utilisation d'ubuntu",
"en": "Invalid selection, using ubuntu",
},
"Version for": {
"fr": "Version des",
"en": "Version for",
},
"Versions for": {
"fr": "Versions des",
"en": "Versions of",
},
"Architecture:": {
"fr": "Architecture :",
"en": "Architecture:",
},
"native": {
"fr": "native",
"en": "native",
},
"IBM Z — emulated, slow on x86; Ubuntu only": {
"fr": "IBM Z — émulé, lent sur x86 ; Ubuntu uniquement",
"en": "IBM Z — emulated, slow on x86; Ubuntu only",
},
"IBM Z — emulated, slow; Ubuntu only": {
"fr": "IBM Z — émulé, lent ; Ubuntu uniquement",
"en": "IBM Z — emulated, slow; Ubuntu only",
},
"ARM 64-bit — emulated, slow": {
"fr": "ARM 64 bits — émulé, lent",
"en": "ARM 64-bit — emulated, slow",
},
"emulated, slow": {
"fr": "émulé, lent",
"en": "emulated, slow",
},
"This architecture is emulated (TCG): boot and install are"
" much slower than the native one.": {
"fr": "Cette architecture est émulée (TCG) : le boot et l'installation"
" sont bien plus lents que l'architecture native.",
"en": "This architecture is emulated (TCG): boot and install are"
" much slower than the native one.",
},
"images for this arch only exist for:": {
"fr": "images pour cette architecture disponibles seulement pour :",
"en": "images for this arch only exist for:",
},
"All supported architectures": {
"fr": "Toutes les architectures supportées",
"en": "All supported architectures",
},
"installation": {
"fr": "installation",
"en": "installation",
},
"Resources per VM (x1 = catalog minimum):": {
"fr": "Ressources par VM (x1 = minimum catalogue) :",
"en": "Resources per VM (x1 = catalog minimum):",
},
"Host:": {
"fr": "Hôte :",
"en": "Host:",
},
"free": {
"fr": "libres",
"en": "free",
},
"free RAM unknown": {
"fr": "RAM libre inconnue",
"en": "free RAM unknown",
},
"> host free RAM": {
"fr": "> RAM libre de l'hôte",
"en": "> host free RAM",
},
"total RAM": {
"fr": "RAM totale",
"en": "total RAM",
},
"Choice (1-4, default 1):": {
"fr": "Choix (1-4, défaut 1) :",
"en": "Choice (1-4, default 1):",
},
"Choice (1-5, default 1):": {
"fr": "Choix (1-5, défaut 1) :",
"en": "Choice (1-5, default 1):",
},
"Custom - set vCPU, RAM and disk": {
"fr": "Personnalisé — choisir vCPU, RAM et disque",
"en": "Custom — set vCPU, RAM and disk",
},
"custom": {
"fr": "personnalisé",
"en": "custom",
},
"varies": {
"fr": "varié",
"en": "varies",
},
"vCPU per VM, blank = keep": {
"fr": "vCPU par VM, vide = garder",
"en": "vCPU per VM, blank = keep",
},
"New vCPU count, blank = keep": {
"fr": "Nouveau nombre de vCPU, vide = garder",
"en": "New vCPU count, blank = keep",
},
"Invalid vCPU count.": {
"fr": "Nombre de vCPU invalide.",
"en": "Invalid vCPU count.",
},
"More vCPU than host cores": {
"fr": "Plus de vCPU que de cœurs sur l'hôte",
"en": "More vCPU than host cores",
},
"> host cores": {
"fr": "> cœurs de l'hôte",
"en": "> host cores",
},
"Total vCPU (all running):": {
"fr": "vCPU total (toutes démarrées) :",
"en": "Total vCPU (all running):",
},
"Deploy VM(s) (one or many)": {
"fr": "🚀 Déployer une ou plusieurs VM",
"en": "🚀 Deploy VM(s) (one or many)",
},
"Deploy ERPLibre VM(s)!": {
"fr": "Déployer une ou plusieurs VM ERPLibre !",
"en": "Deploy ERPLibre VM(s)!",
},
"VM names (default = auto):": {
"fr": "Noms des VM (défaut = auto) :",
"en": "VM names (default = auto):",
},
"Rename which VMs? (numbers, comma-separated; blank = none): ": {
"fr": "Renommer quelles VM ? (numéros séparés par des virgules ; "
"vide = aucune) : ",
"en": "Rename which VMs? (numbers, comma-separated; blank = none): ",
},
"->": {
"fr": "->",
"en": "->",
},
"VMs (default = no change):": {
"fr": "VM (défaut = aucune modification) :",
"en": "VMs (default = no change):",
},
"Modify which VMs? (numbers, comma-separated; blank = none): ": {
"fr": "Modifier quelles VM ? (numéros séparés par des virgules ; "
"vide = aucune) : ",
"en": "Modify which VMs? (numbers, comma-separated; blank = none): ",
},
"new name (blank = keep):": {
"fr": "nouveau nom (vide = garder) :",
"en": "new name (blank = keep):",
},
"New disk size in G, blank = keep": {
"fr": "Nouvelle taille disque en G, vide = garder",
"en": "New disk size in G, blank = keep",
},
"New RAM in MB, blank = keep": {
"fr": "Nouvelle RAM en Mo, vide = garder",
"en": "New RAM in MB, blank = keep",
},
"Duplicate names detected; keeping as entered.": {
"fr": "Noms en double détectés ; conservés tels quels.",
"en": "Duplicate names detected; keeping as entered.",
},
"Preview (dry-run):": {
"fr": "Aperçu (dry-run) :",
"en": "Preview (dry-run):",
},
"(includes emulated architectures — some VMs are slow)": {
"fr": "(inclut des architectures émulées — certaines VM sont lentes)",
"en": "(includes emulated architectures — some VMs are slow)",
},
"install monitoring": {
"fr": "suivi d'installation",
"en": "install monitoring",
},
"Navigation telemetry (TUI)": {
"fr": "📊 Télémétrie de navigation (TUI)",
"en": "📊 Navigation telemetry (TUI)",
},
"Configuration": {
"fr": "⚙ Configuration",
"en": "⚙ Configuration",
},
"Interface": {
"fr": "Interface",
"en": "Interface",
},
"Maintenance": {
"fr": "Maintenance",
"en": "Maintenance",
},
"Language / Langue": {
"fr": "🌐 Langue / Language",
"en": "🌐 Language / Langue",
},
"QEMU deployment interface": {
"fr": "🖥 Interface de déploiement QEMU",
"en": "🖥 QEMU deployment interface",
},
"Display while deploying": {
"fr": "📜 Affichage pendant le déploiement",
"en": "📜 Display while deploying",
},
"Ask every time": {
"fr": "Demander à chaque fois",
"en": "Ask every time",
},
"TUI form": {
"fr": "Formulaire TUI",
"en": "TUI form",
},
"Classic questions (line by line)": {
"fr": "Questions classiques (ligne par ligne)",
"en": "Classic questions (line by line)",
},
"CLI output (easy to copy)": {
"fr": "Sortie CLI (facile à copier)",
"en": "CLI output (easy to copy)",
},
"TUI, collapsible blocks per VM": {
"fr": "TUI, blocs repliables par VM",
"en": "TUI, collapsible blocks per VM",
},
"Choice (number, blank = keep):": {
"fr": "Choix (numéro, vide = garder) :",
"en": "Choice (number, blank = keep):",
},
"SSH port forwarding (open Odoo in the browser)": {
"fr": "🔌 Redirection de port SSH (ouvrir Odoo dans le navigateur)",
"en": "🔌 SSH port forwarding (open Odoo in the browser)",
},
"SSH port forwarding": {
"fr": "Redirection de port SSH",
"en": "SSH port forwarding",
},
"Host (number or name):": {
"fr": "Hôte (numéro ou nom) :",
"en": "Host (number or name):",
},
"Remote port (default:": {
"fr": "Port distant (défaut :",
"en": "Remote port (default:",
},
"Local port (default:": {
"fr": "Port local (défaut :",
"en": "Local port (default:",
},
"virsh is missing: libvirt is not installed here.": {
"fr": "virsh est absent : libvirt n'est pas installé ici.",
"en": "virsh is missing: libvirt is not installed here.",
},
"Every VM command will fail until it is.": {
"fr": "Toutes les commandes VM échoueront tant que ce sera le cas.",
"en": "Every VM command will fail until it is.",
},
"Install the QEMU/libvirt tools now? (Y/n): ": {
"fr": "Installer les outils QEMU/libvirt maintenant ? "
"(O/n, défaut : oui) : ",
"en": "Install the QEMU/libvirt tools now? (Y/n, default: yes): ",
},
"libvirt is available.": {
"fr": "libvirt est disponible.",
"en": "libvirt is available.",
},
"virsh still missing; a reboot may be required.": {
"fr": "virsh toujours absent ; un redémarrage est peut-être requis.",
"en": "virsh still missing; a reboot may be required.",
},
"No SSH public key found in ~/.ssh.": {
"fr": "Aucune clé publique SSH trouvée dans ~/.ssh.",
"en": "No SSH public key found in ~/.ssh.",
},
"Without one the VMs start with no SSH access.": {
"fr": "Sans elle, les VM démarrent sans accès SSH.",
"en": "Without one the VMs start with no SSH access.",
},
"Generate one now? (Y/n): ": {
"fr": "En générer une maintenant ? (O/n, défaut : oui) : ",
"en": "Generate one now? (Y/n, default: yes): ",
},
"Checking the remote port...": {
"fr": "Vérification du port distant...",
"en": "Checking the remote port...",
},
"Nothing is listening on port": {
"fr": "Rien n'écoute sur le port",
"en": "Nothing is listening on port",
},
"of": {
"fr": "de",
"en": "of",
},
"Start the service there, or continue anyway.": {
"fr": "Démarrer le service là-bas, ou continuer quand même.",
"en": "Start the service there, or continue anyway.",
},
"Continue anyway? (y/N): ": {
"fr": "Continuer quand même ? (o/N, défaut : non) : ",
"en": "Continue anyway? (y/N, default: no): ",
},
"Could not probe the remote port; going on.": {
"fr": "Impossible de sonder le port distant ; on continue.",
"en": "Could not probe the remote port; going on.",
},
"Local port already in use:": {
"fr": "Port local déjà occupé :",
"en": "Local port already in use:",
},
"Try anyway? (y/N): ": {
"fr": "Essayer quand même ? (o/N, défaut : non) : ",
"en": "Try anyway? (y/N, default: no): ",
},
"Local port differs from the remote one.": {
"fr": "Le port local diffère du port distant.",
"en": "Local port differs from the remote one.",
},
"Odoo redirects using web.base.url; check it": {
"fr": "Odoo redirige d'après web.base.url ; vérifier qu'il",
"en": "Odoo redirects using web.base.url; check it",
},
"matches http://localhost:": {
"fr": "vaut bien http://localhost:",
"en": "matches http://localhost:",
},
"Ctrl+C closes the tunnel.": {
"fr": "Ctrl+C referme le tunnel.",
"en": "Ctrl+C closes the tunnel.",
},
"Tunnel closed.": {
"fr": "Tunnel refermé.",
"en": "Tunnel closed.",
},
"SSH configuration (~/.ssh/config, ProxyJump)": {
"fr": "🔑 Configuration SSH (~/.ssh/config, ProxyJump)",
"en": "🔑 SSH configuration (~/.ssh/config, ProxyJump)",
},
"SSH configuration for QEMU VMs": {
"fr": "Configuration SSH des VM QEMU",
"en": "SSH configuration for QEMU VMs",
},
"Where should the machines come from?": {
"fr": "D'où viennent les machines à configurer ?",
"en": "Where should the machines come from?",
},
"Local QEMU VMs (virsh)": {
"fr": "VM QEMU locales (virsh)",
"en": "Local QEMU VMs (virsh)",
},
"Hosts from ~/.ssh/config": {
"fr": "Hôtes de ~/.ssh/config",
"en": "Hosts from ~/.ssh/config",
},
"Type a host or an IP": {
"fr": "Saisir un hôte ou une IP",
"en": "Type a host or an IP",
},
"~/.ssh/config holds no host.": {
"fr": "~/.ssh/config ne contient aucun hôte.",
"en": "~/.ssh/config holds no host.",
},
"Which hosts? (numbers, comma-separated; blank = all): ": {
"fr": "Quels hôtes ? (numéros séparés par des virgules ; "
"vide = tous) : ",
"en": "Which hosts? (numbers, comma-separated; blank = all): ",
},
"Host or IP:": {
"fr": "Hôte ou IP :",
"en": "Host or IP:",
},
"User": {
"fr": "Utilisateur",
"en": "User",
},
"Name for ~/.ssh/config": {
"fr": "Nom pour ~/.ssh/config",
"en": "Name for ~/.ssh/config",
},
"Depth (1 = these machines only, default:": {
"fr": "Profondeur (1 = ces machines seulement, défaut :",
"en": "Depth (1 = these machines only, default:",
},
"Which VMs? (numbers, comma-separated; blank = all): ": {
"fr": "Quelles VM ? (numéros séparés par des virgules ; "
"vide = toutes) : ",
"en": "Which VMs? (numbers, comma-separated; blank = all): ",
},
"Depth (default:": {
"fr": "Profondeur (défaut :",
"en": "Depth (default:",
},
"entry": {
"fr": "entrée",
"en": "entry",
},
"written": {
"fr": "écrite(s)",
"en": "written",
},
"skipped": {
"fr": "ignorée(s)",
"en": "skipped",
},
"Level": {
"fr": "Niveau",
"en": "Level",
},
"machines to probe": {
"fr": "machines à sonder",
"en": "machines to probe",
},
"SSH refused the identity.": {
"fr": "SSH a refusé l'identité.",
"en": "SSH refused the identity.",
},
"Existing key:": {
"fr": "Clé existante :",
"en": "Existing key:",
},
"Deploy it on this host (ssh-copy-id)? (Y/n): ": {
"fr": "La déployer sur cet hôte (ssh-copy-id) ? "
"(O/n, défaut : oui) : ",
"en": "Deploy it on this host (ssh-copy-id)? (Y/n, default: yes): ",
},
"No SSH key in ~/.ssh.": {
"fr": "Aucune clé SSH dans ~/.ssh.",
"en": "No SSH key in ~/.ssh.",
},
"Create one and deploy it? (Y/n): ": {
"fr": "En créer une et la déployer ? (O/n, défaut : oui) : ",
"en": "Create one and deploy it? (Y/n, default: yes): ",
},
"access refused": {
"fr": "accès refusé",
"en": "access refused",
},
"timed out": {
"fr": "délai dépassé",
"en": "timed out",
},
"unreachable": {
"fr": "injoignable",
"en": "unreachable",
},
"no QEMU/libvirt here": {
"fr": "pas de QEMU/libvirt ici",
"en": "no QEMU/libvirt here",
},
"virsh present but not accessible": {
"fr": "virsh présent mais inaccessible (droits)",
"en": "virsh present but not accessible (permissions)",
},
"Add the user to the libvirt group there:": {
"fr": "Ajoutez l'utilisateur au groupe libvirt là-bas :",
"en": "Add the user to the libvirt group there:",
},
"QEMU present, no VM": {
"fr": "QEMU présent, aucune VM",
"en": "QEMU present, no VM",
},
"Create the SSH key if missing and deploy it? (Y/n): ": {
"fr": "Créer la clé SSH si absente et la déployer ? "
"(O/n, défaut : oui) : ",
"en": "Create the SSH key if missing and deploy it? "
"(Y/n, default: yes): ",
},
"Generating an ed25519 SSH key": {
"fr": "Génération d'une clé SSH ed25519",
"en": "Generating an ed25519 SSH key",
},
"Cannot generate the key": {
"fr": "Impossible de générer la clé",
"en": "Cannot generate the key",
},
"Deploying the key on": {
"fr": "Déploiement de la clé sur",
"en": "Deploying the key on",
},
"host": {
"fr": "hôte",
"en": "host",
},
"key already accepted": {
"fr": "clé déjà acceptée",
"en": "key already accepted",
},
"deployed": {
"fr": "déployée(s)",
"en": "deployed",
},
"already there": {
"fr": "déjà en place",
"en": "already there",
},
"virt-manager detected": {
"fr": "virt-manager détecté",
"en": "virt-manager detected",
},
"virt-manager: every connection already there": {
"fr": "virt-manager : toutes les connexions sont déjà là",
"en": "virt-manager: every connection already there",
},
"Add the missing connections to virt-manager? (Y/n): ": {
"fr": "Ajouter les connexions manquantes à virt-manager ? "
"(O/n, défaut : oui) : ",
"en": "Add the missing connections to virt-manager? "
"(Y/n, default: yes): ",
},
"Restart virt-manager to see the new connections": {
"fr": "Redémarrer virt-manager pour voir les nouvelles connexions",
"en": "Restart virt-manager to see the new connections",
},
"the names apply live": {
"fr": "les noms s'appliquent à chaud",
"en": "the names apply live",
},
"SSH hosts written": {
"fr": "Hôtes SSH écrits",
"en": "SSH hosts written",
},
"via": {
"fr": "via",
"en": "via",
},
"Odoo migration interface": {
"fr": "🚚 Interface de la migration Odoo",
"en": "🚚 Odoo migration interface",
},
"modules of the list have no code in the active Odoo:": {
"fr": "modules de la liste n'ont plus de code dans l'Odoo actif :",
"en": "modules of the list have no code in the active Odoo:",
},
"Odoo cannot uninstall a module whose code is gone;": {
"fr": "Odoo ne peut pas désinstaller un module dont le code a"
" disparu ;",
"en": "Odoo cannot uninstall a module whose code is gone;",
},
"one of them fails the whole uninstall command.": {
"fr": "un seul d'entre eux fait échouer toute la désinstallation.",
"en": "one of them fails the whole uninstall command.",
},
"are present and can be uninstalled:": {
"fr": "sont présents et peuvent être désinstallés :",
"en": "are present and can be uninstalled:",
},
"No module of the list is present.": {
"fr": "Aucun module de la liste n'est présent.",
"en": "No module of the list is present.",
},
"Uninstall the present ones, skip the missing": {
"fr": "Désinstaller les présents, ignorer les manquants",
"en": "Uninstall the present ones, skip the missing",
},
"Try the whole list anyway (it will fail)": {
"fr": "Tout tenter quand même (ça échouera)",
"en": "Try the whole list anyway (it will fail)",
},
"Uninstall nothing, continue": {
"fr": "Ne rien désinstaller, continuer",
"en": "Uninstall nothing, continue",
},
"Nothing uninstalled.": {
"fr": "Rien de désinstallé.",
"en": "Nothing uninstalled.",
},
"Check the COW views that drifted": {
"fr": "Vérifier les vues COW en retard sur leur vue module",
"en": "Check the COW views that drifted",
},
"Tool not found": {
"fr": "Outil introuvable",
"en": "Tool not found",
},
"To reset one of them onto its module view:": {
"fr": "Pour réinitialiser l'une d'elles sur sa vue module :",
"en": "To reset one of them onto its module view:",
},
"Read the diff first: a copy can hold a customisation.": {
"fr": "Lire le diff d'abord : une copie peut porter une"
" personnalisation.",
"en": "Read the diff first: a copy can hold a customisation.",
},
"Migration statistics (read-only)": {
"fr": "📊 Statistiques de la migration (lecture seule)",
"en": "📊 Migration statistics (read-only)",
},
"Choice (0-3, default 1): ": {
"fr": "Choix (0-3, défaut 1) : ",
"en": "Choice (0-3, default 1): ",
},
"Migration statistics": {
"fr": "Statistiques de la migration",
"en": "Migration statistics",
},
"elapsed": {
"fr": "écoulé",
"en": "elapsed",
},
"Level reached": {
"fr": "Niveau atteint",
"en": "Level reached",
},
"Modules": {
"fr": "Modules",
"en": "Modules",
},
"modules": {
"fr": "modules",
"en": "modules",
},
"At the start": {
"fr": "Au départ",
"en": "At the start",
},
"Removed in total": {
"fr": "Supprimés au total",
"en": "Removed in total",
},
"Reported missing": {
"fr": "Signalés manquants",
"en": "Reported missing",
},
"Duplicated": {
"fr": "En double",
"en": "Duplicated",
},
"Migration fixes": {
"fr": "Correctifs de migration",
"en": "Migration fixes",
},
"COW views": {
"fr": "Vues COW",
"en": "COW views",
},
"views": {
"fr": "vues",
"en": "views",
},
"no snapshot": {
"fr": "aucun instantané",
"en": "no snapshot",
},
"Journal": {
"fr": "Journal",
"en": "Journal",
},
"commands": {
"fr": "commandes",
"en": "commands",
},
"recorded decisions": {
"fr": "décisions annotées",
"en": "recorded decisions",
},
"Removed modules, with their reason": {
"fr": "Modules supprimés, avec leur justification",
"en": "Removed modules, with their reason",
},
"Removed modules, comma-separated (copy)": {
"fr": "Modules supprimés, séparés par des virgules (à copier)",
"en": "Removed modules, comma-separated (copy)",
},
"COW views: snapshots and differences": {
"fr": "Vues COW : instantanés et différences",
"en": "COW views: snapshots and differences",
},
"Recorded decisions (journal)": {
"fr": "Décisions annotées (journal)",
"en": "Recorded decisions (journal)",
},
"Executed commands (last 30)": {
"fr": "Commandes exécutées (30 dernières)",
"en": "Executed commands (last 30)",
},
"nothing recorded": {
"fr": "rien d'annoté",
"en": "nothing recorded",
},
"Need two snapshots to diff.": {
"fr": "Il faut deux instantanés pour comparer.",
"en": "Need two snapshots to diff.",
},
"Diff which two? (e.g. 1,2 — blank to skip)": {
"fr": "Comparer lesquels ? (ex. 1,2 — vide pour passer)",
"en": "Diff which two? (e.g. 1,2 — blank to skip)",
},
"No migration in progress to resume.": {
"fr": "Aucune migration en cours à reprendre.",
"en": "No migration in progress to resume.",
},
"Quit without doing anything": {
"fr": "Quitter sans rien faire",
"en": "Quit without doing anything",
},
"Version bumps": {
"fr": "Montées de version",
"en": "Version bumps",
},
"Step": {
"fr": "Étape",
"en": "Step",
},
"Detail": {
"fr": "Détail",
"en": "Detail",
},
"Enter on a step or a version = replay from there": {
"fr": "Entrée sur une étape ou une version = rejouer depuis là",
"en": "Enter on a step or a version = replay from there",
},
"New migration": {
"fr": "Nouvelle migration",
"en": "New migration",
},
"Keep the zip only": {
"fr": "Garder seulement le zip",
"en": "Keep the zip only",
},
"Reset all preferences": {
"fr": "🧹 Réinitialiser toutes les préférences",
"en": "🧹 Reset all preferences",
},
"Preferences reset": {
"fr": "Préférences réinitialisées",
"en": "Preferences reset",
},
"Interface:": {
"fr": "Interface :",
"en": "Interface:",
},
"(change the default in TODO > Configuration)": {
"fr": "(changer le défaut dans TODO > Configuration)",
"en": "(change the default in TODO > Configuration)",
},
"Loading (VM list, branches)...": {
"fr": "Chargement (liste des VM, branches)...",
"en": "Loading (VM list, branches)...",
},
"Architecture": {
"fr": "Architecture",
"en": "Architecture",
},
"Resources per VM": {
"fr": "Ressources par VM",
"en": "Resources per VM",
},
"Install ERPLibre": {
"fr": "Installer ERPLibre",
"en": "Install ERPLibre",
},
"Production (/opt, confined)": {
"fr": "Production (/opt, confiné)",
"en": "Production (/opt, confined)",
},
"Monitoring dashboard": {
"fr": "Tableau de bord de suivi",
"en": "Monitoring dashboard",
},
"Add each VM to ~/.ssh/config": {
"fr": "Ajouter chaque VM à ~/.ssh/config",
"en": "Add each VM to ~/.ssh/config",
},
"Parallelism": {
"fr": "Parallélisme",
"en": "Parallelism",
},
"Name": {
"fr": "Nom",
"en": "Name",
},
"Distro": {
"fr": "Distro",
"en": "Distro",
},
"Version": {
"fr": "Version",
"en": "Version",
},
"Arch": {
"fr": "Archi",
"en": "Arch",
},
"vCPU": {
"fr": "vCPU",
"en": "vCPU",
},
"RAM (MB)": {
"fr": "RAM (Mo)",
"en": "RAM (MB)",
},
[FIX] qemu monitor: follow the DHCP lease, and keep the log talking Monitoring stayed on the same address for 1170 s, never catching up with the VM. Two holes, in the very re-resolution meant to prevent that. The lease fallback required an answer on port 22. But dnsmasq keeps one lease per MAC: when cloud-init sets the real hostname and the DHCP client asks again, the lease MOVES the address. The old one no longer belongs to the VM and sshd will never answer there. The lease therefore wins as soon as it stops listing the current address. The other hole explains the silence: virsh was muted on both branches, so an unreachable libvirt kept the initial IP without a single line saying so. The log went quiet for a quarter of an hour for the same reason -- cloud-init holds the package lock while writing nothing. --- FR --- Le suivi restait sur la même adresse pendant 1170 s, sans jamais rattraper la VM. Deux trous, dans la re-résolution censée l'éviter. Le repli par bail exigeait une réponse sur le port 22. Or dnsmasq garde un bail par MAC : quand cloud-init pose le vrai nom d'hôte et que le client DHCP redemande, le bail DÉPLACE l'adresse. L'ancienne n'appartient plus à la VM et sshd n'y répondra jamais. Le bail l'emporte donc dès qu'il cesse de lister l'adresse courante. L'autre trou explique le silence : virsh était muet sur les deux branches, si bien qu'un libvirt injoignable conservait l'IP initiale sans une ligne pour le dire. Le log se taisait un quart d'heure pour la même raison — cloud-init tient le verrou des paquets sans rien écrire. Assisted-by: Claude Opus 5
2026-08-11 08:36:13 -04:00
"cloud-init still running after 20 min - install starts anyway"
" (it waits for cloud-init first)": {
"fr": "cloud-init tourne encore apres 20 min - l'installation demarre"
" quand meme (elle attend d'abord cloud-init)",
"en": "cloud-init still running after 20 min - install starts anyway"
" (it waits for cloud-init first)",
},
"Waiting for cloud-init to finish (up to 15 min)": {
"fr": "Attente de la fin de cloud-init (jusqu'a 15 min)",
"en": "Waiting for cloud-init to finish (up to 15 min)",
},
"cloud-init:": {
"fr": "cloud-init :",
"en": "cloud-init:",
},
"WARNING libvirt unreachable: the IP will not be refreshed"
" (libvirt group? re-login required)": {
"fr": "ATTENTION libvirt injoignable : l'IP ne sera pas rafraichie"
" (groupe libvirt ? reconnexion necessaire)",
"en": "WARNING libvirt unreachable: the IP will not be refreshed"
" (libvirt group? re-login required)",
},
"DHCP lease moved:": {
"fr": "bail DHCP deplace :",
"en": "DHCP lease moved:",
},
[ADD] tui qemu: resume an installation already running Installs run detached (setsid -f): closing the terminal does not stop them, but it lost the only view onto them. With no way to resume, the only way out was deleting the VMs and starting over. Picking "Deploy" now looks at the latest run: if VMs there still lack an exit marker, it offers to reopen its monitoring instead of starting another. Time since the last write is shown, a dead run being otherwise indistinguishable from a live one. Only the latest run is examined: an old one left without a marker would flag a phantom install forever. Dry-run creates nothing, so it does not ask. "Reopen monitoring" moves to the Deployment section, where one looks for it. --- FR --- Les installs partent détachées (setsid -f) : fermer le terminal ne les arrête pas, mais faisait perdre la seule vue dessus. Sans moyen de reprendre, la seule issue était d'effacer les VM et de recommencer. Choisir « Déployer » regarde donc le dernier run : s'il lui reste des VM sans marqueur de sortie, il propose de rouvrir son suivi plutôt que d'en lancer un autre. Le silence depuis la dernière écriture est affiché, un run mort n'étant pas distinguable autrement d'un run vivant. Seul le dernier run est examiné : un run ancien laissé sans marqueur signalerait éternellement une install fantôme. L'aperçu ne crée rien, il ne pose pas la question. « Rouvrir le suivi » rejoint la section Déploiement, où on le cherche. Assisted-by: Claude Opus 5
2026-08-11 07:52:52 -04:00
"An install is still running:": {
"fr": "Une installation est encore en cours :",
"en": "An install is still running:",
},
"VM(s) in progress": {
"fr": "VM en cours",
"en": "VM(s) in progress",
},
"Last activity:": {
"fr": "Dernière activité il y a :",
"en": "Last activity:",
},
"Reopen that monitoring": {
"fr": "Rouvrir ce suivi",
"en": "Reopen that monitoring",
},
"Deploy anyway (new run)": {
"fr": "Déployer quand même (nouveau run)",
"en": "Deploy anyway (new run)",
},
"Choice (number, blank = reopen): ": {
"fr": "Choix (numéro, vide = rouvrir) : ",
"en": "Choice (number, blank = reopen): ",
},
[ADD] tui qemu: choose mise or pyenv at deploy time The interpreter provider is now chosen when creating a VM, in both interfaces. "mise" installs it in the VM and lays down a precompiled CPython; "pyenv" keeps today's build-from-source. The question is only asked where it means something: mise publishes binaries for amd64 and arm64 only. An all-s390x fleet never sees it, a mixed fleet sees it along with the architectures that will fall back to pyenv -- said before deploying, not discovered in a log an hour later. mise goes into /usr/local/bin rather than ~/.local/bin: the remote command runs through "ssh host 'command'", where neither ~/.profile nor ~/.bashrc is read. Choosing "mise" exports "auto", not "mise", so a failed install can still fall back to pyenv. The unavailability notice was translated as "pyenv required", which said the opposite of what happens: pyenv is the fallback, not a demand. --- FR --- Le fournisseur d'interpréteur se choisit désormais à la création d'une VM, dans les deux interfaces. « mise » l'installe dans la VM et pose un CPython précompilé ; « pyenv » garde la compilation d'aujourd'hui. La question n'est posée que là où elle a un sens : mise ne publie de binaires que pour amd64 et arm64. Un parc tout s390x ne la voit jamais, un parc mixte la voit avec les architectures qui retomberont sur pyenv — dit avant de déployer, pas découvert dans un log une heure plus tard. mise va dans /usr/local/bin plutôt que ~/.local/bin : la commande distante passe par « ssh hôte 'commande' », où ni ~/.profile ni ~/.bashrc ne sont lus. Choisir « mise » exporte « auto », pas « mise », pour qu'une installation ratée puisse encore retomber sur pyenv. L'avis d'indisponibilité était traduit par « pyenv exigé », qui disait l'inverse de ce qui se passe : pyenv est le repli, pas une exigence. Assisted-by: Claude Opus 5
2026-08-11 22:11:23 -04:00
"Python interpreter:": {
"fr": "Interpreteur Python :",
"en": "Python interpreter:",
},
"mise (precompiled, faster)": {
"fr": "mise (precompile, plus rapide)",
"en": "mise (precompiled, faster)",
},
"pyenv (compiles from source)": {
"fr": "pyenv (compile depuis les sources)",
"en": "pyenv (compiles from source)",
},
"Choice (number, blank = mise): ": {
"fr": "Choix (numero, vide = mise) : ",
"en": "Choice (number, blank = mise): ",
},
"mise has no binary for:": {
"fr": "mise ne publie pas de binaire pour :",
"en": "mise has no binary for:",
},
"those VMs use pyenv": {
"fr": "ces VM utiliseront pyenv",
"en": "those VMs use pyenv",
},
"Installing mise (precompiled Python)": {
"fr": "Installation de mise (Python precompile)",
"en": "Installing mise (precompiled Python)",
},
[ADD] tui qemu: graphical VMs, with a GNOME desktop VMs could only be servers. A server-or-graphical choice joins both interfaces, installing GNOME along with remote access to it. Packages come from the remote command, not cloud-init: their 1 to 2 GB would stretch an already long boot there while leaving no trace in the monitoring, and group installs do not go through it. The desktop therefore does not depend on ERPLibre -- a VM may be wanted graphical and bare. Each distribution has its own names, taken from the source: Arch has no xrdp in its official repositories and takes TigerVNC. The SPICE display is set only on amd64 and arm64; s390x does expose virtio-gpu-ccw, but nothing guarantees its kernel's DRM driver, whereas remote desktop works everywhere. --- FR --- Les VM ne pouvaient être que des serveurs. Un choix serveur ou graphique s'ajoute aux deux interfaces, et pose GNOME avec son accès distant. Les paquets viennent de la commande distante, pas de cloud-init : leurs 1 à 2 Go y allongeraient un démarrage déjà long sans laisser de trace dans le suivi, et les installations par groupe n'y passent pas. Le bureau ne dépend donc pas d'ERPLibre — une VM peut être voulue graphique et nue. Chaque distribution a ses noms, relevés à la source : Arch n'a pas xrdp dans ses dépôts officiels et prend TigerVNC. L'écran virtuel SPICE n'est posé que sur amd64 et arm64 ; s390x expose bien virtio-gpu-ccw, mais rien ne garantit le pilote DRM de son noyau, alors que le bureau distant, lui, marche partout. Assisted-by: Claude Opus 5
2026-08-11 09:49:17 -04:00
"VM type:": {
"fr": "Type de VM :",
"en": "VM type:",
},
"Server (no graphical interface)": {
"fr": "Serveur (sans interface graphique)",
"en": "Server (no graphical interface)",
},
"Graphical (server + GNOME desktop)": {
"fr": "Graphique (serveur + bureau GNOME)",
"en": "Graphical (server + GNOME desktop)",
},
"server": {
"fr": "serveur",
"en": "server",
},
[UPD] support: drop Ubuntu 20.04/22.04, add AlmaLinux and Rocky Ubuntu 20.04 and 22.04 leave EVERY architecture, not just s390x. pikepdf needs qpdf 12.2, whose build requires C++20, while focal ships GCC 9 and publishes no g++-10 for s390x at all. Python 3.8, node 10, cargo 0.67 and OpenSSL 1.1.1 each had a workaround; the pile of them did not. 18.04 follows, already off the lists. The refusal lands before any apt, this script also serving existing machines. AlmaLinux 9 and 10, Rocky 9 and 10 join the catalog on all four architectures: the twelve "latest" URLs were opened, with no index to parse unlike Fedora. They would have booted unreachable though -- the cloud-config forced "groups: users, sudo", but the RHEL family has no sudo group, only wheel, and an unknown group makes useradd fail, hence no password and no key. The very trap already known for Debian, repeated elsewhere. Host side, EPEL and CRB are enabled: without them most -devel packages are missing, silently. The server / graphical choice gains Cinnamon, the Linux Mint desktop, from the distribution's own repositories. Mint's repository is set aside: plain HTTP, and i386/amd64 only, which would rule out arm64 and s390x. Along the way, dnf now installs an ENVIRONMENT rather than a group -- "gnome-desktop" brings gdm and gnome-shell but not base-x, hence no X server. --- FR --- Ubuntu 20.04 et 22.04 partent de TOUTES les architectures, pas seulement de s390x. pikepdf réclame qpdf 12.2, dont la compilation exige C++20, quand focal livre GCC 9 et ne publie même pas de g++-10 pour s390x. Python 3.8, node 10, cargo 0.67 et OpenSSL 1.1.1 avaient chacun leur contournement ; leur accumulation, non. 18.04 suit, déjà hors des listes. Le refus tombe avant tout apt, ce script servant aussi les machines existantes. AlmaLinux 9 et 10, Rocky 9 et 10 entrent au catalogue, sur les quatre architectures : les douze URL « latest » ont été ouvertes, aucun index à analyser contrairement à Fedora. Elles auraient pourtant démarré inaccessibles — le cloud-config imposait « groups: users, sudo », or la famille RHEL n'a pas de groupe sudo mais wheel, et un groupe inconnu fait échouer useradd, donc ni mot de passe ni clé. C'est le piège déjà connu pour Debian, reproduit ailleurs. Côté hôte, EPEL et CRB sont activés : sans eux la plupart des -devel manquent, en silence. Le choix serveur / graphique gagne Cinnamon, le bureau de Linux Mint, depuis les dépôts de la distribution. Le dépôt de Mint lui-même est écarté : il est en HTTP nu et ne publie que i386 et amd64, ce qui exclurait arm64 et s390x. Au passage, dnf installe désormais un ENVIRONNEMENT et non un groupe — « gnome-desktop » apporte gdm et gnome-shell mais pas base-x, donc pas de serveur X. Assisted-by: Claude Opus 5
2026-08-11 18:46:11 -04:00
"Graphical (server + desktop):": {
"fr": "Graphique (serveur + bureau) :",
"en": "Graphical (server + desktop):",
},
"Installing the desktop (long):": {
"fr": "Installation du bureau (long) :",
"en": "Installing the desktop (long):",
},
[ADD] tui qemu: graphical VMs, with a GNOME desktop VMs could only be servers. A server-or-graphical choice joins both interfaces, installing GNOME along with remote access to it. Packages come from the remote command, not cloud-init: their 1 to 2 GB would stretch an already long boot there while leaving no trace in the monitoring, and group installs do not go through it. The desktop therefore does not depend on ERPLibre -- a VM may be wanted graphical and bare. Each distribution has its own names, taken from the source: Arch has no xrdp in its official repositories and takes TigerVNC. The SPICE display is set only on amd64 and arm64; s390x does expose virtio-gpu-ccw, but nothing guarantees its kernel's DRM driver, whereas remote desktop works everywhere. --- FR --- Les VM ne pouvaient être que des serveurs. Un choix serveur ou graphique s'ajoute aux deux interfaces, et pose GNOME avec son accès distant. Les paquets viennent de la commande distante, pas de cloud-init : leurs 1 à 2 Go y allongeraient un démarrage déjà long sans laisser de trace dans le suivi, et les installations par groupe n'y passent pas. Le bureau ne dépend donc pas d'ERPLibre — une VM peut être voulue graphique et nue. Chaque distribution a ses noms, relevés à la source : Arch n'a pas xrdp dans ses dépôts officiels et prend TigerVNC. L'écran virtuel SPICE n'est posé que sur amd64 et arm64 ; s390x expose bien virtio-gpu-ccw, mais rien ne garantit le pilote DRM de son noyau, alors que le bureau distant, lui, marche partout. Assisted-by: Claude Opus 5
2026-08-11 09:49:17 -04:00
"Choice (number, blank = server): ": {
"fr": "Choix (numero, vide = serveur) : ",
"en": "Choice (number, blank = server): ",
},
"Installing the GNOME desktop (long)": {
"fr": "Installation du bureau GNOME (long)",
"en": "Installing the GNOME desktop (long)",
},
[ADD] install: openSUSE support, mirrors and packages Tumbleweed is the only catalog entry whose qpdf already clears the pikepdf threshold: 12.3.2 against 12.2 required, so the half-hour qpdf build under s390x emulation never runs there. It is also the only family foreign to both RHEL and Debian, and it brings zypper, absent from the repository: wired into the install_dev.sh dispatch, a dependency script, the remote bootstrap and the desktop block. Four SUSE traps, all met on real machines. Global options precede the subcommand, and misplaced ones abort the call; without "--auto-agree-with-licenses" zypper waits for an answer nobody gives in a detached install. A compat package that PROVIDES another under a different name makes zypper raise a conflict and drop the whole batch, so only what nothing already provides is requested. pkg-config no longer exists as an RPM, only as a capability. And a single mirror, unreachable one day, sent everyone back to Europe -- three are probed in order now. CentOS Stream is dropped again: it served as a canary but 10 duplicates what Alma and Rocky already cover. --- FR --- Tumbleweed est la seule entrée du catalogue dont qpdf franchit déjà le seuil de pikepdf : 12.3.2 contre 12.2 exigé, si bien que la demi-heure de compilation de qpdf sous émulation s390x ne s'y déclenche jamais. C'est aussi la seule famille étrangère à RHEL comme à Debian, et elle apporte zypper, absent du dépôt : câblé dans l'aiguillage d'install_dev.sh, un script de dépendances, l'amorçage distant et le bloc bureau. Quatre pièges SUSE, tous rencontrés sur machine réelle. Les options globales précèdent la sous-commande, mal placées elles arrêtent l'appel ; sans « --auto-agree-with-licenses » zypper attend une réponse que personne ne donne dans une installation détachée. Un paquet compat qui FOURNIT un autre sous un nom différent fait lever un conflit à zypper, qui abandonne le lot entier : on ne demande donc que ce que rien ne fournit déjà. pkg-config n'existe plus comme RPM, seulement comme capacité. Et un miroir unique, injoignable un jour, renvoyait tout le monde en Europe — trois sont désormais sondés dans l'ordre. CentOS Stream repart : il servait de canari, mais 10 double ce qu'Alma et Rocky couvrent déjà. Assisted-by: Claude Opus 5
2026-08-11 20:38:12 -04:00
"openSUSE mirror:": {
"fr": "miroir openSUSE :",
"en": "openSUSE mirror:",
},
[ADD] tui qemu: graphical VMs, with a GNOME desktop VMs could only be servers. A server-or-graphical choice joins both interfaces, installing GNOME along with remote access to it. Packages come from the remote command, not cloud-init: their 1 to 2 GB would stretch an already long boot there while leaving no trace in the monitoring, and group installs do not go through it. The desktop therefore does not depend on ERPLibre -- a VM may be wanted graphical and bare. Each distribution has its own names, taken from the source: Arch has no xrdp in its official repositories and takes TigerVNC. The SPICE display is set only on amd64 and arm64; s390x does expose virtio-gpu-ccw, but nothing guarantees its kernel's DRM driver, whereas remote desktop works everywhere. --- FR --- Les VM ne pouvaient être que des serveurs. Un choix serveur ou graphique s'ajoute aux deux interfaces, et pose GNOME avec son accès distant. Les paquets viennent de la commande distante, pas de cloud-init : leurs 1 à 2 Go y allongeraient un démarrage déjà long sans laisser de trace dans le suivi, et les installations par groupe n'y passent pas. Le bureau ne dépend donc pas d'ERPLibre — une VM peut être voulue graphique et nue. Chaque distribution a ses noms, relevés à la source : Arch n'a pas xrdp dans ses dépôts officiels et prend TigerVNC. L'écran virtuel SPICE n'est posé que sur amd64 et arm64 ; s390x expose bien virtio-gpu-ccw, mais rien ne garantit le pilote DRM de son noyau, alors que le bureau distant, lui, marche partout. Assisted-by: Claude Opus 5
2026-08-11 09:49:17 -04:00
"Remote desktop:": {
"fr": "Bureau distant :",
"en": "Remote desktop:",
},
"One run per install": {
"fr": "une execution par installation",
"en": "One run per install",
},
"limit to host cores": {
"fr": "limiter au nombre de coeurs de l'hote",
"en": "limit to host cores",
},
"free value…": {
"fr": "valeur libre…",
"en": "free value…",
},
"Disk (e.g. 250G, 1.5T)": {
"fr": "Disque (ex. 250G, 1,5T)",
"en": "Disk (e.g. 250G, 1.5T)",
},
"all archs": {
"fr": "toutes",
"en": "all",
},
"Tick what to deploy": {
"fr": "Cocher ce qu'on veut déployer",
"en": "Tick what to deploy",
},
"F7 main versions · F6 all": {
"fr": "F7 versions principales · F6 tout",
"en": "F7 main versions · F6 all",
},
"exists - skipped": {
"fr": "existe — ignorée",
"en": "exists - skipped",
},
"orphan disk - will FAIL": {
"fr": "disque orphelin — ÉCHOUERA",
"en": "orphan disk - will FAIL",
},
"press F5 again to confirm": {
"fr": "F5 à nouveau pour confirmer",
"en": "press F5 again to confirm",
},
"Deploy": {
"fr": "Déployer",
"en": "Deploy",
},
"Edit VM": {
"fr": "Éditer la VM",
"en": "Edit VM",
},
[ADD] tui qemu: set every VM in place, type included VM type was global: the whole fleet as servers, or all of them GNOME. It now lives on the VM, all the way down -- the creation flag, and one remote command per machine at install time, where a single one served them all. The right pane is no longer a table but a row of widgets per VM: vCPU, RAM, disk and type, each with its usual values and a free entry. The left-hand fields become the shared default, which the screen now says. The scope selector and the F2 modal go away: given two ways to do the same thing, keep the visible one. Three traps came out of it, and the tests lock them. Widget ids carry a RANK, and the rank shifts when an entry is ticked, so an event from an already destroyed widget applied to the VM that took its place -- rows now carry a generation, marked BEFORE mounting, since mount_all empties the pending children and marking after it was a race. The x1..x4 profile no longer reached any VM. And a total of zero never said it had counted nothing. --- FR --- Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une commande distante par machine à l'installation, là où une seule les servait toutes. Le panneau de droite n'est plus un tableau mais une rangée de widgets par VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une saisie libre. Les champs de gauche deviennent le défaut commun, ce que l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent : entre deux façons de faire la même chose, on garde la visible. Trois pièges en sont sortis, et les tests les verrouillent. Les identifiants de widgets portent un RANG, et le rang se décale quand on coche une entrée : un événement émis par un widget déjà détruit s'appliquait à la VM qui avait pris sa place — les rangées portent maintenant une génération, marquée AVANT le montage, car mount_all vide les enfants en attente et marquer après était une course. Le profil x1..x4 n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il n'avait rien compté. Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
"Server": {
"fr": "Serveur",
"en": "Server",
},
"VM type (default):": {
"fr": "Type de VM (défaut) :",
"en": "VM type (default):",
},
"These values are the default for every VM;": {
"fr": "Ces valeurs sont le défaut de chaque VM ;",
"en": "These values are the default for every VM;",
},
"adjust any of them per VM on the right.": {
"fr": "ajustez-les VM par VM dans la vue de droite.",
"en": "adjust any of them per VM on the right.",
},
"Application store:": {
"fr": "Magasin d'applications :",
"en": "Application store:",
},
"Application store (graphical Ubuntu VMs):": {
"fr": "Magasin d'applications (VM Ubuntu graphiques) :",
"en": "Application store (graphical Ubuntu VMs):",
},
"deb only (epiphany-browser)": {
"fr": "deb uniquement (epiphany-browser)",
"en": "deb only (epiphany-browser)",
},
"Flatpak tooling, no Flathub": {
"fr": "outillage Flatpak, sans Flathub",
"en": "Flatpak tooling, no Flathub",
},
"snap (Ubuntu default, Firefox)": {
"fr": "snap (défaut Ubuntu, Firefox)",
"en": "snap (Ubuntu default, Firefox)",
},
"snap needs the store; slow under emulation.": {
"fr": "snap exige le store ; lent sous émulation.",
"en": "snap needs the store; slow under emulation.",
},
"No graphical VM on a snap-based distro.": {
"fr": "Aucune VM graphique sur une distribution à snap.",
"en": "No graphical VM on a snap-based distro.",
},
"Resources — applied to ALL VMs": {
"fr": "Ressources — appliquées à TOUTES les VM",
"en": "Resources — applied to ALL VMs",
},
"The profile and these fields change EVERY VM.": {
"fr": "Le profil et ces champs changent TOUTES les VM.",
"en": "The profile and these fields change EVERY VM.",
},
"A VM edited on the right (marked) keeps its own.": {
"fr": "Une VM modifiée à droite (marquée ✎) garde les siennes.",
"en": "A VM edited on the right (marked) keeps its own.",
},
"Rename the VM": {
"fr": "Renommer la VM",
"en": "Rename the VM",
},
"Rename": {
"fr": "Renommer",
"en": "Rename",
},
"Empty = back to the automatic name:": {
"fr": "Vide = revenir au nom automatique :",
"en": "Empty = back to the automatic name:",
},
"Invalid name: letters, digits, hyphens.": {
"fr": "Nom invalide : lettres, chiffres et traits d'union.",
"en": "Invalid name: letters, digits, hyphens.",
},
"default; a VM may differ, see its line": {
"fr": "défaut ; une VM peut s'en écarter, voir sa ligne",
"en": "default; a VM may differ, see its line",
},
"varies, see each line": {
"fr": "varie, voir chaque ligne",
"en": "varies, see each line",
},
"From your workstation:": {
"fr": "Depuis votre poste :",
"en": "From your workstation:",
},
"then point your client at": {
"fr": "puis pointez votre client sur",
"en": "then point your client at",
},
"Remote desktop tunnel (VNC/RDP through SSH)": {
"fr": "🖥 Tunnel bureau distant (VNC/RDP par SSH)",
"en": "🖥 Remote desktop tunnel (VNC/RDP through SSH)",
},
"Remote desktop tunnel": {
"fr": "Tunnel vers le bureau distant",
"en": "Remote desktop tunnel",
},
"Which VM?": {"fr": "Quelle VM ?", "en": "Which VM?"},
"No IP for this VM; is it running?": {
"fr": "Pas d'IP pour cette VM ; tourne-t-elle ?",
"en": "No IP for this VM; is it running?",
},
"Run this on YOUR workstation:": {
"fr": "À lancer sur VOTRE poste :",
"en": "Run this on YOUR workstation:",
},
"(through the ProxyJump already in ~/.ssh/config)": {
"fr": "(par le ProxyJump déjà dans ~/.ssh/config)",
"en": "(through the ProxyJump already in ~/.ssh/config)",
},
[ADD] script todo: lancer l'émulateur Android et son tunnel adb Le tunnel n'était que du texte à recopier, et le démarrage restait manuel : un second émulateur sur le même AVD tue le premier (« Running multiple emulators »), rencontré deux fois. Le menu le détecte donc avant de lancer. La fenêtre décide du reste. Sans elle, todo.py démarre à distance, détaché par setsid dans le groupe kvm, et enchaîne sur scrcpy ; avec elle, la commande doit partir du poste qui possède l'affichage, pas d'ici. Mesuré : l'émulateur n'écoute que sur le 127.0.0.1 de la VM — l'hyperviseur est refusé sur IP:5555 — d'où le -J qui met la VM en dernier saut. Vérifié sur erplibre-mobile-proof : poignée de main adb CNXN rendant sdk_gphone64_x86_64, enveloppe détachée survivant au ssh ; 30 tests. --- EN --- The tunnel was only text to copy, and starting the emulator stayed manual: a second emulator on the same AVD kills the first ("Running multiple emulators"), hit twice. The menu now detects it before starting. The window decides the rest. Without one, todo.py starts it remotely, detached by setsid in the kvm group, and chains into scrcpy; with one, the command must run from the workstation that owns the display, not from here. Measured: the emulator only listens on the VM's 127.0.0.1 — the hypervisor is refused on IP:5555 — hence the -J putting the VM last. Verified on erplibre-mobile-proof: an adb CNXN handshake returning sdk_gphone64_x86_64, a detached wrapper outliving the ssh; 30 tests. Assisted-by: Claude Opus 5
2026-08-19 03:35:31 -04:00
"(the hypervisor only relays; -J puts the VM last)": {
"fr": "(l'hyperviseur ne fait que relayer ; -J met la VM en dernier)",
"en": "(the hypervisor only relays; -J puts the VM last)",
},
[FIX] script todo: ne plus annoncer un émulateur qui n'a pas démarré Trois défauts vus en conduisant le menu sur de vraies VM. « setsid » détache, donc son code de retour vaut 0 même quand rien ne se lance : le menu disait « Démarré » sur une VM sans SDK, dont le journal disait « not found ». Le démarrage attend maintenant de VOIR le processus, et à défaut cite le journal. Une sonde préalable lit binaire et AVD d'un coup : une VM déployée sans cocher l'outil est le cas normal, pas une panne. Vérifié sur deux VM réelles — outillée (True), migration (« aucun binaire emulator »), sans rien y démarrer. Et tout ce qui n'était pas « 2 » démarrait l'émulateur : un « n » de travers suffisait. Au passage, « Choice » n'était traduit dans aucun des trois menus qui l'affichent. --- EN --- Three defects found while driving the menu against real VMs. setsid detaches, so its exit code is 0 even when nothing launches: the menu said "Started" on a VM with no SDK, whose log said "not found". Starting now waits to SEE the process, and quotes the log when it never appears. A prior probe reads binary and AVD in one go: a VM deployed without ticking the tool is the normal case, not a failure. Verified on two real VMs — tooled (True), migration ("no emulator binary") — without starting anything there. And anything that was not "2" started the emulator; a stray "n" was enough. Along the way, "Choice" was untranslated in all three menus showing it. Assisted-by: Claude Opus 5
2026-08-19 03:46:12 -04:00
"Choice": {"fr": "Choix", "en": "Choice"},
"failed steps": {"fr": "étapes en échec", "en": "failed steps"},
"Failed steps": {"fr": "Étapes en échec", "en": "Failed steps"},
"Hard signals": {"fr": "Signaux durs", "en": "Hard signals"},
"Grouped by shape": {"fr": "Regroupé par forme", "en": "Grouped by shape"},
"line": {"fr": "ligne", "en": "line"},
[FIX] script todo: ne plus laisser PyCharm manger la compilation mobile Sur erplibre-ubuntu-2604-gnome, l'APK s'est fait tuer par le noyau. La cause est ici : « $! » désigne xvfb-run, un script, et le tuer n'atteint ni PyCharm ni Xvfb — l'IDE tournait encore 45 minutes après son étape, avec 1,9 Go, quand Gradle a demandé ses 6,8 Go sur 12. C'est le GROUPE qu'on tue maintenant, et plus rien ne survit : mesuré, 2 Go rendus. Le .idea n'était jamais écrit non plus : 122 684 des 123 021 fichiers d'assets du dépôt mobile épuisaient les 65 536 watches inotify. Relevées à 524288, le projet se crée. Restent 4 Go de swap avant de compiler, et un diagnostic qui nomme la mémoire — avec le compte de l'oom-killer — puis la limite ZIP de 65 535 entrées, sur laquelle la compilation bute désormais, en amont. --- EN --- On erplibre-ubuntu-2604-gnome the APK was killed by the kernel. The cause is here: "$!" is xvfb-run, a script, and killing it reaches neither PyCharm nor Xvfb — the IDE was still running 45 minutes after its step, holding 1.9 GB, when Gradle asked for its 6.8 GB out of 12. The GROUP is killed now, and nothing survives it: 2 GB given back, measured. The .idea was never written either: 122,684 of the mobile repo's 123,021 asset files exhausted the 65,536 inotify watches. Raised to 524288, the project gets created. Also 4 GB of swap before building, and a diagnostic naming memory — with the oom-killer count — then the 65,535-entry ZIP limit the build now hits, upstream of us. Assisted-by: Claude Opus 5
2026-08-19 05:38:01 -04:00
"inotify watches raised for the IDE": {
"fr": "watches inotify relevées pour l'IDE",
"en": "inotify watches raised for the IDE",
},
"closing what survived the first open:": {
"fr": "fermeture de ce qui a survécu à la première ouverture :",
"en": "closing what survived the first open:",
},
"Gradle daemon killed: out of memory": {
"fr": "démon Gradle tué : mémoire épuisée",
"en": "Gradle daemon killed: out of memory",
},
"out of memory": {"fr": "mémoire épuisée", "en": "out of memory"},
"Gradle heap too small": {
"fr": "tas Gradle trop petit",
"en": "Gradle heap too small",
},
"too many asset files for one APK (ZIP limit: 65535 entries)": {
"fr": "trop de fichiers d'assets pour un APK (limite ZIP : 65535"
" entrées)",
"en": "too many asset files for one APK (ZIP limit: 65535 entries)",
},
"4 GB of swap added for the build": {
"fr": "4 Go de swap ajoutés pour la compilation",
"en": "4 GB of swap added for the build",
},
"no swap could be added; build may run short": {
"fr": "swap impossible à ajouter ; la compilation peut manquer de mémoire",
"en": "no swap could be added; build may run short",
},
"memory:": {"fr": "mémoire :", "en": "memory:"},
"MB RAM,": {"fr": "Mo de RAM,", "en": "MB RAM,"},
"MB swap, kernel OOM kills:": {
"fr": "Mo de swap, tués par le noyau (OOM) :",
"en": "MB swap, kernel OOM kills:",
},
[FIX] script todo: ne plus annoncer un émulateur qui n'a pas démarré Trois défauts vus en conduisant le menu sur de vraies VM. « setsid » détache, donc son code de retour vaut 0 même quand rien ne se lance : le menu disait « Démarré » sur une VM sans SDK, dont le journal disait « not found ». Le démarrage attend maintenant de VOIR le processus, et à défaut cite le journal. Une sonde préalable lit binaire et AVD d'un coup : une VM déployée sans cocher l'outil est le cas normal, pas une panne. Vérifié sur deux VM réelles — outillée (True), migration (« aucun binaire emulator »), sans rien y démarrer. Et tout ce qui n'était pas « 2 » démarrait l'émulateur : un « n » de travers suffisait. Au passage, « Choice » n'était traduit dans aucun des trois menus qui l'affichent. --- EN --- Three defects found while driving the menu against real VMs. setsid detaches, so its exit code is 0 even when nothing launches: the menu said "Started" on a VM with no SDK, whose log said "not found". Starting now waits to SEE the process, and quotes the log when it never appears. A prior probe reads binary and AVD in one go: a VM deployed without ticking the tool is the normal case, not a failure. Verified on two real VMs — tooled (True), migration ("no emulator binary") — without starting anything there. And anything that was not "2" started the emulator; a stray "n" was enough. Along the way, "Choice" was untranslated in all three menus showing it. Assisted-by: Claude Opus 5
2026-08-19 03:46:12 -04:00
"Tick the Android emulator tool when deploying.": {
"fr": "Cochez l'outil Émulateur Android au déploiement.",
"en": "Tick the Android emulator tool when deploying.",
},
"Cannot reach this VM.": {
"fr": "VM injoignable.",
"en": "Cannot reach this VM.",
},
"No Android SDK in this VM: no emulator binary.": {
"fr": "Pas de SDK Android dans cette VM : aucun binaire emulator.",
"en": "No Android SDK in this VM: no emulator binary.",
},
"No AVD named erplibre in this VM.": {
"fr": "Aucun AVD nommé erplibre dans cette VM.",
"en": "No AVD named erplibre in this VM.",
},
"It did not start; the VM log says:": {
"fr": "Il n'a pas démarré ; le journal de la VM dit :",
"en": "It did not start; the VM log says:",
},
[ADD] tui qemu: set every VM in place, type included VM type was global: the whole fleet as servers, or all of them GNOME. It now lives on the VM, all the way down -- the creation flag, and one remote command per machine at install time, where a single one served them all. The right pane is no longer a table but a row of widgets per VM: vCPU, RAM, disk and type, each with its usual values and a free entry. The left-hand fields become the shared default, which the screen now says. The scope selector and the F2 modal go away: given two ways to do the same thing, keep the visible one. Three traps came out of it, and the tests lock them. Widget ids carry a RANK, and the rank shifts when an entry is ticked, so an event from an already destroyed widget applied to the VM that took its place -- rows now carry a generation, marked BEFORE mounting, since mount_all empties the pending children and marking after it was a race. The x1..x4 profile no longer reached any VM. And a total of zero never said it had counted nothing. --- FR --- Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une commande distante par machine à l'installation, là où une seule les servait toutes. Le panneau de droite n'est plus un tableau mais une rangée de widgets par VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une saisie libre. Les champs de gauche deviennent le défaut commun, ce que l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent : entre deux façons de faire la même chose, on garde la visible. Trois pièges en sont sortis, et les tests les verrouillent. Les identifiants de widgets portent un RANG, et le rang se décale quand on coche une entrée : un événement émis par un widget déjà détruit s'appliquait à la VM qui avait pris sa place — les rangées portent maintenant une génération, marquée AVANT le montage, car mount_all vide les enfants en attente et marquer après était une course. Le profil x1..x4 n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il n'avait rien compté. Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
"No ~/.ssh/config entry; see SSH configuration.": {
"fr": "Aucune entrée ~/.ssh/config ; voir Configuration SSH.",
"en": "No ~/.ssh/config entry; see SSH configuration.",
},
"Not in an SSH session: check the host address.": {
"fr": "Hors session SSH : vérifiez l'adresse de l'hôte.",
"en": "Not in an SSH session: check the host address.",
},
"The tunnel stays open as long as that ssh runs.": {
"fr": "Le tunnel reste ouvert tant que ce ssh tourne.",
"en": "The tunnel stays open as long as that ssh runs.",
},
"No VM defined.": {"fr": "Aucune VM définie.", "en": "No VM defined."},
"No host in ~/.ssh/config and no local VM.": {
"fr": "Aucun hôte dans ~/.ssh/config et aucune VM locale.",
"en": "No host in ~/.ssh/config and no local VM.",
[ADD] tui qemu: customise one VM without touching the others The three resource fields applied to the whole fleet. With a single machine needing 16 G, the other eight got it too. A scope selector settles it: all VMs, or the row targeted in the plan. The x1..x4 profile stays global, multiplying what each image asks for; under "selected VM" the fields are absolute, hence active even outside the custom profile. The F2 modal already existed but was undiscoverable, and its cursor was unusable: the table never held focus. It stays, the toggle now focuses the table, F4 returns a VM to the shared profile, and the plan marks customised rows with a ✎ -- without it, two rows with different resources have no explanation on screen. One defect found along the way: clear() reset the cursor to the top on every redraw, and the plan recomputes on every keystroke. You picked debian, typed the value, the table redrew, and the next entry landed on ubuntu with nothing to show for it. --- FR --- Les trois champs de ressources s'appliquaient à tout le parc. Une seule machine ayant besoin de 16 G, il fallait les donner aux huit autres. Un sélecteur de portée tranche : toutes les VM, ou la ligne visée dans le plan. Le profil x1..x4 reste global, il multiplie ce que demande chaque image ; en portée « une seule » les champs valent en absolu, et sont donc actifs même hors profil personnalisé. La modale F2 existait déjà mais restait introuvable, et son curseur était inutilisable : le tableau n'avait jamais le focus. Elle demeure, la bascule lui donne le focus, F4 rend une VM au profil commun, et le plan marque d'un ✎ ce qui a été personnalisé — sans marque, deux lignes aux ressources différentes n'ont aucune explication à l'écran. Un défaut au passage : « clear() » ramenait le curseur en tête à chaque redessin, et le plan se recalcule à chaque frappe. On choisissait debian, on tapait la valeur, le tableau se redessinait, et la saisie suivante partait sur ubuntu sans que rien ne le montre. Assisted-by: Claude Opus 5
2026-08-12 01:30:11 -04:00
},
[ADD] tui qemu: set every VM in place, type included VM type was global: the whole fleet as servers, or all of them GNOME. It now lives on the VM, all the way down -- the creation flag, and one remote command per machine at install time, where a single one served them all. The right pane is no longer a table but a row of widgets per VM: vCPU, RAM, disk and type, each with its usual values and a free entry. The left-hand fields become the shared default, which the screen now says. The scope selector and the F2 modal go away: given two ways to do the same thing, keep the visible one. Three traps came out of it, and the tests lock them. Widget ids carry a RANK, and the rank shifts when an entry is ticked, so an event from an already destroyed widget applied to the VM that took its place -- rows now carry a generation, marked BEFORE mounting, since mount_all empties the pending children and marking after it was a race. The x1..x4 profile no longer reached any VM. And a total of zero never said it had counted nothing. --- FR --- Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une commande distante par machine à l'installation, là où une seule les servait toutes. Le panneau de droite n'est plus un tableau mais une rangée de widgets par VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une saisie libre. Les champs de gauche deviennent le défaut commun, ce que l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent : entre deux façons de faire la même chose, on garde la visible. Trois pièges en sont sortis, et les tests les verrouillent. Les identifiants de widgets portent un RANG, et le rang se décale quand on coche une entrée : un événement émis par un widget déjà détruit s'appliquait à la VM qui avait pris sa place — les rangées portent maintenant une génération, marquée AVANT le montage, car mount_all vide les enfants en attente et marquer après était une course. Le profil x1..x4 n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il n'avait rien compté. Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
"local VM": {"fr": "VM locale", "en": "local VM"},
"Remote desktop kind:": {
"fr": "Type de bureau distant :",
"en": "Remote desktop kind:",
[ADD] tui qemu: customise one VM without touching the others The three resource fields applied to the whole fleet. With a single machine needing 16 G, the other eight got it too. A scope selector settles it: all VMs, or the row targeted in the plan. The x1..x4 profile stays global, multiplying what each image asks for; under "selected VM" the fields are absolute, hence active even outside the custom profile. The F2 modal already existed but was undiscoverable, and its cursor was unusable: the table never held focus. It stays, the toggle now focuses the table, F4 returns a VM to the shared profile, and the plan marks customised rows with a ✎ -- without it, two rows with different resources have no explanation on screen. One defect found along the way: clear() reset the cursor to the top on every redraw, and the plan recomputes on every keystroke. You picked debian, typed the value, the table redrew, and the next entry landed on ubuntu with nothing to show for it. --- FR --- Les trois champs de ressources s'appliquaient à tout le parc. Une seule machine ayant besoin de 16 G, il fallait les donner aux huit autres. Un sélecteur de portée tranche : toutes les VM, ou la ligne visée dans le plan. Le profil x1..x4 reste global, il multiplie ce que demande chaque image ; en portée « une seule » les champs valent en absolu, et sont donc actifs même hors profil personnalisé. La modale F2 existait déjà mais restait introuvable, et son curseur était inutilisable : le tableau n'avait jamais le focus. Elle demeure, la bascule lui donne le focus, F4 rend une VM au profil commun, et le plan marque d'un ✎ ce qui a été personnalisé — sans marque, deux lignes aux ressources différentes n'ont aucune explication à l'écran. Un défaut au passage : « clear() » ramenait le curseur en tête à chaque redessin, et le plan se recalcule à chaque frappe. On choisissait debian, on tapait la valeur, le tableau se redessinait, et la saisie suivante partait sur ubuntu sans que rien ne le montre. Assisted-by: Claude Opus 5
2026-08-12 01:30:11 -04:00
},
[ADD] tui qemu: set every VM in place, type included VM type was global: the whole fleet as servers, or all of them GNOME. It now lives on the VM, all the way down -- the creation flag, and one remote command per machine at install time, where a single one served them all. The right pane is no longer a table but a row of widgets per VM: vCPU, RAM, disk and type, each with its usual values and a free entry. The left-hand fields become the shared default, which the screen now says. The scope selector and the F2 modal go away: given two ways to do the same thing, keep the visible one. Three traps came out of it, and the tests lock them. Widget ids carry a RANK, and the rank shifts when an entry is ticked, so an event from an already destroyed widget applied to the VM that took its place -- rows now carry a generation, marked BEFORE mounting, since mount_all empties the pending children and marking after it was a race. The x1..x4 profile no longer reached any VM. And a total of zero never said it had counted nothing. --- FR --- Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une commande distante par machine à l'installation, là où une seule les servait toutes. Le panneau de droite n'est plus un tableau mais une rangée de widgets par VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une saisie libre. Les champs de gauche deviennent le défaut commun, ce que l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent : entre deux façons de faire la même chose, on garde la visible. Trois pièges en sont sortis, et les tests les verrouillent. Les identifiants de widgets portent un RANG, et le rang se décale quand on coche une entrée : un événement émis par un widget déjà détruit s'appliquait à la VM qui avait pris sa place — les rangées portent maintenant une génération, marquée AVANT le montage, car mount_all vide les enfants en attente et marquer après était une course. Le profil x1..x4 n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il n'avait rien compté. Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
"already defined, not counted": {
"fr": "déjà définie(s), non comptée(s)",
"en": "already defined, not counted",
[ADD] tui qemu: customise one VM without touching the others The three resource fields applied to the whole fleet. With a single machine needing 16 G, the other eight got it too. A scope selector settles it: all VMs, or the row targeted in the plan. The x1..x4 profile stays global, multiplying what each image asks for; under "selected VM" the fields are absolute, hence active even outside the custom profile. The F2 modal already existed but was undiscoverable, and its cursor was unusable: the table never held focus. It stays, the toggle now focuses the table, F4 returns a VM to the shared profile, and the plan marks customised rows with a ✎ -- without it, two rows with different resources have no explanation on screen. One defect found along the way: clear() reset the cursor to the top on every redraw, and the plan recomputes on every keystroke. You picked debian, typed the value, the table redrew, and the next entry landed on ubuntu with nothing to show for it. --- FR --- Les trois champs de ressources s'appliquaient à tout le parc. Une seule machine ayant besoin de 16 G, il fallait les donner aux huit autres. Un sélecteur de portée tranche : toutes les VM, ou la ligne visée dans le plan. Le profil x1..x4 reste global, il multiplie ce que demande chaque image ; en portée « une seule » les champs valent en absolu, et sont donc actifs même hors profil personnalisé. La modale F2 existait déjà mais restait introuvable, et son curseur était inutilisable : le tableau n'avait jamais le focus. Elle demeure, la bascule lui donne le focus, F4 rend une VM au profil commun, et le plan marque d'un ✎ ce qui a été personnalisé — sans marque, deux lignes aux ressources différentes n'ont aucune explication à l'écran. Un défaut au passage : « clear() » ramenait le curseur en tête à chaque redessin, et le plan se recalcule à chaque frappe. On choisissait debian, on tapait la valeur, le tableau se redessinait, et la saisie suivante partait sur ubuntu sans que rien ne le montre. Assisted-by: Claude Opus 5
2026-08-12 01:30:11 -04:00
},
[ADD] tui qemu: set every VM in place, type included VM type was global: the whole fleet as servers, or all of them GNOME. It now lives on the VM, all the way down -- the creation flag, and one remote command per machine at install time, where a single one served them all. The right pane is no longer a table but a row of widgets per VM: vCPU, RAM, disk and type, each with its usual values and a free entry. The left-hand fields become the shared default, which the screen now says. The scope selector and the F2 modal go away: given two ways to do the same thing, keep the visible one. Three traps came out of it, and the tests lock them. Widget ids carry a RANK, and the rank shifts when an entry is ticked, so an event from an already destroyed widget applied to the VM that took its place -- rows now carry a generation, marked BEFORE mounting, since mount_all empties the pending children and marking after it was a race. The x1..x4 profile no longer reached any VM. And a total of zero never said it had counted nothing. --- FR --- Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une commande distante par machine à l'installation, là où une seule les servait toutes. Le panneau de droite n'est plus un tableau mais une rangée de widgets par VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une saisie libre. Les champs de gauche deviennent le défaut commun, ce que l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent : entre deux façons de faire la même chose, on garde la visible. Trois pièges en sont sortis, et les tests les verrouillent. Les identifiants de widgets portent un RANG, et le rang se décale quand on coche une entrée : un événement émis par un widget déjà détruit s'appliquait à la VM qui avait pris sa place — les rangées portent maintenant une génération, marquée AVANT le montage, car mount_all vide les enfants en attente et marquer après était une course. Le profil x1..x4 n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il n'avait rien compté. Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
"RAM: 2048 or 8G": {"fr": "RAM : 2048 ou 8G", "en": "RAM: 2048 or 8G"},
[ADD] tui qemu: F9 dumps the form state, widgets and model A per-VM setting appears not to be taken into account, and nothing on screen tells which link failed: the gap may be between the widget and the model, or between the model and the spec. A screenshot shows neither. F9 writes both side by side into ~/.erplibre/deploy-form-dump.txt: profile, shared values, overrides, locks, row generation, then VM by VM what the list DISPLAYS against what the model HOLDS, and finally the spec that would go to deployment. The file can be pasted into a message, unlike an image, and it answers the question on its own: if the list shows 16384 while the model says 1024, the defect is in the intake; if they agree and the created VM differs, it is downstream, in deploy_qemu. --- FR --- Un réglage par VM ne semble pas pris en compte, et rien dans ce que l'écran montre ne permet de trancher : l'écart peut être entre le widget et le modèle, ou entre le modèle et la spec. Une capture d'écran ne dit ni l'un ni l'autre. F9 écrit les deux côte à côte dans ~/.erplibre/deploy-form-dump.txt : profil, valeurs communes, surcharges, verrous, génération des rangées, puis VM par VM ce que la liste AFFICHE contre ce que le modèle CONTIENT, et enfin la spec qui partirait au déploiement. Le fichier se recopie dans un message, contrairement à une image, et il répond seul à la question : si la liste montre 16384 et que le modèle dit 1024, le défaut est dans la prise en compte ; s'ils s'accordent et que la VM créée diffère, il est en aval, dans deploy_qemu. Assisted-by: Claude Opus 5
2026-08-13 04:51:27 -04:00
"Diagnostic dump": {"fr": "Diagnostic", "en": "Diagnostic dump"},
"State written to": {"fr": "État écrit dans", "en": "State written to"},
[ADD] tui qemu: set every VM in place, type included VM type was global: the whole fleet as servers, or all of them GNOME. It now lives on the VM, all the way down -- the creation flag, and one remote command per machine at install time, where a single one served them all. The right pane is no longer a table but a row of widgets per VM: vCPU, RAM, disk and type, each with its usual values and a free entry. The left-hand fields become the shared default, which the screen now says. The scope selector and the F2 modal go away: given two ways to do the same thing, keep the visible one. Three traps came out of it, and the tests lock them. Widget ids carry a RANK, and the rank shifts when an entry is ticked, so an event from an already destroyed widget applied to the VM that took its place -- rows now carry a generation, marked BEFORE mounting, since mount_all empties the pending children and marking after it was a race. The x1..x4 profile no longer reached any VM. And a total of zero never said it had counted nothing. --- FR --- Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une commande distante par machine à l'installation, là où une seule les servait toutes. Le panneau de droite n'est plus un tableau mais une rangée de widgets par VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une saisie libre. Les champs de gauche deviennent le défaut commun, ce que l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent : entre deux façons de faire la même chose, on garde la visible. Trois pièges en sont sortis, et les tests les verrouillent. Les identifiants de widgets portent un RANG, et le rang se décale quand on coche une entrée : un événement émis par un widget déjà détruit s'appliquait à la VM qui avait pris sa place — les rangées portent maintenant une génération, marquée AVANT le montage, car mount_all vide les enfants en attente et marquer après était une course. Le profil x1..x4 n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il n'avait rien compté. Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
"Reset VM": {
"fr": "Réinit. VM",
"en": "Reset VM",
[ADD] tui qemu: customise one VM without touching the others The three resource fields applied to the whole fleet. With a single machine needing 16 G, the other eight got it too. A scope selector settles it: all VMs, or the row targeted in the plan. The x1..x4 profile stays global, multiplying what each image asks for; under "selected VM" the fields are absolute, hence active even outside the custom profile. The F2 modal already existed but was undiscoverable, and its cursor was unusable: the table never held focus. It stays, the toggle now focuses the table, F4 returns a VM to the shared profile, and the plan marks customised rows with a ✎ -- without it, two rows with different resources have no explanation on screen. One defect found along the way: clear() reset the cursor to the top on every redraw, and the plan recomputes on every keystroke. You picked debian, typed the value, the table redrew, and the next entry landed on ubuntu with nothing to show for it. --- FR --- Les trois champs de ressources s'appliquaient à tout le parc. Une seule machine ayant besoin de 16 G, il fallait les donner aux huit autres. Un sélecteur de portée tranche : toutes les VM, ou la ligne visée dans le plan. Le profil x1..x4 reste global, il multiplie ce que demande chaque image ; en portée « une seule » les champs valent en absolu, et sont donc actifs même hors profil personnalisé. La modale F2 existait déjà mais restait introuvable, et son curseur était inutilisable : le tableau n'avait jamais le focus. Elle demeure, la bascule lui donne le focus, F4 rend une VM au profil commun, et le plan marque d'un ✎ ce qui a été personnalisé — sans marque, deux lignes aux ressources différentes n'ont aucune explication à l'écran. Un défaut au passage : « clear() » ramenait le curseur en tête à chaque redessin, et le plan se recalcule à chaque frappe. On choisissait debian, on tapait la valeur, le tableau se redessinait, et la saisie suivante partait sur ubuntu sans que rien ne le montre. Assisted-by: Claude Opus 5
2026-08-12 01:30:11 -04:00
},
"Preview": {
"fr": "Aperçu",
"en": "Preview",
},
"All": {
"fr": "Tout",
"en": "All",
},
"Main versions": {
"fr": "Versions principales",
"en": "Main versions",
},
"None": {
"fr": "Rien",
"en": "None",
},
"Cancel": {
"fr": "Annuler",
"en": "Cancel",
},
"Apply": {
"fr": "Appliquer",
"en": "Apply",
},
"Close": {
"fr": "Fermer",
"en": "Close",
},
"Copy log": {
"fr": "Copier le log",
"en": "Copy log",
},
"Copy all logs": {
"fr": "Copier tous les logs",
"en": "Copy all logs",
},
"all logs": {
"fr": "tous les logs",
"en": "all logs",
},
"c copy log · C copy all · q quit": {
"fr": "c copier le log · C copier tout · q quitter",
"en": "c copy log · C copy all · q quit",
},
"Clipboard": {
"fr": "Presse-papiers",
"en": "Clipboard",
},
"chars": {
"fr": "car.",
"en": "chars",
},
"tail only, log was truncated": {
"fr": "fin seulement, log tronqué",
"en": "tail only, log was truncated",
},
"Needs an OSC 52 capable terminal.": {
"fr": "Nécessite un terminal compatible OSC 52.",
"en": "Needs an OSC 52 capable terminal.",
},
"Nothing to copy.": {
"fr": "Rien à copier.",
"en": "Nothing to copy.",
},
"TODO navigation telemetry": {
"fr": "TODO — télémétrie de navigation",
"en": "TODO navigation telemetry",
},
"navigations": {
"fr": "navigations",
"en": "navigations",
},
"menus": {
"fr": "menus",
"en": "menus",
},
"Telemetry reset.": {
"fr": "Télémétrie réinitialisée.",
"en": "Telemetry reset.",
},
"tree from code": {
"fr": "arbre issu du code",
"en": "tree from code",
},
"visited paths only": {
"fr": "chemins visités seulement",
"en": "visited paths only",
},
"Enter = run, F3 = view": {
"fr": "Entrée = exécuter, F3 = vue",
"en": "Enter = run, F3 = view",
},
"Enter = run, F3/F4 = views": {
"fr": "Entrée = exécuter, F3/F4 = vues",
"en": "Enter = run, F3/F4 = views",
},
"view": {
"fr": "vue",
"en": "view",
},
"avg": {
"fr": "moy",
"en": "avg",
},
"Last install:": {
"fr": "Dernière install :",
"en": "Last install:",
},
"No command found.": {
"fr": "Aucune commande trouvée.",
"en": "No command found.",
},
"Back to telemetry (r) or quit (Enter)? ": {
"fr": "Revenir à la télémétrie (r) ou quitter (Entrée) ? ",
"en": "Back to telemetry (r) or quit (Enter)? ",
},
"F2 system · F3/F4 views · Enter run": {
"fr": "F2 système · F3/F4 vues · Entrée exécute",
"en": "F2 system · F3/F4 views · Enter run",
},
"State": {"fr": "État", "en": "State"},
"uptime": {"fr": "actif depuis", "en": "uptime"},
"load": {"fr": "charge", "en": "load"},
"cores": {"fr": "cœurs", "en": "cores"},
"Memory": {"fr": "Mémoire", "en": "Memory"},
"Disk": {"fr": "Disque", "en": "Disk"},
"Network": {"fr": "Réseau", "en": "Network"},
"Battery": {"fr": "Batterie", "en": "Battery"},
"Temperature": {"fr": "Température", "en": "Temperature"},
"lm-sensors absent — press i to install": {
"fr": "lm-sensors absent — appuyez sur i pour installer",
"en": "lm-sensors absent — press i to install",
},
"Sensors already available.": {
"fr": "Capteurs déjà disponibles.",
"en": "Sensors already available.",
},
"Sensors now available.": {
"fr": "Capteurs désormais disponibles.",
"en": "Sensors now available.",
},
"Still no temperature (reboot/modprobe may be needed).": {
"fr": "Toujours pas de température (redémarrage/modprobe requis ?).",
"en": "Still no temperature (reboot/modprobe may be needed).",
},
"Unknown package manager for lm-sensors.": {
"fr": "Gestionnaire de paquets inconnu pour lm-sensors.",
"en": "Unknown package manager for lm-sensors.",
},
"Proposed install command:": {
"fr": "Commande d'installation proposée :",
"en": "Proposed install command:",
},
"Install lm-sensors now? (y/N): ": {
"fr": "Installer lm-sensors maintenant ? (o/N) : ",
"en": "Install lm-sensors now? (y/N): ",
},
"Command failed: ": {
"fr": "Échec de la commande : ",
"en": "Command failed: ",
},
"Textual is required for this screen.": {
"fr": "Textual est nécessaire pour cet écran.",
"en": "Textual is required for this screen.",
},
"Install it now? (Y/n): ": {
"fr": "L'installer maintenant ? (O/n, défaut : oui) : ",
"en": "Install it now? (Y/n, default: yes): ",
},
"Textual is installed.": {
"fr": "Textual est installé.",
"en": "Textual is installed.",
},
"Installation finished but textual is still missing.": {
"fr": "Installation terminée mais textual reste introuvable.",
"en": "Installation finished but textual is still missing.",
},
"pip exited with": {
"fr": "pip a retourné",
"en": "pip exited with",
},
"Your distribution may package it as python3-textual.": {
"fr": "Votre distribution le fournit peut-être en python3-textual.",
"en": "Your distribution may package it as python3-textual.",
},
"Resize a VM disk": {
"fr": "📐 Redimensionner le disque d'une VM",
"en": "📐 Resize a VM disk",
},
"VM name to resize: ": {
"fr": "Nom de la VM à redimensionner : ",
"en": "VM name to resize: ",
},
"VM not found.": {
"fr": "VM introuvable.",
"en": "VM not found.",
},
"Main disk not found for this VM.": {
"fr": "Disque principal introuvable pour cette VM.",
"en": "Main disk not found for this VM.",
},
"Current disk:": {
"fr": "Disque actuel :",
"en": "Current disk:",
},
"Current virtual size:": {
"fr": "Taille virtuelle actuelle :",
"en": "Current virtual size:",
},
"VM state:": {
"fr": "État de la VM :",
"en": "VM state:",
},
"Could not read current disk size; aborting.": {
"fr": "Impossible de lire la taille actuelle du disque ; abandon.",
"en": "Could not read current disk size; aborting.",
},
"Host free space:": {
"fr": "Espace libre hôte :",
"en": "Host free space:",
},
"max sustainable total (before host full):": {
"fr": "max soutenable au total (avant hôte plein) :",
"en": "max sustainable total (before host full):",
},
"Beyond host capacity by ~%.1f G — overcommit.": {
"fr": "Dépasse la capacité de l'hôte de ~%.1f G — surallocation.",
"en": "Beyond host capacity by ~%.1f G — overcommit.",
},
"The qcow2 is thin: fine until the VM fills it, then the "
"host disk runs out. Max sustainable: ~%.1f G.": {
"fr": "Le qcow2 est creux : OK tant que la VM ne remplit pas, sinon "
"l'hôte tombe à court. Max soutenable : ~%.1f G.",
"en": "The qcow2 is thin: fine until the VM fills it, then the "
"host disk runs out. Max sustainable: ~%.1f G.",
},
"Safe shrink needs libguestfs (virt-resize).": {
"fr": "La réduction sûre nécessite libguestfs (virt-resize).",
"en": "Safe shrink needs libguestfs (virt-resize).",
},
"Install libguestfs-tools now? (y/N): ": {
"fr": "Installer libguestfs-tools maintenant ? (o/N) : ",
"en": "Install libguestfs-tools now? (y/N): ",
},
"Shrink aborted; disk left intact.": {
"fr": "Réduction annulée ; disque laissé intact.",
"en": "Shrink aborted; disk left intact.",
},
"Could not detect the partition to shrink; aborting.": {
"fr": "Partition à réduire introuvable ; abandon.",
"en": "Could not detect the partition to shrink; aborting.",
},
"Missing tools for safe shrink:": {
"fr": "Outils manquants pour la réduction sûre :",
"en": "Missing tools for safe shrink:",
},
"Backing up the disk before shrinking…": {
"fr": "Sauvegarde du disque avant réduction…",
"en": "Backing up the disk before shrinking…",
},
"Back up the disk before shrinking? (Y/n): ": {
"fr": "Sauvegarder le disque avant réduction ? (O/n, défaut : oui) : ",
"en": "Back up the disk before shrinking? (Y/n, default: yes): ",
},
"No backup: a failure could leave the disk broken.": {
"fr": "Sans sauvegarde : un échec pourrait rendre le disque inutilisable.",
"en": "No backup: a failure could leave the disk broken.",
},
"Disk safely shrunk.": {
"fr": "Disque réduit en toute sécurité.",
"en": "Disk safely shrunk.",
},
"No backup to restore; run fsck on the disk before use.": {
"fr": "Pas de sauvegarde à restaurer ; lancez fsck avant utilisation.",
"en": "No backup to restore; run fsck on the disk before use.",
},
"A disk backup was kept:": {
"fr": "Une sauvegarde du disque a été conservée :",
"en": "A disk backup was kept:",
},
"Delete this backup now? (y/N): ": {
"fr": "Effacer cette sauvegarde maintenant ? (o/N) : ",
"en": "Delete this backup now? (y/N): ",
},
"Backup deleted.": {
"fr": "Sauvegarde effacée.",
"en": "Backup deleted.",
},
"Backup kept (delete later via Clean up QEMU).": {
"fr": "Sauvegarde conservée (à effacer plus tard via Nettoyer QEMU).",
"en": "Backup kept (delete later via Clean up QEMU).",
},
"disk backup (resize)": {
"fr": "sauvegarde de disque (redim.)",
"en": "disk backup (resize)",
},
"Backup failed; aborting.": {
"fr": "Échec de la sauvegarde ; abandon.",
"en": "Backup failed; aborting.",
},
"Could not attach the disk (nbd); aborting.": {
"fr": "Impossible d'attacher le disque (nbd) ; abandon.",
"en": "Could not attach the disk (nbd); aborting.",
},
"Only ext2/3/4 can be shrunk safely; aborting.": {
"fr": "Seul ext2/3/4 peut être réduit en sécurité ; abandon.",
"en": "Only ext2/3/4 can be shrunk safely; aborting.",
},
"Target size too small for this layout; aborting.": {
"fr": "Taille cible trop petite pour cette disposition ; abandon.",
"en": "Target size too small for this layout; aborting.",
},
"Not enough used-space margin to shrink; aborting.": {
"fr": "Espace utilisé trop proche de la cible ; abandon.",
"en": "Not enough used-space margin to shrink; aborting.",
},
"Shrinking guest ext filesystem": {
"fr": "Réduction du système de fichiers ext invité",
"en": "Shrinking guest ext filesystem",
},
"resize2fs failed; reverting.": {
"fr": "resize2fs a échoué ; restauration.",
"en": "resize2fs failed; reverting.",
},
"Internal size check failed; reverting.": {
"fr": "Contrôle de taille interne échoué ; restauration.",
"en": "Internal size check failed; reverting.",
},
"Shrinking the partition…": {
"fr": "Réduction de la partition…",
"en": "Shrinking the partition…",
},
"Partition rewrite failed; reverting.": {
"fr": "Réécriture de la partition échouée ; restauration.",
"en": "Partition rewrite failed; reverting.",
},
"Shrinking the qcow2 container…": {
"fr": "Réduction du conteneur qcow2…",
"en": "Shrinking the qcow2 container…",
},
"Container shrink failed; reverting.": {
"fr": "Réduction du conteneur échouée ; restauration.",
"en": "Container shrink failed; reverting.",
},
"Restoring the original disk from backup…": {
"fr": "Restauration du disque d'origine depuis la sauvegarde…",
"en": "Restoring the original disk from backup…",
},
"Shrinking guest FS + partition via virt-resize": {
"fr": "Réduction du FS invité + partition via virt-resize",
"en": "Shrinking guest FS + partition via virt-resize",
},
"virt-resize failed; original disk left intact.": {
"fr": "virt-resize a échoué ; disque d'origine laissé intact.",
"en": "virt-resize failed; original disk left intact.",
},
"Disk safely shrunk. Backup kept at:": {
"fr": "Disque réduit en toute sécurité. Sauvegarde conservée :",
"en": "Disk safely shrunk. Backup kept at:",
},
"click to expand": {
"fr": "cliquer pour déplier",
"en": "click to expand",
},
"No error detected.": {
"fr": "Aucune erreur détectée.",
"en": "No error detected.",
},
"Test a VM (open Odoo in a CLI browser)": {
"fr": "🧪 Tester une VM (ouvrir Odoo dans un navigateur CLI)",
"en": "🧪 Test a VM (open Odoo in a CLI browser)",
},
"Reopen install monitoring (last run / history)": {
"fr": "📈 Rouvrir le suivi d'installation (dernier run / historique)",
"en": "📈 Reopen install monitoring (last run / history)",
},
"No install run found in history.": {
"fr": "Aucun run d'installation dans l'historique.",
"en": "No install run found in history.",
},
"Install runs (most recent first):": {
"fr": "Runs d'installation (du plus récent) :",
"en": "Install runs (most recent first):",
},
"Choice (number, blank = last): ": {
"fr": "Choix (numéro, vide = le dernier) : ",
"en": "Choice (number, blank = last): ",
},
"Invalid selection.": {
"fr": "Sélection invalide.",
"en": "Invalid selection.",
},
"Resolving VM IP...": {
"fr": "Résolution de l'IP de la VM…",
"en": "Resolving VM IP...",
},
"No IP found for this VM.": {
"fr": "Aucune IP trouvée pour cette VM.",
"en": "No IP found for this VM.",
},
"No CLI browser installed. Which to install?": {
"fr": "Aucun navigateur CLI installé. Lequel installer ?",
"en": "No CLI browser installed. Which to install?",
},
"Which browser to install?": {
"fr": "Quel navigateur installer ?",
"en": "Which browser to install?",
},
"Install another browser": {
"fr": "Installer un autre navigateur",
"en": "Install another browser",
},
"Choice (number, blank = w3m): ": {
"fr": "Choix (numéro, vide = w3m) : ",
"en": "Choice (number, blank = w3m): ",
},
"Unknown package manager; install it manually.": {
"fr": "Gestionnaire de paquets inconnu ; installez-le manuellement.",
"en": "Unknown package manager; install it manually.",
},
"Install now? (y/N): ": {
"fr": "Installer maintenant ? (o/N) : ",
"en": "Install now? (y/N): ",
},
"Which browser to view the page?": {
"fr": "Quel navigateur pour voir la page ?",
"en": "Which browser to view the page?",
},
"Choice (number, blank = first): ": {
"fr": "Choix (numéro, vide = le premier) : ",
"en": "Choice (number, blank = first): ",
},
"Page may not have loaded: Odoo not started on :8069, "
"or network/firewall.": {
"fr": "La page ne s'est peut-être pas affichée : Odoo n'est pas "
"démarré sur :8069, ou réseau/pare-feu.",
"en": "Page may not have loaded: Odoo not started on :8069, "
"or network/firewall.",
},
"errors": {
"fr": "erreurs",
"en": "errors",
},
"warnings": {
"fr": "avertissements",
"en": "warnings",
},
"Esc to close": {
"fr": "Échap pour fermer",
"en": "Esc to close",
},
"No running VM to pause.": {
"fr": "Aucune VM en cours à mettre en pause.",
"en": "No running VM to pause.",
},
"No paused VM to resume.": {
"fr": "Aucune VM en pause à reprendre.",
"en": "No paused VM to resume.",
},
"resumed": {
"fr": "reprise(s)",
"en": "resumed",
},
"SSH grow failed; trying the guest agent.": {
"fr": "Échec SSH ; tentative via l'agent invité.",
"en": "SSH grow failed; trying the guest agent.",
},
"No IP; trying the guest agent (no network).": {
"fr": "Pas d'IP ; tentative via l'agent invité (sans réseau).",
"en": "No IP; trying the guest agent (no network).",
},
"Guest filesystem grown via guest agent.": {
"fr": "FS invité étendu via l'agent invité.",
"en": "Guest filesystem grown via guest agent.",
},
"Guest agent grow failed; falling back to console.": {
"fr": "Échec de l'agent invité ; repli sur la console.",
"en": "Guest agent grow failed; falling back to console.",
},
"Guest agent unavailable; falling back to serial console.": {
"fr": "Agent invité indisponible ; repli sur la console série.",
"en": "Guest agent unavailable; falling back to serial console.",
},
"Running via guest agent (no network)…": {
"fr": "Exécution via l'agent invité (sans réseau)…",
"en": "Running via guest agent (no network)…",
},
"SSH grow failed; falling back to serial console.": {
"fr": "Échec de l'extension via SSH ; repli sur la console série.",
"en": "SSH grow failed; falling back to serial console.",
},
"No IP; falling back to serial console.": {
"fr": "Pas d'IP ; repli sur la console série.",
"en": "No IP; falling back to serial console.",
},
"Serial console fallback. Log in, then paste:": {
"fr": "Repli console série. Connectez-vous, puis collez :",
"en": "Serial console fallback. Log in, then paste:",
},
"Open the serial console now? (y/N): ": {
"fr": "Ouvrir la console série maintenant ? (o/N) : ",
"en": "Open the serial console now? (y/N): ",
},
"The VM must be off. Shut it down and retry? (y/N): ": {
"fr": "La VM doit être éteinte. L'éteindre et réessayer ? (o/N) : ",
"en": "The VM must be off. Shut it down and retry? (y/N): ",
},
"VM is still not off; aborting.": {
"fr": "La VM n'est toujours pas éteinte ; abandon.",
"en": "VM is still not off; aborting.",
},
"Waiting for the VM to shut down...": {
"fr": "Attente de l'arrêt de la VM…",
"en": "Waiting for the VM to shut down...",
},
"timeout": {
"fr": "délai max",
"en": "timeout",
},
"shutting down": {
"fr": "arrêt en cours",
"en": "shutting down",
},
"remaining": {
"fr": "restantes",
"en": "remaining",
},
"VM is off.": {
"fr": "VM éteinte.",
"en": "VM is off.",
},
"The VM was shut down for the resize.": {
"fr": "La VM a été éteinte pour le redimensionnement.",
"en": "The VM was shut down for the resize.",
},
"Start the VM now? (y/N): ": {
"fr": "Démarrer la VM maintenant ? (o/N) : ",
"en": "Start the VM now? (y/N): ",
},
"Graceful shutdown timed out. Force off (destroy)? (y/N): ": {
"fr": "Arrêt gracieux trop long. Forcer l'arrêt (destroy) ? (o/N) : ",
"en": "Graceful shutdown timed out. Force off (destroy)? (y/N): ",
},
"Show advanced info (vCPU, RAM, disk)? (y/N): ": {
"fr": "Afficher les infos avancées (vCPU, RAM, disque) ? (o/N) : ",
"en": "Show advanced info (vCPU, RAM, disk)? (y/N): ",
},
"What do you want to do?": {
"fr": "Que voulez-vous faire ?",
"en": "What do you want to do?",
},
"Advanced info (vCPU, RAM, disk)": {
"fr": "Infos avancées (vCPU, RAM, disque)",
"en": "Advanced info (vCPU, RAM, disk)",
},
"Change the state of one or more VMs": {
"fr": "Changer l'état d'une ou plusieurs VM",
"en": "Change the state of one or more VMs",
},
"Enter": {
"fr": "Entrée",
"en": "Enter",
},
"Nothing": {
"fr": "Rien",
"en": "Nothing",
},
"Choice: ": {
"fr": "Choix : ",
"en": "Choice: ",
},
"Available VMs:": {
"fr": "VM disponibles :",
"en": "Available VMs:",
},
"VMs to change (comma-separated): ": {
"fr": "VM à modifier (séparées par des virgules) : ",
"en": "VMs to change (comma-separated): ",
},
"Unknown VM(s):": {
"fr": "VM inconnue(s) :",
"en": "Unknown VM(s):",
},
"Target state:": {
"fr": "État cible :",
"en": "Target state:",
},
"Target environment?": {
"fr": "Environnement cible ?",
"en": "Target environment?",
},
"Development (~/git/erplibre, SELinux relaxed)": {
"fr": "Développement (~/git/erplibre, SELinux relâché)",
"en": "Development (~/git/erplibre, SELinux relaxed)",
},
"Production (/opt/erplibre, SELinux enforced)": {
"fr": "Production (/opt/erplibre, SELinux confiné)",
"en": "Production (/opt/erplibre, SELinux enforced)",
},
"Choice (1-2, default 1): ": {
"fr": "Choix (1-2, défaut 1) : ",
"en": "Choice (1-2, default 1): ",
},
"Open (start)": {
"fr": "Ouvrir (démarrer)",
"en": "Open (start)",
},
"Close (shut down)": {
"fr": "Fermer (éteindre)",
"en": "Close (shut down)",
},
"start": {
"fr": "démarrer",
"en": "start",
},
"shut down": {
"fr": "éteindre",
"en": "shut down",
},
"Apply:": {
"fr": "Appliquer :",
"en": "Apply:",
},
"Confirm for real? (y/N): ": {
"fr": "Confirmer pour de vrai ? (o/N) : ",
"en": "Confirm for real? (y/N): ",
},
"Storage": {
"fr": "Stockage",
"en": "Storage",
},
"total": {
"fr": "au total",
"en": "total",
},
"used": {
"fr": "utilisés",
"en": "used",
},
"Enter +NG to grow, -NG to shrink, or NG for a target size "
"(e.g. +20G, -10G, 60G).": {
"fr": "Entrez +NG pour agrandir, -NG pour réduire, ou NG pour une "
"taille cible (ex. +20G, -10G, 60G).",
"en": "Enter +NG to grow, -NG to shrink, or NG for a target size "
"(e.g. +20G, -10G, 60G).",
},
"Resize: ": {
"fr": "Redimensionner : ",
"en": "Resize: ",
},
"Invalid size.": {
"fr": "Taille invalide.",
"en": "Invalid size.",
},
"No change.": {
"fr": "Aucun changement.",
"en": "No change.",
},
"New virtual size:": {
"fr": "Nouvelle taille virtuelle :",
"en": "New virtual size:",
},
"SHRINKING is DANGEROUS: the guest filesystem is NOT shrunk. "
"Data beyond the new size is LOST. Shrink the guest FS FIRST, "
"and only then shrink here.": {
"fr": "RÉDUIRE est DANGEREUX : le système de fichiers invité n'est "
"PAS réduit. Les données au-delà de la nouvelle taille sont PERDUES. "
"Réduisez d'ABORD le FS invité, puis seulement ici.",
"en": "SHRINKING is DANGEROUS: the guest filesystem is NOT shrunk. "
"Data beyond the new size is LOST. Shrink the guest FS FIRST, "
"and only then shrink here.",
},
"Shut the VM off before shrinking (virsh shutdown).": {
"fr": "Éteignez la VM avant de réduire (virsh shutdown).",
"en": "Shut the VM off before shrinking (virsh shutdown).",
},
"Type y to confirm you understand the risk (y/N): ": {
"fr": "Tapez o pour confirmer que vous comprenez le risque "
"(o/N, défaut : non) : ",
"en": "Type y to confirm you understand the risk (y/N): ",
},
"Resize failed (see error above).": {
"fr": "Échec du redimensionnement (voir l'erreur ci-dessus).",
"en": "Resize failed (see error above).",
},
"Virtual disk resized.": {
"fr": "Disque virtuel redimensionné.",
"en": "Virtual disk resized.",
},
"Grow the guest filesystem now (over SSH)? (y/N): ": {
"fr": "Étendre le système de fichiers invité maintenant (via SSH) ? "
"(o/N, défaut : non) : ",
"en": "Grow the guest filesystem now (over SSH)? (y/N): ",
},
"No IP; grow the guest FS manually once booted.": {
"fr": "Pas d'IP ; étendez le FS invité manuellement après le boot.",
"en": "No IP; grow the guest FS manually once booted.",
},
"completed": {
"fr": "terminées",
"en": "completed",
},
"deleted": {
"fr": "effacée",
"en": "deleted",
},
"paused": {
"fr": "pause",
"en": "paused",
},
"Waiting for the VM to start (boot + cloud-init)": {
"fr": "En attente du démarrage de la VM (boot + cloud-init)",
"en": "Waiting for the VM to start (boot + cloud-init)",
},
"(an emulated architecture can be slow; this is normal)": {
"fr": "(une architecture émulée peut être lente ; c'est normal)",
"en": "(an emulated architecture can be slow; this is normal)",
},
"VM ready - starting the ERPLibre install": {
"fr": "VM prête — installation ERPLibre en cours",
"en": "VM ready - starting the ERPLibre install",
},
"Choice (number or name, blank = native):": {
"fr": "Choix (numéro ou nom, vide = native) :",
"en": "Choice (number or name, blank = native):",
},
"Pick exact versions (comma-separated list)": {
"fr": "Choisir des versions précises (liste séparée par des virgules)",
"en": "Pick exact versions (comma-separated list)",
},
"Selection (numbers, 'all', 'principal' or 'granulaire',"
" default: all): ": {
"fr": "Sélection (numéros, « all », « principal » ou « granulaire »,"
" défaut : all) : ",
"en": "Selection (numbers, 'all', 'principal' or 'granulaire',"
" default: all): ",
},
"All versions:": {
"fr": "Toutes les versions :",
"en": "All versions:",
},
"Selection (comma-separated numbers): ": {
"fr": "Sélection (numéros séparés par des virgules) : ",
"en": "Selection (comma-separated numbers): ",
},
"s390x images only exist for:": {
"fr": "images s390x disponibles seulement pour :",
"en": "s390x images only exist for:",
},
"ignored:": {
"fr": "ignoré :",
"en": "ignored:",
},
"IBM Z — emulated, slow on x86": {
"fr": "IBM Z — émulé, lent sur x86",
"en": "IBM Z — emulated, slow on x86",
},
"Choice (number or name, blank = amd64):": {
"fr": "Choix (numéro ou nom, vide = amd64) :",
"en": "Choice (number or name, blank = amd64):",
},
"s390x is emulated (TCG): boot and install are much "
"slower than x86.": {
"fr": "s390x est émulé (TCG) : le boot et l'installation sont bien "
"plus lents que x86.",
"en": "s390x is emulated (TCG): boot and install are much "
"slower than x86.",
},
"Choice (number or version, blank = default):": {
"fr": "Choix (numéro ou version, vide = défaut) :",
"en": "Choice (number or version, blank = default):",
},
"Invalid selection, using": {
"fr": "Sélection invalide, utilisation de",
"en": "Invalid selection, using",
},
"select all": {
"fr": "tout sélectionner",
"en": "select all",
},
"Delete VM(s)": {
[ADD] qemu: build the tunnel to the remote desktop A "Remote desktop tunnel" entry under SSH configuration. It lists the targets, resolves them, and composes the full command -- nothing to fill in. todo.py runs on the libvirt HOST; the tunnel starts from the workstation. It cannot open it, but it alone knows the VM's private IP, its port and the address through which it was reached. That last one comes from SSH_CONNECTION, whose third field is exactly the server address used -- far safer than a "hostname" that may resolve to nothing outside. When ~/.ssh/config already carries the entry, the tunnel takes it: "ssh -N -L 5902:localhost:5901 <vm>". ProxyJump makes the route and "localhost" means the VM's OWN loopback, so the tunnel survives an IP change. That config is also the right source of targets, not the local libvirt: a graphical VM is often nested, and virsh would only ever list the orchestrator. Two details: the hypervisor console needs VNC bound to the loopback, as "listen=none" opens no socket at all; and two menu entries had no icon. --- FR --- Une entrée « Tunnel bureau distant » sous Configuration SSH. Elle liste les cibles, les résout, et compose la commande complète — rien à remplir. todo.py tourne sur l'HÔTE libvirt ; le tunnel, lui, part du poste de travail. Il ne peut donc pas l'ouvrir, mais il est le seul à connaître l'IP privée de la VM, son port et l'adresse par laquelle on l'a joint. Cette dernière vient de SSH_CONNECTION, dont le troisième champ est exactement l'adresse serveur utilisée — bien plus sûr qu'un « hostname » qui peut ne rien résoudre depuis l'extérieur. Quand ~/.ssh/config porte déjà l'entrée, le tunnel l'emprunte : « ssh -N -L 5902:localhost:5901 <vm> ». Le ProxyJump fait la route et « localhost » désigne le bouclage DE LA VM, si bien que le tunnel survit à un changement d'IP. Ce fichier est aussi la bonne source de cibles, pas le libvirt local : une VM graphique est souvent imbriquée, et virsh ne listerait jamais que l'orchestrateur. Deux détails : la console de l'hyperviseur exige un VNC sur la boucle locale, « listen=none » n'ouvrant aucun socket ; et deux entrées de menu n'avaient pas d'icône. Assisted-by: Claude Opus 5
2026-08-13 03:40:22 -04:00
"fr": "🗑 Effacer une ou plusieurs VM",
"en": "🗑 Delete VM(s)",
},
"No VM found.": {
"fr": "Aucune VM trouvée.",
"en": "No VM found.",
},
"Select VMs to delete:": {
"fr": "Sélectionner les VM à effacer :",
"en": "Select VMs to delete:",
},
"Selection (numbers, or 'all'): ": {
"fr": "Sélection (numéros, ou « all ») : ",
"en": "Selection (numbers, or 'all'): ",
},
"Also delete disk images (qcow2 + seed ISO)? (y/N): ": {
"fr": "Effacer aussi les disques (qcow2 + seed ISO) ? (o/N, défaut : non) : ",
"en": "Also delete disk images (qcow2 + seed ISO)? (y/N, default: no): ",
},
"Will delete:": {
"fr": "Sera effacé :",
"en": "Will delete:",
},
"disk images and seed ISOs": {
"fr": "les disques et les seed ISO",
"en": "disk images and seed ISOs",
},
"disks kept": {
"fr": "disques conservés",
"en": "disks kept",
},
"Confirm deletion? (y/N): ": {
"fr": "Confirmer l'effacement ? (o/N, défaut : non) : ",
"en": "Confirm deletion? (y/N, default: no): ",
},
"Deletion done.": {
"fr": "Effacement terminé.",
"en": "Deletion done.",
},
"Clean up QEMU (orphan files)": {
"fr": "🧹 Nettoyer QEMU (fichiers orphelins)",
"en": "🧹 Clean up QEMU (orphan files)",
},
"Scanning for orphan QEMU files...": {
"fr": "Recherche des fichiers QEMU orphelins...",
"en": "Scanning for orphan QEMU files...",
},
"orphan disk": {
"fr": "disque orphelin",
"en": "orphan disk",
},
"orphan seed": {
"fr": "seed orphelin",
"en": "orphan seed",
},
"partial download": {
"fr": "téléchargement interrompu",
"en": "partial download",
},
"orphan UEFI nvram": {
"fr": "nvram UEFI orpheline",
"en": "orphan UEFI nvram",
},
"No orphan files found.": {
"fr": "Aucun fichier orphelin trouvé.",
"en": "No orphan files found.",
},
"Orphan files:": {
"fr": "Fichiers orphelins :",
"en": "Orphan files:",
},
"Total:": {
"fr": "Total :",
"en": "Total:",
},
"files": {
"fr": "fichiers",
"en": "files",
},
"Delete these orphan files? (y/N): ": {
"fr": "Effacer ces fichiers orphelins ? (o/N, défaut : non) : ",
"en": "Delete these orphan files? (y/N, default: no): ",
},
"Cleanup done.": {
"fr": "Nettoyage terminé.",
"en": "Cleanup done.",
},
"Cached base images (reusable):": {
"fr": "Images de base en cache (réutilisables) :",
"en": "Cached base images (reusable):",
},
"Also delete the cached base images? (y/N): ": {
"fr": "Effacer aussi les images de base en cache ? (o/N, défaut : non) : ",
"en": "Also delete the cached base images? (y/N, default: no): ",
},
"All cached base images (reusable):": {
"fr": "Toutes les images de base en cache (réutilisables) :",
"en": "All cached base images (reusable):",
},
"Delete ALL cached base images? (y/N): ": {
"fr": "Effacer TOUTES les images de base en cache ? (o/N, défaut : non) : ",
"en": "Delete ALL cached base images? (y/N, default: no): ",
},
"Ghost domains (defined but disk missing):": {
"fr": "Domaines fantômes (définis, disque manquant) :",
"en": "Ghost domains (defined but disk missing):",
},
"Undefine these ghost domains? (y/N): ": {
"fr": "Supprimer la définition de ces domaines ? (o/N, défaut : non) : ",
"en": "Undefine these ghost domains? (y/N, default: no): ",
},
"Stale codename-named Ubuntu images (duplicates):": {
"fr": "Images Ubuntu nommées par codename (doublons) :",
"en": "Stale codename-named Ubuntu images (duplicates):",
},
"Delete these duplicate images? (y/N): ": {
"fr": "Effacer ces images en double ? (o/N, défaut : non) : ",
"en": "Delete these duplicate images? (y/N, default: no): ",
},
"Orphan ~/.ssh/config entries:": {
"fr": "Entrées ~/.ssh/config orphelines :",
"en": "Orphan ~/.ssh/config entries:",
},
"Remove these ~/.ssh/config entries? (y/N): ": {
"fr": "Retirer ces entrées de ~/.ssh/config ? (o/N, défaut : non) : ",
"en": "Remove these ~/.ssh/config entries? (y/N, default: no): ",
},
"Stale DHCP leases (no matching VM):": {
"fr": "Baux DHCP périmés (aucune VM correspondante) :",
"en": "Stale DHCP leases (no matching VM):",
},
"Clear these stale leases? (y/N): ": {
"fr": "Effacer ces baux périmés ? (o/N, défaut : non) : ",
"en": "Clear these stale leases? (y/N, default: no): ",
},
"Deploy ERPLibre infra (one minimal VM per image)": {
"fr": "Déployer l'infra ERPLibre (une VM minimale par image)",
"en": "Deploy ERPLibre infra (one minimal VM per image)",
},
"Deploy ERPLibre infra: one minimal VM per image": {
"fr": "Déployer l'infra ERPLibre : une VM minimale par image",
"en": "Deploy ERPLibre infra: one minimal VM per image",
},
"Cannot load QEMU catalog: ": {
"fr": "Impossible de charger le catalogue QEMU : ",
"en": "Cannot load QEMU catalog: ",
},
"Distributions:": {
"fr": "Distributions :",
"en": "Distributions:",
},
"Whole catalog (every version)": {
"fr": "Tout le catalogue (chaque version)",
"en": "Whole catalog (every version)",
},
"The main version of each distro (marked *)": {
"fr": "La version principale de chaque distro (marquée *)",
"en": "The main version of each distro (marked *)",
},
"Selection (numbers, 'all' or 'principal', default: all): ": {
"fr": "Sélection (numéros, « all » ou « principal », défaut : all) : ",
"en": "Selection (numbers, 'all' or 'principal', default: all): ",
},
"Selection (numbers, or 'all', default: all): ": {
"fr": "Sélection (numéros, ou « all », défaut : all) : ",
"en": "Selection (numbers, or 'all', default: all): ",
},
"Nothing selected.": {
"fr": "Rien de sélectionné.",
"en": "Nothing selected.",
},
"Deployment plan": {
"fr": "Plan de déploiement",
"en": "Deployment plan",
},
"disk": {
"fr": "disque",
"en": "disk",
},
"Total RAM (all running):": {
"fr": "RAM totale (toutes actives) :",
"en": "Total RAM (all running):",
},
"Total virtual disk (thin qcow2):": {
"fr": "Disque virtuel total (qcow2 thin) :",
"en": "Total virtual disk (thin qcow2):",
},
"Host RAM available:": {
"fr": "RAM disponible de l'hôte :",
"en": "Host RAM available:",
},
"Total RAM exceeds host free RAM: not all VMs will run at once.": {
"fr": "La RAM totale dépasse la RAM libre de l'hôte : les VM ne "
"tourneront pas toutes en même temps.",
"en": "Total RAM exceeds host free RAM: not all VMs will run at once.",
},
"Install ERPLibre into ~/git/erplibre on each VM? (Y/n): ": {
"fr": "Installer ERPLibre dans ~/git/erplibre sur chaque VM ? "
"(O/n, défaut : oui) : ",
"en": "Install ERPLibre into ~/git/erplibre on each VM? "
"(Y/n, default: yes): ",
},
"Deploy these VMs now? (Y/n): ": {
"fr": "Déployer ces VM maintenant ? (O/n, défaut : oui) : ",
"en": "Deploy these VMs now? (Y/n, default: yes): ",
},
"Discard everything and start over? (y/N): ": {
"fr": "Tout abandonner et recommencer ? (o/N, défaut : non) : ",
"en": "Discard everything and start over? (y/N, default: no): ",
},
"Final review before deployment": {
"fr": "Récapitulatif final avant déploiement",
"en": "Final review before deployment",
},
"VMs to create:": {
"fr": "VM à créer :",
"en": "VMs to create:",
},
"Existing, left untouched:": {
"fr": "Existantes, laissées intactes :",
"en": "Existing, left untouched:",
},
"ERPLibre install:": {
"fr": "Installation ERPLibre :",
"en": "ERPLibre install:",
},
"profile": {
"fr": "profil",
"en": "profile",
},
"branch": {
"fr": "branche",
"en": "branch",
},
"production (/opt, confined)": {
"fr": "production (/opt, confiné)",
"en": "production (/opt, confined)",
},
"development (~/git)": {
"fr": "développement (~/git)",
"en": "development (~/git)",
},
"no": {
"fr": "non",
"en": "no",
},
"SSH key:": {
"fr": "Clé SSH :",
"en": "SSH key:",
},
"~/.ssh/config:": {
"fr": "~/.ssh/config :",
"en": "~/.ssh/config:",
},
"one entry per VM": {
"fr": "une entrée par VM",
"en": "one entry per VM",
},
"untouched": {
"fr": "inchangé",
"en": "untouched",
},
"Parallelism:": {
"fr": "Parallélisme :",
"en": "Parallelism:",
},
"at a time": {
"fr": "à la fois",
"en": "at a time",
},
"Nothing to create - every VM already exists.": {
"fr": "Rien à créer — toutes les VM existent déjà.",
"en": "Nothing to create — every VM already exists.",
},
"Name collisions detected": {
"fr": "Collisions de noms détectées",
"en": "Name collisions detected",
},
"VM already defined - SKIPPED, nothing overwritten": {
"fr": "VM déjà définie — IGNORÉE, rien ne sera écrasé",
"en": "VM already defined — SKIPPED, nothing overwritten",
},
"disk present without VM - deployment will FAIL": {
"fr": "disque présent sans VM — le déploiement ÉCHOUERA",
"en": "disk present without VM - deployment will FAIL",
},
"Remove it by hand, or rename the VM.": {
"fr": "L'effacer à la main, ou renommer la VM.",
"en": "Remove it by hand, or rename the VM.",
},
"Continue despite these collisions? (y/N): ": {
"fr": "Continuer malgré ces collisions ? (o/N, défaut : non) : ",
"en": "Continue despite these collisions? (y/N, default: no): ",
},
"Resolving VM IPs (parallel, emulated boot is slow)...": {
"fr": "Résolution des IP des VM (en parallèle, le démarrage émulé "
"est lent)...",
"en": "Resolving VM IPs (parallel, emulated boot is slow)...",
},
"no IP": {
"fr": "pas d'IP",
"en": "no IP",
},
"Cloning ERPLibre on each VM": {
"fr": "Clonage d'ERPLibre sur chaque VM",
"en": "Cloning ERPLibre on each VM",
},
"Installing ERPLibre on each VM": {
"fr": "Installation d'ERPLibre sur chaque VM",
"en": "Installing ERPLibre on each VM",
},
"Install ERPLibre into ~/git/erplibre on this VM? (y/N): ": {
"fr": "Installer ERPLibre dans ~/git/erplibre sur cette VM ? (o/N, défaut : non) : ",
"en": "Install ERPLibre into ~/git/erplibre on this VM? (y/N, default: no): ",
},
"installing ERPLibre": {
"fr": "installation d'ERPLibre",
"en": "installing ERPLibre",
},
"no IP obtained, ERPLibre install skipped.": {
"fr": "aucune IP obtenue, installation ERPLibre ignorée.",
"en": "no IP obtained, ERPLibre install skipped.",
},
"waiting for SSH...": {
"fr": "attente du SSH...",
"en": "waiting for SSH...",
},
"SSH not reachable, ERPLibre install skipped.": {
"fr": "SSH injoignable, installation ERPLibre ignorée.",
"en": "SSH not reachable, ERPLibre install skipped.",
},
"What to install on the VM(s)?": {
"fr": "Que veut-on installer sur la/les VM ?",
"en": "What to install on the VM(s)?",
},
"Choice (number, blank = Odoo 18): ": {
"fr": "Choix (numéro, vide = Odoo 18) : ",
"en": "Choice (number, blank = Odoo 18): ",
},
"ERPLibre + all Odoo versions": {
"fr": "ERPLibre + toutes les versions Odoo",
"en": "ERPLibre + all Odoo versions",
},
"ERPLibre only (no Odoo)": {
"fr": "ERPLibre seulement (sans Odoo)",
"en": "ERPLibre only (no Odoo)",
},
"ERPLibre mobile (home)": {
"fr": "ERPLibre mobile (home)",
"en": "ERPLibre mobile (home)",
},
"ERPLibre Deployment (+ QEMU + dev)": {
"fr": "ERPLibre Déploiement (+ QEMU + dev)",
"en": "ERPLibre Deployment (+ QEMU + dev)",
},
"Interactive monitoring dashboard? (y/N): ": {
"fr": "Suivi interactif (dashboard) ? (O/n, défaut : oui) : ",
"en": "Interactive monitoring dashboard? (Y/n, default: yes): ",
},
"resolving IP...": {
"fr": "résolution de l'IP...",
"en": "resolving IP...",
},
"no IP, skipped.": {
"fr": "pas d'IP, ignorée.",
"en": "no IP, skipped.",
},
"No VM to install.": {
"fr": "Aucune VM à installer.",
"en": "No VM to install.",
},
"Opening the interactive monitor...": {
"fr": "Ouverture du suivi interactif...",
"en": "Opening the interactive monitor...",
},
"Monitor closed. Installs keep running in the background.": {
"fr": "Suivi fermé. Les installations continuent en arrière-plan.",
"en": "Monitor closed. Installs keep running in the background.",
},
"Logs:": {
"fr": "Logs :",
"en": "Logs:",
},
"Read the logs:": {
"fr": "Lire les logs :",
"en": "Read the logs:",
},
"Log files:": {
"fr": "Fichiers de log :",
"en": "Log files:",
},
"Parallel deployments (default:": {
"fr": "Déploiements en parallèle (défaut :",
"en": "Parallel deployments (default:",
},
"VMs": {
"fr": "VM",
"en": "VMs",
},
"Deploy summary:": {
"fr": "Bilan déploiement :",
"en": "Deploy summary:",
},
"IPs resolved:": {
"fr": "IP résolues :",
"en": "IPs resolved:",
},
"still waiting for": {
"fr": "encore en attente de",
"en": "still waiting for",
},
"TOTAL summary": {
"fr": "Sommaire TOTAL",
"en": "TOTAL summary",
},
"VMs deployed:": {
"fr": "VM déployées :",
"en": "VMs deployed:",
},
"total incl. existing:": {
"fr": "total avec existantes :",
"en": "total incl. existing:",
},
"Total time:": {
"fr": "Temps total :",
"en": "Total time:",
},
"Deploying": {
"fr": "Déploiement de",
"en": "Deploying",
},
"parallel jobs:": {
"fr": "tâches parallèles :",
"en": "parallel jobs:",
},
"ERPLibre infra deployment done.": {
"fr": "Déploiement de l'infra ERPLibre terminé.",
"en": "ERPLibre infra deployment done.",
},
"Manage with:": {
"fr": "Gérer avec :",
"en": "Manage with:",
},
"Fetching ERPLibre branch list...": {
"fr": "Récupération de la liste des branches ERPLibre...",
"en": "Fetching ERPLibre branch list...",
},
"Branch (default:": {
"fr": "Branche (défaut :",
"en": "Branch (default:",
},
"Branches:": {
"fr": "Branches :",
"en": "Branches:",
},
"Choice (number or name, default:": {
"fr": "Choix (numéro ou nom, défaut :",
"en": "Choice (number or name, default:",
},
"no IP obtained, ERPLibre clone skipped.": {
"fr": "aucune IP obtenue, clonage ERPLibre ignoré.",
"en": "no IP obtained, ERPLibre clone skipped.",
},
"cloning ERPLibre": {
"fr": "clonage d'ERPLibre",
"en": "cloning ERPLibre",
},
"VM name (required): ": {
"fr": "Nom de la VM (requis) : ",
"en": "VM name (required): ",
},
"VM name is required!": {
"fr": "Le nom de la VM est requis !",
"en": "VM name is required!",
},
"VM name: ": {
"fr": "Nom de la VM : ",
"en": "VM name: ",
},
"VM name or ID: ": {
"fr": "Nom ou ID de la VM : ",
"en": "VM name or ID: ",
},
"VM name or ID (or 'all'): ": {
"fr": "Nom ou ID de la VM (ou « all ») : ",
"en": "VM name or ID (or 'all'): ",
},
"RAM in MB (blank = version minimum): ": {
"fr": "RAM en Mo (vide = minimum de la version) : ",
"en": "RAM in MB (blank = version minimum): ",
},
"vCPUs (default: 2): ": {
"fr": "vCPU (défaut : 2) : ",
"en": "vCPUs (default: 2): ",
},
"Disk size (blank = version min; e.g. 30G, 1T): ": {
"fr": "Taille du disque (vide = min. version ; ex. 30G, 1T) : ",
"en": "Disk size (blank = version min; e.g. 30G, 1T): ",
},
"VM name (default: ": {
"fr": "Nom de la VM (défaut : ",
"en": "VM name (default: ",
},
"Deployment failed (see the error above).": {
"fr": "Échec du déploiement (voir l'erreur ci-dessus).",
"en": "Deployment failed (see the error above).",
},
"SSH public key path": {
"fr": "Chemin de la clé publique SSH",
"en": "SSH public key path",
},
"Timezone": {
"fr": "Fuseau horaire",
"en": "Timezone",
},
"virsh cannot reach qemu:///system without sudo.": {
"fr": "virsh ne peut pas joindre qemu:///system sans sudo.",
"en": "virsh cannot reach qemu:///system without sudo.",
},
"The install monitor runs detached and cannot type a": {
"fr": "Le suivi d'installation tourne détaché et ne peut pas saisir",
"en": "The install monitor runs detached and cannot type a",
},
"password: it would lose the VM when its lease moves.": {
"fr": "de mot de passe : il perdrait la VM au changement de bail.",
"en": "password: it would lose the VM when its lease moves.",
},
"You are in the libvirt group, but this session": {
"fr": "Vous êtes dans le groupe libvirt, mais cette session est",
"en": "You are in the libvirt group, but this session",
},
"predates it. Log out and back in, or run:": {
"fr": "antérieure. Reconnectez-vous, ou lancez :",
"en": "predates it. Log out and back in, or run:",
},
"Group is active, so the cause is elsewhere:": {
"fr": "Le groupe est actif : la cause est donc ailleurs.",
"en": "Group is active, so the cause is elsewhere:",
},
"Add your user to the libvirt group?": {
"fr": "Ajouter votre utilisateur au groupe libvirt ?",
"en": "Add your user to the libvirt group?",
},
"Run it now? (Y/n): ": {
"fr": "L'exécuter maintenant ? (O/n) : ",
"en": "Run it now? (Y/n): ",
},
"Command failed.": {
"fr": "La commande a échoué.",
"en": "Command failed.",
},
"Added. Log out and back in for it to take effect,": {
"fr": "Ajouté. Reconnectez-vous pour que ça prenne effet,",
"en": "Added. Log out and back in for it to take effect,",
},
"or start a new shell with: newgrp libvirt": {
"fr": "ou ouvrez un shell neuf avec : newgrp libvirt",
"en": "or start a new shell with: newgrp libvirt",
},
"Timezone for the VMs": {
"fr": "Fuseau horaire des VM",
"en": "Timezone for the VMs",
},
"Locale for the VMs": {
"fr": "Locale des VM",
"en": "Locale for the VMs",
},
"KVM is unavailable: the VMs will be EMULATED.": {
"fr": "KVM indisponible : les VM seront ÉMULÉES.",
"en": "KVM is unavailable: the VMs will be EMULATED.",
},
"A boot then takes 10-15 min, not under a minute.": {
"fr": "Un démarrage prend alors 10 à 15 min, pas moins d'une minute.",
"en": "A boot then takes 10-15 min, not under a minute.",
},
"Cause: /dev/kvm is missing. This host is itself a VM": {
"fr": "Cause : /dev/kvm est absent. Cet hôte est lui-même une VM",
"en": "Cause: /dev/kvm is missing. This host is itself a VM",
},
"whose hypervisor does not expose nested virtualization.": {
"fr": "dont l'hyperviseur n'expose pas la virtualisation imbriquée.",
"en": "whose hypervisor does not expose nested virtualization.",
},
"To fix it ON THE PARENT HYPERVISOR, not here:": {
"fr": "Pour corriger, SUR L'HYPERVISEUR PARENT, pas ici :",
"en": "To fix it ON THE PARENT HYPERVISOR, not here:",
},
"then set this VM to the host-passthrough CPU mode and": {
"fr": "puis donner à cette VM le mode CPU « host-passthrough », et",
"en": "then set this VM to the host-passthrough CPU mode and",
},
"stop it and start it again - a reboot is not enough.": {
"fr": "l'arrêter puis la redémarrer — un reboot ne suffit pas.",
"en": "stop it and start it again - a reboot is not enough.",
},
"Without access to that hypervisor, nothing to do here.": {
"fr": "Sans accès à cet hyperviseur, il n'y a rien à régler ici.",
"en": "Without access to that hypervisor, nothing to do here.",
},
"Usual cause: this host is itself a VM without nested": {
"fr": "Cause habituelle : cet hôte est lui-même une VM sans",
"en": "Usual cause: this host is itself a VM without nested",
},
"virtualization. Check: systemd-detect-virt": {
"fr": "virtualisation imbriquée. Vérifier : systemd-detect-virt",
"en": "virtualization. Check: systemd-detect-virt",
},
"Unknown timezone, keeping": {
"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): ",
},
"Overwrite existing VM disk if present? (y/N): ": {
"fr": "Écraser le disque de la VM s'il existe ? (o/N, défaut : non) : ",
"en": "Overwrite existing VM disk if present? (y/N, default: no): ",
},
"Verify SHA256 after download? (y/N): ": {
"fr": "Vérifier le SHA256 après téléchargement ? (o/N, défaut : non) : ",
"en": "Verify SHA256 after download? (y/N, default: no): ",
},
"QEMU - Sample dry-run (demo-vm, Ubuntu 24.04)": {
[ADD] qemu: build the tunnel to the remote desktop A "Remote desktop tunnel" entry under SSH configuration. It lists the targets, resolves them, and composes the full command -- nothing to fill in. todo.py runs on the libvirt HOST; the tunnel starts from the workstation. It cannot open it, but it alone knows the VM's private IP, its port and the address through which it was reached. That last one comes from SSH_CONNECTION, whose third field is exactly the server address used -- far safer than a "hostname" that may resolve to nothing outside. When ~/.ssh/config already carries the entry, the tunnel takes it: "ssh -N -L 5902:localhost:5901 <vm>". ProxyJump makes the route and "localhost" means the VM's OWN loopback, so the tunnel survives an IP change. That config is also the right source of targets, not the local libvirt: a graphical VM is often nested, and virsh would only ever list the orchestrator. Two details: the hypervisor console needs VNC bound to the loopback, as "listen=none" opens no socket at all; and two menu entries had no icon. --- FR --- Une entrée « Tunnel bureau distant » sous Configuration SSH. Elle liste les cibles, les résout, et compose la commande complète — rien à remplir. todo.py tourne sur l'HÔTE libvirt ; le tunnel, lui, part du poste de travail. Il ne peut donc pas l'ouvrir, mais il est le seul à connaître l'IP privée de la VM, son port et l'adresse par laquelle on l'a joint. Cette dernière vient de SSH_CONNECTION, dont le troisième champ est exactement l'adresse serveur utilisée — bien plus sûr qu'un « hostname » qui peut ne rien résoudre depuis l'extérieur. Quand ~/.ssh/config porte déjà l'entrée, le tunnel l'emprunte : « ssh -N -L 5902:localhost:5901 <vm> ». Le ProxyJump fait la route et « localhost » désigne le bouclage DE LA VM, si bien que le tunnel survit à un changement d'IP. Ce fichier est aussi la bonne source de cibles, pas le libvirt local : une VM graphique est souvent imbriquée, et virsh ne listerait jamais que l'orchestrateur. Deux détails : la console de l'hyperviseur exige un VNC sur la boucle locale, « listen=none » n'ouvrant aucun socket ; et deux entrées de menu n'avaient pas d'icône. Assisted-by: Claude Opus 5
2026-08-13 03:40:22 -04:00
"fr": "🧰 QEMU - Exemple dry-run (demo-vm, Ubuntu 24.04)",
"en": "🧰 QEMU - Sample dry-run (demo-vm, Ubuntu 24.04)",
},
# QEMU - statistics screen
"Statistics (installs, durations, VMs)": {
"fr": "📊 Statistiques (installations, durées, VM)",
"en": "📊 Statistics (installs, durations, VMs)",
},
"QEMU statistics": {"fr": "Statistiques QEMU", "en": "QEMU statistics"},
"No installation recorded yet.": {
"fr": "Aucune installation enregistrée pour l'instant.",
"en": "No installation recorded yet.",
},
"Installations": {"fr": "Installations", "en": "Installations"},
"Total": {"fr": "Total", "en": "Total"},
"succeeded": {"fr": "réussies", "en": "succeeded"},
"failed": {"fr": "échouées", "en": "failed"},
"Period": {"fr": "Période", "en": "Period"},
"days": {"fr": "jours", "en": "days"},
"Median duration": {"fr": "Durée médiane", "en": "Median duration"},
"min": {"fr": "min", "en": "min"},
"max": {"fr": "max", "en": "max"},
"Cumulated time": {"fr": "Temps cumulé", "en": "Cumulated time"},
"By distribution": {"fr": "Par distribution", "en": "By distribution"},
"By version": {"fr": "Par version", "en": "By version"},
"By architecture": {"fr": "Par architecture", "en": "By architecture"},
"Virtual machines": {
"fr": "Machines virtuelles",
"en": "Virtual machines",
},
"Defined": {"fr": "Définies", "en": "Defined"},
"running": {"fr": "en cours", "en": "running"},
"stopped": {"fr": "arrêtées", "en": "stopped"},
"Disk used": {"fr": "Disque utilisé", "en": "Disk used"},
"image": {"fr": "image", "en": "image"},
"failure": {"fr": "échec", "en": "failure"},
"Reset the statistics": {
"fr": "Réinitialiser les statistiques",
"en": "Reset the statistics",
},
"Nothing to reset.": {"fr": "Rien à effacer.", "en": "Nothing to reset."},
"Erase": {"fr": "Effacer", "en": "Erase"},
"recorded runs": {"fr": "runs enregistrés", "en": "recorded runs"},
"runs erased": {"fr": "runs effacés", "en": "runs erased"},
"Cancelled.": {"fr": "Annulé.", "en": "Cancelled."},
# Database migration - resume menu
"Migration in progress": {
"fr": "Migration en cours",
"en": "Migration in progress",
},
"File": {"fr": "Fichier", "en": "File"},
"Database": {"fr": "Base", "en": "Database"},
"Target": {"fr": "Cible", "en": "Target"},
"Started": {"fr": "Démarrée", "en": "Started"},
"Steps": {"fr": "Étapes", "en": "Steps"},
"Prepare the environment": {
"fr": "Préparer l'environnement",
"en": "Prepare the environment",
},
"Restore and neutralize the database": {
"fr": "Restaurer et neutraliser la base",
"en": "Restore and neutralize the database",
},
"Update all addons": {
"fr": "Mettre à jour tous les modules",
"en": "Update all addons",
},
"Clean up before data migration": {
"fr": "Nettoyer avant la migration des données",
"en": "Clean up before data migration",
},
"Upgrade version by version (OpenUpgrade)": {
"fr": "Monter de version en version (OpenUpgrade)",
"en": "Upgrade version by version (OpenUpgrade)",
},
"not started": {"fr": "non démarrée", "en": "not started"},
"done": {"fr": "terminée", "en": "done"},
"partially done": {"fr": "partielle", "en": "partially done"},
"version bumps migrated": {
"fr": "montées de version faites",
"en": "version bumps migrated",
},
"Continue where it stopped": {
"fr": "Continuer là où ça s'est arrêté",
"en": "Continue where it stopped",
},
"Replay from that step": {
"fr": "Reprendre à partir de cette étape",
"en": "Replay from that step",
},
"erases the progression of that step and the next ones": {
"fr": "efface la progression de cette étape et des suivantes",
"en": "erases the progression of that step and the next ones",
},
"New migration, erase everything": {
"fr": "Nouvelle migration, tout effacer",
"en": "New migration, erase everything",
},
"Keep the zip only, ask every question again": {
"fr": "Garder seulement le zip, reposer toutes les questions",
"en": "Keep the zip only, ask every question again",
},
"Your choice": {"fr": "Votre choix", "en": "Your choice"},
"Unknown choice, continuing where it stopped": {
"fr": "Choix inconnu, on continue là où ça s'est arrêté",
"en": "Unknown choice, continuing where it stopped",
},
"Replaying from step": {
"fr": "Reprise à partir de l'étape",
"en": "Replaying from step",
},
"Replay the upgrade from version N": {
"fr": "Reprendre la montée à partir de la version N",
"en": "Replay the upgrade from version N",
},
"rebuilds the intermediate database": {
"fr": "recrée la base intermédiaire",
"en": "rebuilds the intermediate database",
},
"Unknown version": {"fr": "Version inconnue", "en": "Unknown version"},
"The progression file is invalid, ignoring it": {
"fr": "Le fichier de progression est invalide, on l'ignore",
"en": "The progression file is invalid, ignoring it",
},
# --- Menu Execute › Analyse ---
# L'émoji vit dans la valeur, jamais dans la clé. Deux clés distinctes,
# comme « Data » / « Database » : l'entrée de menu porte l'émoji,
# l'étiquette du fil d'Ariane est nue — _todo_telemetry_tui traduit le
# dernier segment du chemin, et un émoji y détonnerait.
"Analyse - Odoo database analysis": {
"fr": "🔬 Analyse - Analyse de base de données Odoo",
"en": "🔬 Analyse - Odoo database analysis",
},
"Analyse": {"fr": "Analyse", "en": "Analysis"},
"Analyse a database, without ever writing to it!": {
"fr": "Analyser une base, sans jamais y écrire !",
"en": "Analyse a database, without ever writing to it!",
},
# Les icônes du sous-menu Analyse. Elles sont réservées d'avance pour la
# famille entière — 🧱 index, 🎈 ballonnement, 📎 pièces jointes,
# 🔗 ir_model_data — pour qu'ajouter un outil ne demande pas d'en
# rechercher une libre, et que deux outils voisins ne se ressemblent pas.
"Structure": {"fr": "🗄 Structure", "en": "🗄 Structure"},
"Tables and database size": {
"fr": "📏 Tables et poids de la base",
"en": "📏 Tables and database size",
},
"Customisation": {"fr": "🎭 Personnalisation", "en": "🎭 Customisation"},
"Customised views, website copies included": {
"fr": "🖼 Vues personnalisées, copies de site web comprises",
"en": "🖼 Customised views, website copies included",
},
"Analysis failed: ": {
"fr": "L'analyse a échoué : ",
"en": "Analysis failed: ",
},
"Full list and JSON output:": {
"fr": "Liste complète et sortie JSON :",
"en": "Full list and JSON output:",
},
"A backup holds no registry, so nothing was compared with the module"
" source. The classification above needs none; only the differences do."
" Restore it, or run this on the database.": {
"fr": "Une sauvegarde n'a pas de registre, donc rien n'a été comparé"
" avec la source du module. Le classement ci-dessus n'en a pas besoin"
" ; seuls les écarts en ont. Restaurez-la, ou lancez ceci sur la base.",
"en": "A backup holds no registry, so nothing was compared with the"
" module source. The classification above needs none; only the"
" differences do. Restore it, or run this on the database.",
},
[ADD] migration: keep the previous log when a run restarts The progression log lives at one path and the next migration writes over it. Restarting therefore erased everything known about the run before — which steps passed, which modules were missing, how long each took — and that is exactly what someone looks for after having had to restart. Both answers that discard the progression now copy it aside first, named after the origin database and the moment of the copy, so two attempts on the same database do not cover each other and a file moved out of its folder still says what it is. Partial replays keep the log, so they archive nothing. It lands under private/, not beside the original: reinstalling wipes .venv.erplibre/ and would take the history with it. Two copies in the same second overwrote each other in silence — undoing the very loss this prevents. The second one is numbered now, and the test that had skipped that case asserts it. Checked on the real 51 KB log from the VM: 18 states copied unchanged, the name carries the database and the timestamp, and a log with no state at all is not archived. 12 tests. --- FR --- Le journal de progression vit à un seul endroit et la migration suivante écrit par-dessus. Recommencer effaçait donc tout ce qu'on savait de la tentative précédente — quels paliers étaient passés, quels modules manquaient, combien de temps chacun avait pris — soit exactement ce qu'on cherche après avoir dû recommencer. Les deux réponses qui jettent la progression la copient désormais d'abord, sous un nom portant la base d'origine et l'instant de la copie, pour que deux tentatives sur la même base ne se recouvrent pas et qu'un fichier sorti de son dossier se décrive encore. Une reprise partielle garde le journal, donc n'archive rien. La copie va sous private/, pas à côté de l'originale : une réinstallation efface .venv.erplibre/ et emporterait l'historique avec elle. Deux copies dans la même seconde s'écrasaient en silence — annulant la perte même qu'on évite. La seconde est numérotée, et le test qui sautait ce cas l'affirme. Vérifié sur le vrai journal de 51 Ko de la VM : 18 états copiés à l'identique, le nom porte la base et l'horodatage, et un journal sans aucun état n'est pas archivé. 12 tests. Assisted-by: Claude Opus 5
2026-08-12 00:34:17 -04:00
"Could not archive the previous log": {
"fr": "Impossible d'archiver le journal précédent",
"en": "Could not archive the previous log",
},
"Previous migration log kept in": {
"fr": "Journal de la migration précédente conservé dans",
"en": "Previous migration log kept in",
},
[FIX] migration: updating the addons early left step 2 « not started » A database coming from an old version is updated before the neutralization — the tool offers it at the very start. That work IS step 2, done earlier, but it was only recorded as state_1_update_all while the screen reads state_2_*. So the step stayed « not started » right after running. The costly half was invisible. Step 2 skipped the work through a local variable, which is False again on a resume: with only the old flag written, resuming a migration re-ran update_addons_all on an already-updated database. Hours, for nothing. The check now reads the written trace, not the session. The screen says when it happened rather than just « done », since the moment is exactly what raised the doubt. Logs written before this fix read correctly without being touched — the early flag alone is recognised. Checked on the real log from the VM: step 2 goes from « not started » to « done early, before the neutralization », the four other steps unchanged. 10 tests; removing the fix makes the resume one fail. --- FR --- Une base venant d'une vieille version se met à jour avant la neutralisation — l'outil le propose au tout début. Ce travail EST l'étape 2, faite plus tôt, mais il n'était enregistré que sous state_1_update_all quand l'écran lit state_2_*. L'étape restait donc « non démarrée » juste après avoir tourné. La moitié coûteuse était invisible. L'étape 2 sautait le travail grâce à une variable locale, remise à False à la reprise : avec seulement l'ancien drapeau écrit, reprendre une migration relançait update_addons_all sur une base déjà à jour. Des heures, pour rien. Le contrôle lit maintenant la trace écrite, pas la session. L'écran dit quand cela a eu lieu plutôt que « terminée », le moment étant justement la source du doute. Les journaux écrits avant ce correctif se lisent correctement sans être modifiés — le drapeau précoce seul est reconnu. Vérifié sur le vrai journal de la VM : l'étape 2 passe de « non démarrée » à « faite plus tôt, avant la neutralisation », les quatre autres inchangées. 10 tests ; retirer le correctif fait tomber celui de la reprise. Assisted-by: Claude Opus 5
2026-08-12 00:43:16 -04:00
"done early, before the neutralization": {
"fr": "faite plus tôt, avant la neutralisation",
"en": "done early, before the neutralization",
},
"compare each website copy with the module view it shadows": {
"fr": "comparer chaque copie de site web à la vue de module qu'elle"
" masque",
"en": "compare each website copy with the module view it shadows",
},
"Compare the website copies with the view they shadow": {
"fr": "🪞 Comparer les copies de site web à la vue qu'elles masquent",
"en": "🪞 Compare the website copies with the view they shadow",
},
"website copies compared with their module view,": {
"fr": "copies de site web comparées à leur vue de module,",
"en": "website copies compared with their module view,",
},
"differ.": {"fr": "diffèrent.", "en": "differ."},
"What do you want to do with these COW copies?": {
"fr": "Que voulez-vous faire de ces copies COW ?",
"en": "What do you want to do with these COW copies?",
},
"Enter = decide at the version bump": {
"fr": "Entrée = décider au palier de version",
"en": "Enter = decide at the version bump",
},
"what each copy holds": {
"fr": "ce que chaque copie porte",
"en": "what each copy holds",
},
"why it breaks": {
"fr": "pourquoi ça casse",
"en": "why it breaks",
},
"full screen": {
"fr": "plein écran",
"en": "full screen",
},
"neutralize now, reversible": {
"fr": "neutraliser maintenant, réversible",
"en": "neutralize now, reversible",
},
"Undo with": {
"fr": "Annuler avec",
"en": "Undo with",
},
"Nothing decided": {
"fr": "Rien de décidé",
"en": "Nothing decided",
},
"the migration will ask again at the version bump.": {
"fr": "la migration reposera la question au palier de version.",
"en": "the migration will ask again at the version bump.",
},
"Target version directory not found": {
"fr": "Répertoire de la version cible introuvable",
"en": "Target version directory not found",
},
"No website COW view changes shape in": {
"fr": "Aucune vue COW de site ne change de forme dans",
"en": "No website COW view changes shape in",
},
"website COW view(s) will break when moving to": {
"fr": "vue(s) COW de site casseront au passage à",
"en": "website COW view(s) will break when moving to",
},
"the copy keeps an arch whose shape no longer matches what": {
"fr": "la copie garde une arch dont la forme ne correspond plus à ce que",
"en": "the copy keeps an arch whose shape no longer matches what",
},
"the target module view expects.": {
"fr": "la vue module cible attend.",
"en": "the target module view expects.",
},
"The migration offers to neutralize them, and shows what": {
"fr": "La migration propose de les neutraliser, et montre ce que",
"en": "The migration offers to neutralize them, and shows what",
},
"each copy holds before you answer. To look now:": {
"fr": "chaque copie porte avant que vous répondiez. Pour regarder maintenant :",
"en": "each copy holds before you answer. To look now:",
},
"reversible with --restore": {
"fr": "réversible avec --restore",
"en": "reversible with --restore",
},
"Or by hand. To neutralize a copy, rename its key": {
"fr": "Ou à la main. Pour neutraliser une copie, renommer sa clé",
"en": "Or by hand. To neutralize a copy, rename its key",
},
"an unmatched key is never paired with the": {
"fr": "une clé sans correspondance n'est jamais appariée à la",
"en": "an unmatched key is never paired with the",
},
"module view, so the copy never receives the new": {
"fr": "vue module, la copie ne reçoit donc jamais le nouveau",
"en": "module view, so the copy never receives the new",
},
"Setting active=false alone is NOT enough:": {
"fr": "Mettre active=false seul ne suffit PAS :",
"en": "Setting active=false alone is NOT enough:",
},
"an inactive copy keeping the same key still shadows it.": {
"fr": "une copie inactive gardant la même clé la masque toujours.",
"en": "an inactive copy keeping the same key still shadows it.",
},
"COW view(s) belong to a module absent from": {
"fr": "vue(s) COW appartiennent à un module absent de",
"en": "COW view(s) belong to a module absent from",
},
"COW view(s) are pages or records made in the website": {
"fr": "vue(s) COW sont des pages ou des enregistrements faits dans",
"en": "COW view(s) are pages or records made in the website",
},
"editor (no module view of that name): not at risk.": {
"fr": "l'éditeur de site (aucune vue module de ce nom) : hors de danger.",
"en": "editor (no module view of that name): not at risk.",
},
"Use -v to list them.": {
"fr": "Utiliser -v pour les lister.",
"en": "Use -v to list them.",
},
"No view with this prefix on": {
"fr": "Aucune vue avec ce préfixe sur",
"en": "No view with this prefix on",
},
"archived COW view(s) on": {
"fr": "vue(s) COW archivées sur",
"en": "archived COW view(s) on",
},
"Their arch is intact. Restore them all with --restore,": {
"fr": "Leur arch est intacte. Toutes les restaurer avec --restore,",
"en": "Their arch is intact. Restore them all with --restore,",
},
"once the module view they shadow has the right shape.": {
"fr": "une fois que la vue module qu'elles masquent a la bonne forme.",
"en": "once the module view they shadow has the right shape.",
},
"COW view(s) restored on": {
"fr": "vue(s) COW restaurées sur",
"en": "COW view(s) restored on",
},
"No website COW view to neutralize.": {
"fr": "Aucune vue COW de site à neutraliser.",
"en": "No website COW view to neutralize.",
},
"website COW view(s) would break the bump": {
"fr": "vue(s) COW de site casseraient le palier",
"en": "website COW view(s) would break the bump",
},
"to": {
"fr": "vers",
"en": "to",
},
"Dry-run. Add --apply to rename their key to": {
"fr": "Simulation. Ajouter --apply pour renommer leur clé en",
"en": "Dry-run. Add --apply to rename their key to",
},
"and deactivate them. Reversible": {
"fr": "et les désactiver. Réversible",
"en": "and deactivate them. Reversible",
},
"with --restore; the arch stays in database.": {
"fr": "avec --restore ; l'arch reste en base.",
"en": "with --restore; the arch stays in database.",
},
"COW view(s) neutralized (key prefixed with": {
"fr": "vue(s) COW neutralisées (clé préfixée par",
"en": "COW view(s) neutralized (key prefixed with",
},
"deactivated). The arch is kept as an": {
"fr": "désactivées). L'arch est conservée comme",
"en": "deactivated). The arch is kept as an",
},
"archive; use --restore to undo.": {
"fr": "archive ; utiliser --restore pour annuler.",
"en": "archive; use --restore to undo.",
},
"target inherits, copy holds a standalone template": {
"fr": "la cible hérite, la copie porte un modèle autonome",
"en": "target inherits, copy holds a standalone template",
},
"target is a root view, copy holds inheritance specs": {
"fr": "la cible est une vue racine, la copie porte des specs d'héritage",
"en": "target is a root view, copy holds inheritance specs",
},
"No module view carries this key, so there is nothing to": {
"fr": "Aucune vue module ne porte cette clé, il n'y a donc rien à",
"en": "No module view carries this key, so there is nothing to",
},
"compare: this copy is a page made in the website editor.": {
"fr": "comparer : cette copie est une page faite dans l'éditeur de site.",
"en": "compare: this copy is a page made in the website editor.",
},
"The copy is identical to the module view.": {
"fr": "La copie est identique à la vue module.",
"en": "The copy is identical to the module view.",
},
"line(s) added": {
"fr": "ligne(s) ajoutée(s)",
"en": "line(s) added",
},
"removed": {
"fr": "retirée(s)",
"en": "removed",
},
"this is what neutralizing gives up.": {
"fr": "c'est ce que la neutralisation abandonne.",
"en": "this is what neutralizing gives up.",
},
"the module no longer declares this template": {
"fr": "le module ne déclare plus ce modèle",
"en": "the module no longer declares this template",
},
"inheritance specs (needs inherit_id)": {
"fr": "des specs d'héritage (exige inherit_id)",
"en": "inheritance specs (needs inherit_id)",
},
"a standalone template": {
"fr": "un modèle autonome",
"en": "a standalone template",
},
"Odoo changing the shape of its own template is NOT the": {
"fr": "Qu'Odoo change la forme de son propre modèle n'est PAS le",
"en": "Odoo changing the shape of its own template is NOT the",
},
"problem: on a database without a copy, the module upgrade": {
"fr": "problème : sur une base sans copie, la mise à jour du module",
"en": "problem: on a database without a copy, the module upgrade",
},
"rewrites the view and nothing breaks. It breaks here because": {
"fr": "réécrit la vue et rien ne casse. Ça casse ici parce qu'une",
"en": "rewrites the view and nothing breaks. It breaks here because",
},
"a COPY exists and froze the old shape — the copy follows the": {
"fr": "COPIE existe et a figé l'ancienne forme — la copie suit le",
"en": "a COPY exists and froze the old shape — the copy follows the",
},
"module and becomes an extension, while still holding a": {
"fr": "module et devient une extension, tout en portant encore un",
"en": "module and becomes an extension, while still holding a",
},
"standalone template. Odoo then applies that template as an": {
"fr": "modèle autonome. Odoo applique alors ce modèle comme une",
"en": "standalone template. Odoo then applies that template as an",
},
"inheritance spec and stops on « cannot be located in parent": {
"fr": "spec d'héritage et s'arrête sur « cannot be located in parent",
"en": "inheritance spec and stops on « cannot be located in parent",
},
"view ».": {
"fr": "view ».",
"en": "view ».",
},
"This copy has no module view of that name: it is a page": {
"fr": "Cette copie n'a aucune vue module de ce nom : c'est une page",
"en": "This copy has no module view of that name: it is a page",
},
"made in the website editor, and nothing holds its content.": {
"fr": "faite dans l'éditeur de site, et rien d'autre ne porte son contenu.",
"en": "made in the website editor, and nothing holds its content.",
},
"This copy is IDENTICAL to the module view it shadows: it": {
"fr": "Cette copie est IDENTIQUE à la vue module qu'elle masque : elle",
"en": "This copy is IDENTICAL to the module view it shadows: it",
},
"holds no customization, so neutralizing it loses nothing.": {
"fr": "ne porte aucune personnalisation, la neutraliser ne perd rien.",
"en": "holds no customization, so neutralizing it loses nothing.",
},
"This copy differs from the module view by": {
"fr": "Cette copie diffère de la vue module de",
"en": "This copy differs from the module view by",
},
"line(s):": {
"fr": "ligne(s) :",
"en": "line(s):",
},
"that is the customization, and all neutralizing gives up.": {
"fr": "c'est la personnalisation, et tout ce que la neutralisation abandonne.",
"en": "that is the customization, and all neutralizing gives up.",
},
"Run without --shape to read it.": {
"fr": "Lancer sans --shape pour la lire.",
"en": "Run without --shape to read it.",
},
"No website COW view is at risk.": {
"fr": "Aucune vue COW de site n'est en danger.",
"en": "No website COW view is at risk.",
},
"No COW copy has drifted from its module view.": {
"fr": "Aucune copie COW n'a dérivé de sa vue module.",
"en": "No COW copy has drifted from its module view.",
},
"COW copy(ies) drifted from their module": {
"fr": "copie(s) COW ont dérivé de leur vue",
"en": "COW copy(ies) drifted from their module",
},
"view in": {
"fr": "module dans",
"en": "view in",
},
"Odoo surfaces this when the module view is rewritten (a": {
"fr": "Odoo le fait apparaître quand la vue module est réécrite (un",
"en": "Odoo surfaces this when the module view is rewritten (a",
},
"version bump) or when the page is rendered.": {
"fr": "palier de version) ou quand la page est rendue.",
"en": "version bump) or when the page is rendered.",
},
"NO module view with this key": {
"fr": "AUCUNE vue module avec cette clé",
"en": "NO module view with this key",
},
"child": {
"fr": "enfant",
"en": "child",
},
"cannot apply": {
"fr": "ne peut s'appliquer",
"en": "cannot apply",
},
"Nothing changed. Re-run with --reset <key> --apply to": {
"fr": "Rien n'a changé. Relancer avec --reset <clé> --apply pour",
"en": "Nothing changed. Re-run with --reset <key> --apply to",
},
"reset a copy onto its module view.": {
"fr": "réinitialiser une copie sur sa vue module.",
"en": "reset a copy onto its module view.",
},
"no module view to reset onto, skipped.": {
"fr": "aucune vue module sur laquelle réinitialiser, ignorée.",
"en": "no module view to reset onto, skipped.",
},
"dry-run": {
"fr": "simulation",
"en": "dry-run",
},
"would reset": {
"fr": "réinitialiserait",
"en": "would reset",
},
"onto": {
"fr": "sur",
"en": "onto",
},
"reset": {
"fr": "réinitialisé",
"en": "reset",
},
"previous arch saved to": {
"fr": "arch précédente sauvegardée dans",
"en": "previous arch saved to",
},
"copy(ies) reset. Re-apply any real customisation": {
"fr": "copie(s) réinitialisée(s). Réappliquer toute vraie personnalisation",
"en": "copy(ies) reset. Re-apply any real customisation",
},
"as an INHERITING view, not a copy, so it cannot go stale": {
"fr": "comme une vue HÉRITANTE, pas une copie, pour qu'elle ne périme plus",
"en": "as an INHERITING view, not a copy, so it cannot go stale",
},
"again.": {
"fr": "à l'avenir.",
"en": "again.",
},
"lxml is required to resolve the xpath expressions.": {
"fr": "lxml est requis pour résoudre les expressions xpath.",
"en": "lxml is required to resolve the xpath expressions.",
},
"COW view(s) recorded in": {
"fr": "vue(s) COW enregistrées dans",
"en": "COW view(s) recorded in",
},
"COW view(s) disappeared": {
"fr": "vue(s) COW ont disparu",
"en": "COW view(s) disappeared",
},
"COW view(s) appeared": {
"fr": "vue(s) COW sont apparues",
"en": "COW view(s) appeared",
},
"arch rewritten": {
"fr": "arch réécrite",
"en": "arch rewritten",
},
"COW view(s) changed": {
"fr": "vue(s) COW ont changé",
"en": "COW view(s) changed",
},
"No change on the website COW views.": {
"fr": "Aucun changement sur les vues COW de site.",
"en": "No change on the website COW views.",
},
"Welcome to the Odoo database upgrade with ERPLibre": {
"fr": "Bienvenue dans la mise à niveau de base de données Odoo avec ERPLibre",
"en": "Welcome to the Odoo database upgrade with ERPLibre",
},
"Select the zip file of your database backup.": {
"fr": "Sélectionnez le fichier zip de votre sauvegarde de base de données.",
"en": "Select the zip file of your database backup.",
},
"Give the path of the file, or empty to use a file": {
"fr": "Donnez le chemin du fichier, ou rien pour utiliser un",
"en": "Give the path of the file, or empty to use a file",
},
"browser, or type": {
"fr": "navigateur de fichiers, ou tapez",
"en": "browser, or type",
},
"to download from production": {
"fr": "pour télécharger depuis la production",
"en": "to download from production",
},
"Cannot retrieve the database from remote, please retry the migration.": {
"fr": "Impossible de récupérer la base distante, veuillez relancer la migration.",
"en": "Cannot retrieve the database from remote, please retry the migration.",
},
"Open file": {
"fr": "Ouverture du fichier",
"en": "Open file",
},
"Detected Odoo CE version": {
"fr": "Version Odoo CE détectée",
"en": "Detected Odoo CE version",
},
"Which version do you want to upgrade to?": {
"fr": "Vers quelle version voulez-vous migrer ?",
"en": "Which version do you want to upgrade to?",
},
"Search the Odoo version": {
"fr": "Recherche de la version Odoo",
"en": "Search the Odoo version",
},
"Find the right environment, read the .zip file": {
"fr": "Recherche du bon environnement, lecture du fichier .zip",
"en": "Find the right environment, read the .zip file",
},
"Would you like to install": {
"fr": "Voulez-vous installer",
"en": "Would you like to install",
},
"You need an installed system before": {
"fr": "Il faut un système installé avant de",
"en": "You need an installed system before",
},
"continuing, check your Odoo installation.": {
"fr": "continuer, vérifiez votre installation Odoo.",
"en": "continuing, check your Odoo installation.",
},
"Install the environment if missing": {
"fr": "Installation de l'environnement s'il manque",
"en": "Install the environment if missing",
},
"Search missing modules": {
"fr": "Recherche des modules manquants",
"en": "Search missing modules",
},
"Install the missing modules, search for them or": {
"fr": "Installer les modules manquants, les chercher ou",
"en": "Install the missing modules, search for them or",
},
"ask to uninstall them (can break data)": {
"fr": "demander à les désinstaller (peut casser des données)",
"en": "ask to uninstall them (can break data)",
},
"Cannot set up the environment to begin.": {
"fr": "Impossible de préparer l'environnement pour commencer.",
"en": "Cannot set up the environment to begin.",
},
"Missing module": {
"fr": "Module manquant",
"en": "Missing module",
},
"Duplicate module": {
"fr": "Module en double",
"en": "Duplicate module",
},
"Missing or duplicate module detected at init,": {
"fr": "Module manquant ou en double détecté à l'init,",
"en": "Missing or duplicate module detected at init,",
},
"do you want to continue?": {
"fr": "voulez-vous continuer ?",
"en": "do you want to continue?",
},
"Missing or duplicate module detected at init, do": {
"fr": "Module manquant ou en double détecté à l'init,",
"en": "Missing or duplicate module detected at init, do",
},
"you want to continue?": {
"fr": "voulez-vous continuer ?",
"en": "you want to continue?",
},
"Which database name do you want to work with?": {
"fr": "Avec quel nom de base voulez-vous travailler ?",
"en": "Which database name do you want to work with?",
},
"Ignore the database neutralization": {
"fr": "Ignorer la neutralisation de la base",
"en": "Ignore the database neutralization",
},
"Working with database": {
"fr": "Travail sur la base",
"en": "Working with database",
},
"will copy": {
"fr": "va copier",
"en": "will copy",
},
"a file already exists, do you want to": {
"fr": "un fichier existe déjà, voulez-vous",
"en": "a file already exists, do you want to",
},
"continue?": {
"fr": "continuer ?",
"en": "continue?",
},
"Restore the database": {
"fr": "Restauration de la base",
"en": "Restore the database",
},
"Update all addons before neutralizing (already": {
"fr": "Mettre à jour tous les modules avant la neutralisation (déjà",
"en": "Update all addons before neutralizing (already",
},
"neutralized by Odoo if supported)": {
"fr": "neutralisée par Odoo si pris en charge)",
"en": "neutralized by Odoo if supported)",
},
"Do you need to upgrade before neutralizing the": {
"fr": "Faut-il mettre à jour avant de neutraliser la",
"en": "Do you need to upgrade before neutralizing the",
},
"database? Press enter to ignore": {
"fr": "base ? Entrée pour ignorer",
"en": "database? Press enter to ignore",
},
"Update the database before neutralizing, by module": {
"fr": "Mise à jour de la base avant neutralisation, par module",
"en": "Update the database before neutralizing, by module",
},
"Neutralize the database": {
"fr": "Neutralisation de la base",
"en": "Neutralize the database",
},
"Modules to uninstall before the migration": {
"fr": "Modules à désinstaller avant la migration",
"en": "Modules to uninstall before the migration",
},
"Uninstall modules": {
"fr": "Désinstallation des modules",
"en": "Uninstall modules",
},
"Install modules": {
"fr": "Installation des modules",
"en": "Install modules",
},
"Go to Settings / Technical / Cleanup... / Purge and": {
"fr": "Aller dans Configuration / Technique / Nettoyage... / Purger et",
"en": "Go to Settings / Technical / Cleanup... / Purge and",
},
"purge the obsolete modules": {
"fr": "purger les modules obsolètes",
"en": "purge the obsolete modules",
},
"Press enter to continue step 3": {
"fr": "Entrée pour continuer l'étape 3",
"en": "Press enter to continue step 3",
},
"Inspect zip": {
"fr": "Inspection du zip",
"en": "Inspect zip",
},
"Import database from zip": {
"fr": "Import de la base depuis le zip",
"en": "Import database from zip",
},
"Succeed update all addons": {
"fr": "Mise à jour de tous les modules",
"en": "Succeed update all addons",
},
"Clean up database before data migration": {
"fr": "Nettoyage de la base avant la migration des données",
"en": "Clean up database before data migration",
},
"Upgrade version with OpenUpgrade": {
"fr": "Montée de version avec OpenUpgrade",
"en": "Upgrade version with OpenUpgrade",
},
"Cleaning up database after upgrade": {
"fr": "Nettoyage de la base après la montée de version",
"en": "Cleaning up database after upgrade",
},
"Migration finished": {
"fr": "Migration terminée",
"en": "Migration finished",
},
"Clone to Odoo": {
"fr": "Clonage vers Odoo",
"en": "Clone to Odoo",
},
"FAILED (status": {
"fr": "ÉCHEC (statut",
"en": "FAILED (status",
},
"Stopping:": {
"fr": "Arrêt :",
"en": "Stopping:",
},
"is not usable.": {
"fr": "n'est pas utilisable.",
"en": "is not usable.",
},
"Clone Odoo": {
"fr": "Clonage Odoo",
"en": "Clone Odoo",
},
"nothing": {
"fr": "rien",
"en": "nothing",
},
"Switch Odoo": {
"fr": "Bascule Odoo",
"en": "Switch Odoo",
},
"done with update": {
"fr": "terminée avec mise à jour",
"en": "done with update",
},
"Module upgrade Odoo": {
"fr": "Mise à jour des modules Odoo",
"en": "Module upgrade Odoo",
},
"Database upgrade Odoo": {
"fr": "Migration de la base Odoo",
"en": "Database upgrade Odoo",
},
"Modules to uninstall before Odoo": {
"fr": "Modules à désinstaller avant Odoo",
"en": "Modules to uninstall before Odoo",
},
"to redo the command": {
"fr": "pour refaire la commande",
"en": "to redo the command",
},
"Error detected, press enter to continue or": {
"fr": "Erreur détectée, Entrée pour continuer ou",
"en": "Error detected, press enter to continue or",
},
"to stop": {
"fr": "pour arrêter",
"en": "to stop",
},
"Clone done for Odoo": {
"fr": "Clonage terminé pour Odoo",
"en": "Clone done for Odoo",
},
"Clone already done for Odoo": {
"fr": "Clonage déjà fait pour Odoo",
"en": "Clone already done for Odoo",
},
"Switch already done for Odoo": {
"fr": "Bascule déjà faite pour Odoo",
"en": "Switch already done for Odoo",
},
"Switch done with update for Odoo": {
"fr": "Bascule terminée avec mise à jour pour Odoo",
"en": "Switch done with update for Odoo",
},
"Module upgrade done for Odoo": {
"fr": "Mise à jour des modules terminée pour Odoo",
"en": "Module upgrade done for Odoo",
},
"Module upgrade already done for Odoo": {
"fr": "Mise à jour des modules déjà faite pour Odoo",
"en": "Module upgrade already done for Odoo",
},
"Database upgrade done for Odoo": {
"fr": "Migration de la base terminée pour Odoo",
"en": "Database upgrade done for Odoo",
},
"Database upgrade already done for Odoo": {
"fr": "Migration de la base déjà faite pour Odoo",
"en": "Database upgrade already done for Odoo",
},
"Cloning to Odoo": {
"fr": "Clonage vers Odoo",
"en": "Cloning to Odoo",
},
"from": {
"fr": "depuis",
"en": "from",
},
"Duplicate module in Odoo": {
"fr": "Module en double dans Odoo",
"en": "Duplicate module in Odoo",
},
"Duplicate module error detected, handle it": {
"fr": "Erreur de module en double détectée, la traiter",
"en": "Duplicate module error detected, handle it",
},
"manually then press enter to continue.": {
"fr": "manuellement puis Entrée pour continuer.",
"en": "manually then press enter to continue.",
},
"Missing module error detected, missing in": {
"fr": "Erreur de module manquant détectée, manquant dans",
"en": "Missing module error detected, missing in",
},
"All of the list above": {
"fr": "Toute la liste ci-dessus",
"en": "All of the list above",
},
"Add an extra custom one": {
"fr": "Ajouter un module personnalisé",
"en": "Add an extra custom one",
},
"List the missing modules to delete,": {
"fr": "Énumérer les modules manquants à supprimer,",
"en": "List the missing modules to delete,",
},
"separated by commas. The others will be": {
"fr": "séparés par des virgules. Les autres seront",
"en": "separated by commas. The others will be",
},
"migrated": {
"fr": "migrés",
"en": "migrated",
},
"Migration fix for Odoo": {
"fr": "Correctif de migration pour Odoo",
"en": "Migration fix for Odoo",
},
"Migration fix done for Odoo": {
"fr": "Correctif de migration terminé pour Odoo",
"en": "Migration fix done for Odoo",
},
"No migration fix to run for Odoo": {
"fr": "Aucun correctif de migration à exécuter pour Odoo",
"en": "No migration fix to run for Odoo",
},
"Migration fix already done for Odoo": {
"fr": "Correctif de migration déjà fait pour Odoo",
"en": "Migration fix already done for Odoo",
},
"List the module names to delete,": {
"fr": "Énumérer les noms de modules à supprimer,",
"en": "List the module names to delete,",
},
"separated by commas": {
"fr": "séparés par des virgules",
"en": "separated by commas",
},
"Missing module error": {
"fr": "Erreur de module manquant",
"en": "Missing module error",
},
"Duplicate module error": {
"fr": "Erreur de module en double",
"en": "Duplicate module error",
},
"Module error": {
"fr": "Erreur de module",
"en": "Module error",
},
"Please validate the commits after the code": {
"fr": "Veuillez valider les commits après la migration",
"en": "Please validate the commits after the code",
},
"migration.": {
"fr": "du code.",
"en": "migration.",
},
"To show the repo status": {
"fr": "Pour afficher l'état des dépôts",
"en": "To show the repo status",
},
"Please validate this path in config.conf": {
"fr": "Veuillez valider ce chemin dans config.conf",
"en": "Please validate this path in config.conf",
},
"Database migration to Odoo": {
"fr": "Migration de la base vers Odoo",
"en": "Database migration to Odoo",
},
"is now half migrated: replaying the command on": {
"fr": "est maintenant à moitié migrée : rejouer la commande dessus",
"en": "is now half migrated: replaying the command on",
},
"it would never recover, so it is NOT offered.": {
"fr": "ne s'en remettrait jamais, ce n'est donc PAS proposé.",
"en": "it would never recover, so it is NOT offered.",
},
"The clone step has been reset. Fix the": {
"fr": "L'étape de clonage a été réinitialisée. Corrigez la",
"en": "The clone step has been reset. Fix the",
},
"cause, then relaunch the migration and answer": {
"fr": "cause, puis relancez la migration et répondez",
"en": "cause, then relaunch the migration and answer",
},
"continue": {
"fr": "continuer",
"en": "continue",
},
"will be dropped and rebuilt from the previous": {
"fr": "sera supprimée et reconstruite depuis la version",
"en": "will be dropped and rebuilt from the previous",
},
"version before retrying.": {
"fr": "précédente avant de réessayer.",
"en": "version before retrying.",
},
"Do you want to upgrade all": {
"fr": "Voulez-vous tout mettre à jour",
"en": "Do you want to upgrade all",
},
"Press y/Y to upgrade all addons of the": {
"fr": "y/Y pour mettre à jour tous les modules de la",
"en": "Press y/Y to upgrade all addons of the",
},
"Open the server with Selenium": {
"fr": "Ouvrir le serveur avec Selenium",
"en": "Open the server with Selenium",
},
"Do you want to test this upgrade? Choose": {
"fr": "Voulez-vous tester cette migration ? Choisissez",
"en": "Do you want to test this upgrade? Choose",
},
"or press enter to ignore it": {
"fr": "ou Entrée pour ignorer",
"en": "or press enter to ignore it",
},
"Press enter to continue": {
"fr": "Entrée pour continuer",
"en": "Press enter to continue",
},
"Re-update i18n, purge the data and the tables": {
"fr": "Remise à jour de l'i18n, purge des données et des tables",
"en": "Re-update i18n, purge the data and the tables",
},
"except mail_test and mail_test_full": {
"fr": "sauf mail_test et mail_test_full",
"en": "except mail_test and mail_test_full",
},
"A backup can be created": {
"fr": "Une sauvegarde peut être créée",
"en": "A backup can be created",
},
"Press y/Y or type filename.zip to export, or": {
"fr": "y/Y ou un nom fichier.zip pour exporter, ou",
"en": "Press y/Y or type filename.zip to export, or",
},
"enter to continue": {
"fr": "Entrée pour continuer",
"en": "enter to continue",
},
"Test the migration, press y/Y": {
"fr": "Tester la migration, y/Y",
"en": "Test the migration, press y/Y",
},
"Documentation for this version": {
"fr": "Documentation de cette version",
"en": "Documentation for this version",
},
"Command not found": {
"fr": "Commande non trouvée",
"en": "Command not found",
},
"Execute command": {
"fr": "Exécution de la commande",
"en": "Execute command",
},
"Textual is missing from this interpreter:": {
"fr": "Textual manque à cet interpréteur :",
"en": "Textual is missing from this interpreter:",
},
"Run it with": {
"fr": "Le lancer avec",
"en": "Run it with",
},
[ADD] addons: uninstall a theme the way Odoo removes one There was no counterpart to install_addons_theme.sh, so themes were removed with a plain --uninstall. That takes out the module, not the theme: it skips _theme_remove(), whose first act is _reset_default_config() — the call that writes font-number and its three neighbours into user_values.scss. Measured on a real 12 -> 13 migration: web.assets_frontend stopped on « Undefined variable: $o-theme-font-number ». Odoo 12 defined it in option_font_body_*, dropped in 13.0; only the theme still redefined it, so removing the theme exposed a frozen 2020 customization. theme_leftover.py then reports what unloading does not take — 15 attachments on the measured database. It deletes nothing: their content may be the only trace of a customization. --- FR --- [ADD] addons : désinstaller un thème comme Odoo le retire install_addons_theme.sh n'avait pas de symétrique : on retirait donc les thèmes par un --uninstall nu. Cela enlève le module, pas le thème — cela saute _theme_remove(), dont le premier geste est _reset_default_config(), l'appel qui écrit font-number et ses trois voisines dans user_values.scss. Mesuré sur une vraie migration 12 → 13 : web.assets_frontend s'arrête sur « Undefined variable: $o-theme-font-number ». Odoo 12 la définissait dans option_font_body_*, supprimés en 13.0 ; seul le thème la redéfinissait, et le retirer a mis à nu un SCSS figé en 2020. theme_leftover.py signale ensuite ce que le déchargement ne prend pas — 15 pièces jointes sur la base mesurée. Il ne supprime rien : leur contenu peut être la seule trace d'une personnalisation. Assisted-by: Claude Opus 5 (cherry picked from commit 16a7b1e1333752e2ef603d66f47374a22590d665)
2026-08-13 05:56:06 -04:00
"No leftover for theme": {
"fr": "Aucun reste pour le thème",
"en": "No leftover for theme",
},
"attachment(s) still under": {
"fr": "pièce(s) jointe(s) encore sous",
"en": "attachment(s) still under",
},
"view(s) whose key still names it": {
"fr": "vue(s) dont la clé le nomme encore",
"en": "view(s) whose key still names it",
},
"Nothing was deleted: their content may be the only trace": {
"fr": "Rien n'a été supprimé : leur contenu peut être la seule trace",
"en": "Nothing was deleted: their content may be the only trace",
},
"of a customization. Read before removing.": {
"fr": "d'une personnalisation. Lire avant de retirer.",
"en": "of a customization. Read before removing.",
},
"Installed theme(s) on": {
"fr": "Thème(s) installé(s) sur",
"en": "Installed theme(s) on",
},
"A theme carries view copies and SCSS through every": {
"fr": "Un thème traîne des copies de vues et des SCSS à travers chaque",
"en": "A theme carries view copies and SCSS through every",
},
"version bump, and a bump can rename what they rely on.": {
"fr": "palier de version, et un palier peut renommer ce dont ils dépendent.",
"en": "version bump, and a bump can rename what they rely on.",
},
"Uninstall them properly before migrating?": {
"fr": "Les désinstaller proprement avant de migrer ?",
"en": "Uninstall them properly before migrating?",
},
"Kept. Nothing was uninstalled.": {
"fr": "Conservés. Rien n'a été désinstallé.",
"en": "Kept. Nothing was uninstalled.",
},
[ADD] migration: predict which customized SCSS the next bump breaks Customized SCSS lives in ir_attachment, frozen the day it is written, still using the variables of that version. A bump can rename them: website declared $o-theme-font-number in 12.0 and replaced the whole mechanism in 13.0, so a 2020 customization stopped the frontend bundle. Same shape as a stale COW view. Reading the stored SCSS against the target sources answers before the bump and names the attachment. Booting Odoo and opening a page answers after, with « Style error » and no name — on a page a broken frontend bundle is exactly what keeps you from reaching. Measured: on the pre-bump database, targeting 13.0, it predicts the failure; against its own 12.0 sources it reports nothing. That noise floor is what makes it usable — the first version cried wolf on six mixin parameters and named @include arguments. --- FR --- [ADD] migration : prédire quel SCSS personnalisé le palier va casser Un SCSS personnalisé vit dans ir_attachment, figé le jour où il est écrit, employant encore les variables de cette version-là. Un palier peut les renommer : website déclarait $o-theme-font-number en 12.0 et a remplacé le mécanisme en 13.0, arrêtant le bundle sur une personnalisation de 2020. Même forme qu'une vue COW périmée. Lire le SCSS stocké contre les sources cibles répond AVANT le palier et nomme la pièce jointe. Démarrer Odoo et ouvrir une page répond après, par « Style error » sans nom — et un bundle cassé est justement ce qui empêche d'atteindre la page. Mesuré : sur la base d'avant le palier, cible 13.0, il prédit la panne ; contre ses propres sources 12.0 il ne signale rien. Ce bruit de fond nul est ce qui le rend utilisable — la première version criait à tort sur six paramètres de mixin et arguments nommés. Assisted-by: Claude Opus 5 (cherry picked from commit c6611ee1431cfee94e321536d20052b8cca3ec10)
2026-08-15 03:21:25 -04:00
"No customized SCSS is at risk in": {
"fr": "Aucun SCSS personnalisé n'est en danger dans",
"en": "No customized SCSS is at risk in",
},
"customized SCSS use(s) a variable that": {
"fr": "SCSS personnalisé(s) utilisent une variable que",
"en": "customized SCSS use(s) a variable that",
},
"no longer defines: the bundle will not compile.": {
"fr": "ne définit plus : le bundle ne compilera pas.",
"en": "no longer defines: the bundle will not compile.",
},
"Each one is a copy frozen on an older version. Dropping it": {
"fr": "Chacun est une copie figée sur une version antérieure. L'abandonner",
"en": "Each one is a copy frozen on an older version. Dropping it",
},
"restores the module file:": {
"fr": "rend le fichier du module :",
"en": "restores the module file:",
},
"Read it first: what it holds beyond the stale variable is": {
"fr": "Le lire d'abord : ce qu'il porte au-delà de la variable périmée est",
"en": "Read it first: what it holds beyond the stale variable is",
},
"a real customization, to re-apply as a small file.": {
"fr": "une vraie personnalisation, à réappliquer en petit fichier.",
"en": "a real customization, to re-apply as a small file.",
},
"Read the customizations above before continuing.": {
"fr": "Lire les personnalisations ci-dessus avant de continuer.",
"en": "Read the customizations above before continuing.",
},
[ADD] migration: read the stale SCSS, then fix it, without leaving the tool The report named the attachment and printed a command to paste. Deciding « reset it » still meant accepting to lose one did not know what: the diff against the module file is the only thing resetting gives up. The tool now shows it and asks. --diff prints it, --tui browses it, --apply resets without asking; with no flag and a terminal it asks, and looking does not answer — the prompt comes back after each read. --apply writes the copies under private/ first: reset_asset deletes the attachment, so without that the customized lines would be nowhere. The migration runs it on the real terminal, or none of this would be reachable from there — the same pipe that was closing the TUI. --- FR --- [ADD] migration : lire le SCSS périmé, puis le corriger, sans sortir Le rapport nommait la pièce jointe et imprimait une commande à coller. Répondre « réinitialise » revenait encore à accepter de perdre on ne sait quoi : l'écart avec le fichier du module est la seule chose que la réinitialisation abandonne. L'outil le montre et pose la question. --diff l'imprime, --tui le parcourt, --apply réinitialise sans demander ; sans drapeau et devant un terminal il demande, et regarder ne répond pas — l'invite revient après chaque lecture. --apply écrit d'abord les copies sous private/ : reset_asset supprime la pièce jointe, sans quoi les lignes personnalisées ne seraient plus nulle part. La migration le lance sur le vrai terminal, sinon rien de tout cela n'y serait atteignable — le tube même qui fermait la TUI. Assisted-by: Claude Opus 5 (cherry picked from commit 9e6c88622a74f7be408a8b1c523838eeadda1da3)
2026-08-15 03:41:56 -04:00
"Missing:": {
"fr": "Manquantes :",
"en": "Missing:",
},
"No module file of that name in": {
"fr": "Aucun fichier de module de ce nom dans",
"en": "No module file of that name in",
},
"the target no longer ships it, so there is nothing to": {
"fr": "la cible ne le livre plus, il n'y a donc rien sur quoi",
"en": "the target no longer ships it, so there is nothing to",
},
"fall back on. Read the copy before dropping it.": {
"fr": "retomber. Lire la copie avant de l'abandonner.",
"en": "fall back on. Read the copy before dropping it.",
},
"The copy is identical to the module file.": {
"fr": "La copie est identique au fichier du module.",
"en": "The copy is identical to the module file.",
},
"line(s): that is what resetting gives up.": {
"fr": "ligne(s) : c'est ce que la réinitialisation abandonne.",
"en": "line(s): that is what resetting gives up.",
},
"What do you want to do with these customizations?": {
"fr": "Que voulez-vous faire de ces personnalisations ?",
"en": "What do you want to do with these customizations?",
},
"Enter = nothing": {
"fr": "Entrée = rien",
"en": "Enter = nothing",
},
"what the copy changed": {
"fr": "ce que la copie a changé",
"en": "what the copy changed",
},
"reset them onto the module file": {
"fr": "les réinitialiser sur le fichier du module",
"en": "reset them onto the module file",
},
"Full screen view unavailable.": {
"fr": "Vue plein écran indisponible.",
"en": "Full screen view unavailable.",
},
"Saved before resetting": {
"fr": "Sauvegardé avant réinitialisation",
"en": "Saved before resetting",
},
"Reset failed, nothing was changed.": {
"fr": "La réinitialisation a échoué, rien n'a été modifié.",
"en": "Reset failed, nothing was changed.",
},
"Reset done.": {
"fr": "Réinitialisation faite.",
"en": "Reset done.",
},
"This copy uses variables that": {
"fr": "Cette copie utilise des variables que",
"en": "This copy uses variables that",
},
"no longer defines": {
"fr": "ne définit plus",
"en": "no longer defines",
},
"The copy was written against an older version and frozen": {
"fr": "La copie a été écrite contre une version antérieure et figée",
"en": "The copy was written against an older version and frozen",
},
"there. The module has since renamed what it relies on.": {
"fr": "là. Le module a depuis renommé ce dont elle dépend.",
"en": "there. The module has since renamed what it relies on.",
},
"Resetting restores": {
"fr": "La réinitialisation rend",
"en": "Resetting restores",
},
"(nothing: the target no longer ships this file)": {
"fr": "(rien : la cible ne livre plus ce fichier)",
"en": "(nothing: the target no longer ships this file)",
},
"Reset command copied.": {
"fr": "Commande de réinitialisation copiée.",
"en": "Reset command copied.",
},
2026-08-15 16:34:26 -04:00
"Resetting needs Odoo 13.0 or later; this checkout is on": {
"fr": "La réinitialisation exige Odoo 13.0 ou plus ; ce checkout est sur",
"en": "Resetting needs Odoo 13.0 or later; this checkout is on",
},
"The prediction stands, the fix does not: run it again once": {
"fr": "La prédiction tient, pas la correction : relancer une fois",
"en": "The prediction stands, the fix does not: run it again once",
},
"the bump is done, on the upgraded database.": {
"fr": "le palier passé, sur la base montée de version.",
"en": "the bump is done, on the upgraded database.",
},
"Applying it from here fails with": {
"fr": "L'appliquer d'ici échoue sur",
"en": "Applying it from here fails with",
},
"and changes nothing.": {
"fr": "et ne change rien.",
"en": "and changes nothing.",
},
"Reset one of them onto its module view": {
"fr": "Réinitialiser l'une d'elles sur sa vue module",
"en": "Reset one of them onto its module view",
},
"Drifted COW copies": {
"fr": "Copies COW ayant dérivé",
"en": "Drifted COW copies",
},
"Which one(s) to reset onto the module view?": {
"fr": "Laquelle ou lesquelles réinitialiser sur la vue module ?",
"en": "Which one(s) to reset onto the module view?",
},
"numbers separated by commas, a = all, empty =": {
"fr": "numéros séparés par des virgules, a = toutes, vide =",
"en": "numbers separated by commas, a = all, empty =",
},
"Kept. Nothing was reset.": {
"fr": "Conservées. Rien n'a été réinitialisé.",
"en": "Kept. Nothing was reset.",
},
"Unknown choice, nothing was reset.": {
"fr": "Choix inconnu, rien n'a été réinitialisé.",
"en": "Unknown choice, nothing was reset.",
},
"Delete these leftovers, or keep them?": {
"fr": "Effacer ces restes, ou les garder ?",
"en": "Delete these leftovers, or keep them?",
},
"Enter = keep": {
"fr": "Entrée = garder",
"en": "Enter = keep",
},
"delete, after saving them": {
"fr": "effacer, après les avoir sauvegardés",
"en": "delete, after saving them",
},
"Kept. Nothing was deleted.": {
"fr": "Conservés. Rien n'a été effacé.",
"en": "Kept. Nothing was deleted.",
},
"Saved before deleting": {
"fr": "Sauvegardé avant effacement",
"en": "Saved before deleting",
},
"Deletion failed, nothing was removed.": {
"fr": "L'effacement a échoué, rien n'a été retiré.",
"en": "Deletion failed, nothing was removed.",
},
"attachment(s) deleted.": {
"fr": "pièce(s) jointe(s) effacée(s).",
"en": "attachment(s) deleted.",
},
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
"Request every public URL of this database now?": {
"fr": "Interroger maintenant toutes les URL publiques de cette base ?",
"en": "Request every public URL of this database now?",
},
"The sitemap listed no URL: nothing was tested.": {
"fr": "Le sitemap n'a listé aucune URL : rien n'a été testé.",
"en": "The sitemap listed no URL: nothing was tested.",
},
"public URL(s) answered without error.": {
"fr": "URL publique(s) ont répondu sans erreur.",
"en": "public URL(s) answered without error.",
},
"public URL(s) failed": {
"fr": "URL publique(s) ont échoué",
"en": "public URL(s) failed",
},
"no answer": {
"fr": "aucune réponse",
"en": "no answer",
},
"A page listed for search engines that does not answer is": {
"fr": "Une page listée pour les moteurs de recherche qui ne répond pas est",
"en": "A page listed for search engines that does not answer is",
},
"a page your visitors do not reach either.": {
"fr": "une page que vos visiteurs n'atteignent pas non plus.",
"en": "a page your visitors do not reach either.",
},
"The server never answered on": {
"fr": "Le serveur n'a jamais répondu sur",
"en": "The server never answered on",
},
"Could not read": {
"fr": "Impossible de lire",
"en": "Could not read",
},
"Starting Odoo on": {
"fr": "Démarrage d'Odoo sur",
"en": "Starting Odoo on",
},
"port": {
"fr": "port",
"en": "port",
},
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
"parent view(s) in cause": {
"fr": "vue(s) parente(s) en cause",
"en": "parent view(s) in cause",
},
"Those parents are copies frozen on an older version;": {
"fr": "Ces parents sont des copies figées sur une version antérieure ;",
"en": "Those parents are copies frozen on an older version;",
},
"resetting them onto the module view is the fix": {
"fr": "les réinitialiser sur la vue module est le correctif",
"en": "resetting them onto the module view is the fix",
},
"No parent view named: nothing to offer.": {
"fr": "Aucune vue parente nommée : rien à proposer.",
"en": "No parent view named: nothing to offer.",
},
"Copies to reset onto their module view": {
"fr": "Copies à réinitialiser sur leur vue module",
"en": "Copies to reset onto their module view",
},
"Which one(s) to reset?": {
"fr": "Laquelle ou lesquelles réinitialiser ?",
"en": "Which one(s) to reset?",
},
"Re-checked the": {
"fr": "Revérification des",
"en": "Re-checked the",
},
"failing URL(s) after the reset": {
"fr": "URL en échec après la réinitialisation",
"en": "failing URL(s) after the reset",
},
"Something already listens on port": {
"fr": "Quelque chose écoute déjà sur le port",
"en": "Something already listens on port",
},
"it would be tested instead of this database.": {
"fr": "il serait testé à la place de cette base.",
"en": "it would be tested instead of this database.",
},
"Enter =": {
"fr": "Entrée =",
"en": "Enter =",
},
[FIX] migration: reset the copy that is actually stale, and say when a key misses Answering « all » reset one copy of two and reported success. Two defects behind it, both mine. A requested key matching no finding did nothing, silently: the tool returned early on « nothing drifted » and never looked. Detection is differential — it only sees a copy whose CHILD breaks — so a stale copy without children escapes it. A key can now be reset outside detection, an unknown one is named, and the exit code is 2. And the culprit is not always the parent. On /contactus the parent was identical to its module view; the CHILD held the stale arch. Both are proposed now, parent first — it is the more common case. Measured on the migration: 33 of 33 public URLs answer. --- FR --- [FIX] migration : réinitialiser la copie vraiment périmée, et signaler une clé sans objet Répondre « toutes » réinitialisait une copie sur deux et annonçait un succès. Deux défauts derrière, tous deux à moi. Une clé demandée ne correspondant à aucun constat ne faisait rien, en silence : l'outil sortait sur « rien n'a dérivé » sans jamais chercher. La détection est différentielle — elle ne voit qu'une copie dont un ENFANT casse — donc une copie périmée sans enfant lui échappe. Une clé peut désormais être réinitialisée hors détection, une clé inconnue est nommée, et le code de sortie vaut 2. Et le coupable n'est pas toujours le parent. Sur /contactus, le parent était identique à sa vue module ; c'est l'ENFANT qui portait l'arch périmée. Les deux sont proposés, le parent d'abord — le cas le plus fréquent. Mesuré sur la migration : 33 URL publiques sur 33 répondent. Assisted-by: Claude Opus 5
2026-08-17 04:49:32 -04:00
"No COW copy carries this key": {
"fr": "Aucune copie COW ne porte cette clé",
"en": "No COW copy carries this key",
},
"already identical to the module view.": {
"fr": "déjà identique à la vue module.",
"en": "already identical to the module view.",
},
"Odoo surfaces this when the module view is rewritten": {
"fr": "Odoo le fait apparaître quand la vue module est réécrite",
"en": "Odoo surfaces this when the module view is rewritten",
},
"(a version bump) or when the page is rendered.": {
"fr": "(un palier de version) ou quand la page est rendue.",
"en": "(a version bump) or when the page is rendered.",
},
"Before starting: what already answers, and what": {
"fr": "Avant de commencer : ce qui répond déjà, et ce qui",
"en": "Before starting: what already answers, and what",
},
"does not. A page broken now will still be broken": {
"fr": "ne répond pas. Une page cassée maintenant le sera encore",
"en": "does not. A page broken now will still be broken",
},
"after — and that is not the migration.": {
"fr": "après — et ce ne sera pas la migration.",
"en": "after — and that is not the migration.",
},
"No child fails on this copy: it drifted without": {
"fr": "Aucun enfant n'échoue sur cette copie : elle a dérivé sans",
"en": "No child fails on this copy: it drifted without",
},
"breaking anything yet.": {
"fr": "encore rien casser.",
"en": "breaking anything yet.",
},
"These children no longer find their anchor in the copy": {
"fr": "Ces enfants ne trouvent plus leur point d'ancrage dans la copie",
"en": "These children no longer find their anchor in the copy",
},
"The anchor exists in the module view": {
"fr": "L'ancrage existe dans la vue module",
"en": "The anchor exists in the module view",
},
"but not in this copy, frozen on an older version.": {
"fr": "mais pas dans cette copie, figée sur une version antérieure.",
"en": "but not in this copy, frozen on an older version.",
},
"Resetting restores the module arch; the customization the": {
"fr": "Réinitialiser rend l'arch du module ; la personnalisation que",
"en": "Resetting restores the module arch; the customization the",
},
"copy carried is saved first, and is yours to re-apply as an": {
"fr": "la copie portait est sauvegardée d'abord, à vous de la réappliquer en",
"en": "copy carried is saved first, and is yours to re-apply as an",
},
"INHERITING view with its own key.": {
"fr": "vue HÉRITANTE avec sa propre clé.",
"en": "INHERITING view with its own key.",
},
"No module view carries this key: nothing to reset": {
"fr": "Aucune vue module ne porte cette clé : rien sur quoi",
"en": "No module view carries this key: nothing to reset",
},
"onto.": {
"fr": "réinitialiser.",
"en": "onto.",
},
"Browse the differences full screen": {
"fr": "Parcourir les différences en plein écran",
"en": "Browse the differences full screen",
},
[ADD] migration: run the OCA database cleanup before testing the pages The eight purges are not independent: purging a model frees the columns that referenced it, purging a table frees the data pointing at it. One pass is never enough, so the requested order runs again until a full pass repairs nothing new. Refusals are expected — a foreign key still holds, a module says no. Purging a list at once loses everything to the first one, so each entry purges inside its own savepoint: a refusal rolls back that entry alone. What one pass could not take, the next may, once its neighbours are gone. Leftovers are reported as a warning: a database can carry some that nothing removes, and stopping there would help no one. It refuses a version mismatch. Measured while building it: a shell on Odoo 14 opened against a 17.0 database went rewriting ir_model before dying on a jsonb it did not know. An older Odoo does not merely fail on a newer database — it writes on the way. --- FR --- [ADD] migration : lancer le nettoyage OCA avant de tester les pages Les huit purges ne sont pas indépendantes : purger un modèle libère les colonnes qui le référençaient, purger une table libère les données qui la visaient. Une passe ne suffit jamais, l'ordre demandé est donc rejoué jusqu'à ce qu'une passe entière ne répare plus rien. Les refus sont attendus — une clé étrangère tient, un module dit non. Purger d'un bloc perdrait tout au premier : chaque entrée passe dans son propre point de reprise, un refus n'emporte que la sienne. Ce qu'une passe n'a pu prendre, la suivante le peut, une fois les voisines parties. Les restes sont un avertissement : une base peut en porter que rien ne retire, et s'arrêter là n'aiderait personne. Il refuse une version qui ne correspond pas. Mesuré en le construisant : un shell en Odoo 14 ouvert sur une base 17.0 est parti réécrire ir_model avant de mourir sur un jsonb inconnu de lui. Un Odoo plus ancien n'échoue pas simplement sur une base plus récente — il écrit en chemin. Assisted-by: Claude Opus 5
2026-08-17 09:05:03 -04:00
"The checkout is on Odoo": {
"fr": "Le checkout est sur Odoo",
"en": "The checkout is on Odoo",
},
"and the database on": {
"fr": "et la base sur",
"en": "and the database on",
},
"Opening it with the wrong version writes to it before": {
"fr": "L'ouvrir avec la mauvaise version y écrit avant",
"en": "Opening it with the wrong version writes to it before",
},
"failing. Switch the checkout first.": {
"fr": "d'échouer. Basculer le checkout d'abord.",
"en": "failing. Switch the checkout first.",
},
"Database cleanup on": {
"fr": "Nettoyage de la base",
"en": "Database cleanup on",
},
"Cleaning": {
"fr": "Nettoyage de",
"en": "Cleaning",
},
"pass": {
"fr": "passe",
"en": "pass",
},
"purged": {
"fr": "purgés",
"en": "purged",
},
"purged;": {
"fr": "purgés ;",
"en": "purged;",
},
"could not be, and are left as they are": {
"fr": "n'ont pas pu l'être, et restent en l'état",
"en": "could not be, and are left as they are",
},
"entries purged, nothing left.": {
"fr": "entrées purgées, rien ne reste.",
"en": "entries purged, nothing left.",
},
"no such wizard in this version, skipped.": {
"fr": "pas d'assistant pour cela dans cette version, ignoré.",
"en": "no such wizard in this version, skipped.",
},
"A database can carry leftovers nothing can remove.": {
"fr": "Une base peut porter des restes que rien ne peut retirer.",
"en": "A database can carry leftovers nothing can remove.",
},
"This is a warning, not a failure.": {
"fr": "C'est un avertissement, pas un échec.",
"en": "This is a warning, not a failure.",
},
"The cleanup produced no report.": {
"fr": "Le nettoyage n'a produit aucun rapport.",
"en": "The cleanup produced no report.",
},
"Unreadable report": {
"fr": "Rapport illisible",
"en": "Unreadable report",
},
"obsolete models": {
"fr": "modèles obsolètes",
"en": "obsolete models",
},
"obsolete modules": {
"fr": "modules obsolètes",
"en": "obsolete modules",
},
"obsolete columns": {
"fr": "colonnes obsolètes",
"en": "obsolete columns",
},
"obsolete tables": {
"fr": "tables obsolètes",
"en": "obsolete tables",
},
"obsolete data entries": {
"fr": "entrées de données obsolètes",
"en": "obsolete data entries",
},
"obsolete menu entries": {
"fr": "entrées de menu obsolètes",
"en": "obsolete menu entries",
},
"missing indexes": {
"fr": "index manquants",
"en": "missing indexes",
},
"obsolete properties": {
"fr": "propriétés obsolètes",
"en": "obsolete properties",
},
"entries would be purged": {
"fr": "entrées seraient purgées",
"en": "entries would be purged",
},
"nothing was changed": {
"fr": "rien n'a été modifié",
"en": "nothing was changed",
},
[ADD] migration: make Enter mean the answer you always give Auto-run promised to "take the default after five seconds", but every default was EMPTY: it took nothing. Worse, half the prompts of a migration are asked by separate processes — the theme uninstaller, the stale-SCSS detector, the smoke test. They knew nothing of auto-run and waited forever for a keystroke that never came. The countdown now lives in one file and travels through the ENVIRONMENT, the only channel a fork shares. Enter takes the default everywhere, not only under auto-run: a prompt that prints (Y/n) and does otherwise is worse than no prompt. Every text was rewritten to say what Enter does, and every default kept an explicit way out. Two defaults now write. Both are tenable only because the backup runs first, and a test locks that ORDER rather than trusting a promise. --- FR --- [ADD] migration : faire d'Entrée la réponse qu'on donne toujours L'auto-exécution promettait « le défaut après cinq secondes », mais tous les défauts étaient VIDES : elle ne prenait rien. Pire, la moitié des invites d'une migration sont posées par d'autres processus — le désinstalleur de thème, le détecteur de SCSS figé, le test de fumée. Ils ignoraient l'auto-exécution et attendaient sans fin une frappe. Le compte à rebours tient désormais dans un seul fichier et voyage par l'ENVIRONNEMENT, le seul canal qu'un fork partage. Entrée vaut le défaut partout, pas seulement en auto : une invite qui affiche (Y/n) et fait l'inverse est pire que pas d'invite. Chaque texte dit ce que fait Entrée, et chaque défaut garde une issue explicite. Deux défauts écrivent. Ils ne tiennent que parce que la sauvegarde passe d'abord, et un test verrouille cet ORDRE plutôt qu'une promesse. Assisted-by: Claude Opus 5
2026-08-17 20:47:17 -04:00
"Enter = 1": {
"fr": "Entrée = 1",
"en": "Enter = 1",
},
"Enter = all, n = none": {
"fr": "Entrée = tous, n = aucun",
"en": "Enter = all, n = none",
},
"Enter = delete, after saving them": {
"fr": "Entrée = effacer, après les avoir sauvegardés",
"en": "Enter = delete, after saving them",
},
"Enter = neutralize now, reversible": {
"fr": "Entrée = neutraliser maintenant, réversible",
"en": "Enter = neutralize now, reversible",
},
"Enter = reset them onto the module file": {
"fr": "Entrée = les réinitialiser sur le fichier du module",
"en": "Enter = reset them onto the module file",
},
"Enter upgrades all addons of the": {
"fr": "Entrée met à jour tous les modules de la",
"en": "Enter upgrades all addons of the",
},
"Open it at EVERY version bump, stop asking": {
"fr": "L'ouvrir à CHAQUE palier de version, ne plus demander",
"en": "Open it at EVERY version bump, stop asking",
},
"Testing every bump, as chosen earlier.": {
"fr": "Test à chaque palier, comme choisi plus tôt.",
"en": "Testing every bump, as chosen earlier.",
},
"numbers separated by commas, Enter = all, n =": {
"fr": "numéros séparés par des virgules, Entrée = toutes, n =",
"en": "numbers separated by commas, Enter = all, n =",
},
"decide at the version bump": {
"fr": "décider au palier de version",
"en": "decide at the version bump",
},
"keep": {
"fr": "garder",
"en": "keep",
},
[ADD] migration: open every app as the neutralization test user The public smoke test opens what a visitor reaches. It says nothing about the back office, which is where a migration does most of its damage: a field dropped from a model but still named in a form, a module installed in the database whose code no longer ships with the target version. None of it stops the module loading — it stops the day someone opens the app. Neutralizing installs a `test` / `test` login carrying the SYSTEM user's groups, and it survives the module's uninstall. So the tool checks for that user rather than trusting a flag, and it rides the server the public pass already started: booting Odoo is what costs minutes, not requests. Measured on a real 18.0 database of 25 apps, which found four defects of mine: web_search_read changed signature in 17, a bare 404 hides an unregistered model, embedded sub-views are not the parent's fields, and an empty psql result meant two different things. --- FR --- [ADD] migration : ouvrir chaque application avec l'utilisateur test Le test de fumée public ouvre ce qu'un visiteur atteint. Il ne dit rien du back-office, où une migration fait pourtant l'essentiel de ses dégâts : un champ retiré du modèle mais toujours nommé dans un formulaire, un module installé en base dont le code n'accompagne plus la version cible. Rien de cela n'arrête le chargement ; cela arrête le jour où l'on ouvre l'appli. La neutralisation pose un compte `test` / `test` portant les groupes du superutilisateur, et il survit à la désinstallation du module. L'outil vérifie donc cet utilisateur plutôt qu'un drapeau, et réutilise le serveur déjà démarré : c'est le démarrage qui coûte, pas les requêtes. Mesuré sur une vraie base 18.0 de 25 applications, ce qui a révélé quatre défauts de mon fait. Assisted-by: Claude Opus 5
2026-08-17 22:16:40 -04:00
"The server did not serve the login page.": {
"fr": "Le serveur n'a pas servi la page de connexion.",
"en": "The server did not serve the login page.",
},
"Wrong login or password for user": {
"fr": "Identifiant ou mot de passe refusé pour l'utilisateur",
"en": "Wrong login or password for user",
},
"Could not read the menus": {
"fr": "Impossible de lire les menus",
"en": "Could not read the menus",
},
"Apps opened as the test user": {
"fr": "Applications ouvertes avec l'utilisateur test",
"en": "Apps opened as the test user",
},
"not openable this way (client or server": {
"fr": "non ouvrables ainsi (action client ou",
"en": "not openable this way (client or server",
},
"action): nothing to render.": {
"fr": "serveur) : rien à rendre.",
"en": "action): nothing to render.",
},
"opened with an empty domain: theirs": {
"fr": "ouvertes avec un domaine vide : le leur",
"en": "opened with an empty domain: theirs",
},
"needs the browser to evaluate it.": {
"fr": "demande le navigateur pour être évalué.",
"en": "needs the browser to evaluate it.",
},
"Every app opened its first page.": {
"fr": "Chaque application a ouvert sa première page.",
"en": "Every app opened its first page.",
},
"Apps that failed to open": {
"fr": "Applications qui n'ont pas pu s'ouvrir",
"en": "Apps that failed to open",
},
"reading the action": {
"fr": "lecture de l'action",
"en": "reading the action",
},
"rendering the views": {
"fr": "rendu des vues",
"en": "rendering the views",
},
"loading the first records": {
"fr": "chargement des premiers enregistrements",
"en": "loading the first records",
},
"These are back-office failures: the public smoke test": {
"fr": "Ce sont des échecs du back-office : le test de fumée public",
"en": "These are back-office failures: the public smoke test",
},
"cannot see them.": {
"fr": "ne peut pas les voir.",
"en": "cannot see them.",
},
"Could not log in as": {
"fr": "Connexion impossible en tant que",
"en": "Could not log in as",
},
"No": {
"fr": "Aucun",
"en": "No",
},
"user in": {
"fr": "utilisateur dans",
"en": "user in",
},
"the database was not neutralized, nothing to browse.": {
"fr": "la base n'a pas été neutralisée, rien à parcourir.",
"en": "the database was not neutralized, nothing to browse.",
},
"Back office not browsed": {
"fr": "Back-office non parcouru",
"en": "Back office not browsed",
},
"no test user: the database was not neutralized": {
"fr": "pas d'utilisateur test : la base n'a pas été neutralisée",
"en": "no test user: the database was not neutralized",
},
"Could not tell whether the": {
"fr": "Impossible de savoir si l'utilisateur",
"en": "Could not tell whether the",
},
"user exists in": {
"fr": "existe dans",
"en": "user exists in",
},
"could not tell whether the test user exists": {
"fr": "impossible de savoir si l'utilisateur test existe",
"en": "could not tell whether the test user exists",
},
"listing the model fields": {
"fr": "inventaire des champs du modèle",
"en": "listing the model fields",
},
"model absent from the running registry": {
"fr": "modèle absent du registre en cours",
"en": "model absent from the running registry",
},
"installed in the database, but its module code": {
"fr": "installé dans la base, mais le code de son module",
"en": "installed in the database, but its module code",
},
"is not in this addons path.": {
"fr": "n'est pas dans cet addons path.",
"en": "is not in this addons path.",
},
"Views naming a field the model no longer has": {
"fr": "Vues nommant un champ que le modèle n'a plus",
"en": "Views naming a field the model no longer has",
},
"Public pages, then the back office as the test user": {
"fr": "Les pages publiques, puis le back-office avec l'utilisateur test",
"en": "Public pages, then the back office as the test user",
},
"if the database was neutralized.": {
"fr": "si la base a été neutralisée.",
"en": "if the database was neutralized.",
},
[ADD] migration: open /my as the test user, and blame the right request The portal is a third rendering, neither the public site nor the back office: QWeb frontend with counters that each query their own model. The sitemap does not list /my — the page needs a session — and no RPC call goes through it. A migration can break it with nothing else noticing. Measured on a real database: /my answered 500 on a user field a module no longer defines, while every public page and sixteen apps were fine. Two guards the run proved necessary. Requested without a session, /my redirects to the login form and answers 200: counting that as a success would announce a healthy portal never seen. And the production error page shows no traceback, so the reason comes from the server log — attributed by PATH, because the portal is checked first and taking the last trace blamed it for an app's failure. A wrong cause costs more than none. --- FR --- [ADD] migration : ouvrir /my avec l'utilisateur test, et accuser la bonne requête Le portail est un troisième rendu, ni le site public ni le back-office : du QWeb frontend, avec ses compteurs qui interrogent chacun leur modèle. Le sitemap ne liste pas /my — la page demande une session — et aucun appel RPC n'y passe. Une migration peut le casser sans que rien ne le dise. Mesuré sur une vraie base : /my rendait 500 sur un champ utilisateur qu'un module ne définit plus, alors que les pages publiques et seize applications allaient bien. Deux garde-fous que l'essai a rendus nécessaires. Sans session, /my redirige vers le formulaire de connexion et rend 200 : le compter pour une réussite annoncerait un portail jamais vu. Et la page d'erreur de production ne montre aucune trace : la raison vient du journal, attribuée par CHEMIN — le portail est interrogé en premier, et prendre la dernière trace l'accusait de la panne d'une application. Assisted-by: Claude Opus 5
2026-08-17 22:48:40 -04:00
"Portal": {
"fr": "Portail",
"en": "Portal",
},
"Portal pages opened as the test user": {
"fr": "Pages du portail ouvertes avec l'utilisateur test",
"en": "Portal pages opened as the test user",
},
"not signed in": {
"fr": "pas connecté",
"en": "not signed in",
},
"the page served was the login form": {
"fr": "la page servie était le formulaire de connexion",
"en": "the page served was the login form",
},
"server log": {
"fr": "journal du serveur",
"en": "server log",
},
[FIX] database_cleanup: purge the batch, and say what you are doing Seventeen minutes with no output looks exactly like an infinite loop. It was not one: measured on the running process, zero lock waits and queries changing every sample. It was working, silently, one module every thirty seconds. Both halves were mine. Purging line by line — added so one refusal could not sink a category — calls button_immediate_uninstall() per line, and that reloads the WHOLE registry: 5984 modules re-read each time. The batch is now purged in one call, exactly as the OCA wizard intends, and the per-line isolation only costs when a refusal actually happens. And the tool captured its child's output, so nothing showed before the end. It now relays each step as it arrives, with elapsed seconds, and a timer kills the process group when the deadline passes. --- FR --- [FIX] database_cleanup : purger le lot, et dire ce qu'on fait Dix-sept minutes sans une ligne ressemblent trait pour trait à une boucle infinie. Ce n'en était pas une : mesuré sur le processus vivant, zéro verrou en attente et des requêtes qui changeaient à chaque instantané. Il travaillait, en silence, à raison d'un module toutes les trente secondes. Les deux moitiés étaient de mon fait. Purger ligne par ligne — ajouté pour qu'un refus n'emporte pas la catégorie — appelle button_immediate_uninstall() par ligne, ce qui recharge TOUT le registre : 5984 modules relus à chaque fois. Le lot est désormais purgé en un seul appel, comme le module OCA le prévoit, et l'isolement ne coûte que lorsqu'un refus survient vraiment. Et l'outil capturait la sortie de son enfant : rien avant la fin. Il relaie maintenant chaque étape, temps écoulé compris. Assisted-by: Claude Opus 5
2026-08-17 23:19:04 -04:00
"Purging modules reloads the whole registry: on a big": {
"fr": "Purger les modules recharge tout le registre : sur un grand",
"en": "Purging modules reloads the whole registry: on a big",
},
"addons path this takes minutes, not seconds.": {
"fr": "addons path cela prend des minutes, pas des secondes.",
"en": "addons path this takes minutes, not seconds.",
},
"Took": {
"fr": "Durée :",
"en": "Took",
},
"seconds.": {
"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",
},
"seconds and was stopped.": {
"fr": "secondes et a été arrêté.",
"en": "seconds and was stopped.",
},
"the whole batch": {
"fr": "le lot entier",
"en": "the whole batch",
},
[FIX] migration: repair, replay, and know when to stop The failure that stops an upgrade is almost always the same one — a COW copy left behind on the previous version — and the repair is known. So Enter now repairs, and a repair that actually changed something replays the command by itself. A default that acts must know when to stop, and here it must twice over. Repairing when there is nothing left to repair, then offering it again, loops without end: measured, « no COW copy has drifted » over and over. And a repair that does not help would replay the command forever. Three attempts, then the prompt says plainly that this one needs a developer. The turn counter is deliberately redundant with that logic: both guard the same failure, but the counter holds even if the logic is broken one day by accident. An endless loop in an unattended migration costs a night. Also: the smoke tool forced `ask=input` on its own prompt, which short-circuited auto-run — the question waited for a keystroke nobody was there to give. --- FR --- [FIX] migration : réparer, rejouer, et savoir s'arrêter L'échec qui arrête une migration est presque toujours le même — une copie COW restée sur la version d'avant — et la réparation est connue. Entrée répare donc, et une réparation qui a vraiment changé quelque chose rejoue la commande d'elle-même. Un défaut qui agit doit savoir s'arrêter, et ici deux fois plutôt qu'une. Réparer quand il n'y a plus rien à réparer, puis le reproposer, boucle sans fin : mesuré, « Aucune copie COW n'a dérivé », encore et encore. Et une réparation qui ne suffit pas rejouerait indéfiniment. Trois tentatives, puis l'invite dit qu'il faut un développeur. Le compteur de tours double volontairement cette logique : il tient même si elle est cassée un jour par mégarde. Assisted-by: Claude Opus 5
2026-08-18 00:08:31 -04:00
"Still failing after": {
"fr": "Échoue toujours après",
"en": "Still failing after",
},
"attempts: this one needs a developer.": {
"fr": "tentatives : celle-ci demande un développeur.",
"en": "attempts: this one needs a developer.",
},
"Error detected. Choose, or ctrl+c to": {
"fr": "Erreur détectée. Choisissez, ou ctrl+c pour",
"en": "Error detected. Choose, or ctrl+c to",
},
"stop": {
"fr": "arrêter",
"en": "stop",
},
"Too many turns on this prompt: moving on.": {
"fr": "Trop de tours sur cette invite : on passe.",
"en": "Too many turns on this prompt: moving on.",
},
[FIX] migration: the module-upgrade step no longer waits forever Six « press to continue » prompts on the module-code migration were bare input() calls. Auto-run never reached them, so it stopped there and said nothing — the question had been asked, after all. They now go through the timed reader, where Enter and the countdown mean the same thing. They were English too, on a path the rest of which speaks the system language. The guard that forbids a bare input() only looked at a handful of methods, and internal_module_upgrade was not among them — which is exactly how these six survived the first sweep. It is now covered, and the module menu's own prompts are documented as deliberately blocking: they ask for a module name, a path, a version, and « » after five seconds would be a wrong answer, not a convenience. --- FR --- [FIX] migration : l'étape de migration de code n'attend plus indéfiniment Six invites « appuyez pour continuer » de la migration de code étaient des input() nus. L'auto-exécution ne les atteignait pas : elle s'arrêtait là sans rien dire — la question avait bien été posée. Elles passent désormais par le lecteur temporisé, où Entrée et le compte à rebours font la même chose. Elles étaient aussi en anglais, sur un chemin dont tout le reste parle la langue du système. Le garde-fou qui interdit les input() nus ne regardait qu'une poignée de méthodes, et internal_module_upgrade n'en faisait pas partie — c'est ainsi que ces six-là avaient survécu. Il la couvre maintenant, et les invites du menu de modules sont documentées comme bloquantes à dessein : elles demandent un nom, un chemin, une version. Assisted-by: Claude Opus 5
2026-08-18 02:36:44 -04:00
"Please validate git commit on repos, press to": {
"fr": "Veuillez valider les commits sur les dépôts, appuyez pour",
"en": "Please validate git commit on repos, press to",
},
"Check the migrated code, press to continue": {
"fr": "Vérifiez le code migré, appuyez pour continuer",
"en": "Check the migrated code, press to continue",
},
"Please validate the repo is ready to run": {
"fr": "Veuillez valider que le dépôt est prêt pour",
"en": "Please validate the repo is ready to run",
},
"press to continue": {
"fr": "appuyez pour continuer",
"en": "press to continue",
},
[FIX] migration: a skipped back office must not read as a healthy one The back-office pass was already there, and it works. What did not work was the way it declined: one discreet line at the end of a long report, saying « the database was not neutralized ». On six databases of a real migration the test user is present up to the 15 bump and GONE at 17 and 18 — so the pass stopped silently exactly where a migration does the most damage, and said something that was not even true. The migration knows what it neutralized, so it now asks for the back office by name. A missing test user on a database it neutralized is a finding, printed loudly and counted as a failure. A missing tool is too: returning None made the whole pass vanish without a word. And it says UP FRONT which passes will run, on that database, by name. --- FR --- [FIX] migration : un back-office sauté ne doit pas se lire comme un sain La passe back-office était déjà là et elle fonctionne. Ce qui ne fonctionnait pas, c'est sa façon de renoncer : une ligne discrète en fin d'un long rapport, disant « la base n'a pas été neutralisée ». Sur les six bases d'une vraie migration, l'utilisateur test est présent jusqu'au palier 15 et ABSENT en 17 et 18 — la passe s'arrêtait donc sans bruit là où une migration fait le plus de dégâts, en disant quelque chose de faux. La migration sait ce qu'elle a neutralisé : elle réclame désormais le back-office. Un utilisateur test manquant sur une base qu'elle a neutralisée est une trouvaille, affichée fort et comptée comme un échec. Un outil absent aussi : rendre None faisait disparaître la passe entière. Et elle annonce AVANT de lancer ce qui sera parcouru. Assisted-by: Claude Opus 5
2026-08-19 03:21:25 -04:00
"smoke_internal_ui.py is missing": {
"fr": "smoke_internal_ui.py est absent",
"en": "smoke_internal_ui.py is missing",
},
"the test user is gone from a neutralized database: the back office was NOT checked": {
"fr": "l'utilisateur test a disparu d'une base neutralisée : le back-office n'a PAS été vérifié",
"en": "the test user is gone from a neutralized database: the back office was NOT checked",
},
"Back office NOT browsed": {
"fr": "Back-office NON parcouru",
"en": "Back office NOT browsed",
},
"Public pages, then the back office and /my as the": {
"fr": "Les pages publiques, puis le back-office et /my avec",
"en": "Public pages, then the back office and /my as the",
},
"test user (neutralized database).": {
"fr": "l'utilisateur test (base neutralisée).",
"en": "test user (neutralized database).",
},
"Public pages only:": {
"fr": "Pages publiques seulement :",
"en": "Public pages only:",
},
"was not neutralized, so there is no test user to": {
"fr": "n'a pas été neutralisée, il n'y a donc pas d'utilisateur test pour",
"en": "was not neutralized, so there is no test user to",
},
"sign in with.": {
"fr": "s'y connecter.",
"en": "sign in with.",
},
[ADD] migration: « t » shows where the migration stands A migration crosses six bumps, runs hundreds of commands and lasts hours. The journal said what had been LAUNCHED; it never said what came of it. Three hours in, one reads two hundred command lines without knowing which one failed, nor what the smoke test concluded. « v » could not carry this: it already means « view the differences » in three prompts, and a letter meaning two things is worse than a letter meaning nothing. « t » was free, and it is the same everywhere — one shared string carries both shortcuts, so no prompt can drift. Looking is not answering: the same question comes back afterwards. What was missing was the data. Failures and tool verdicts are now recorded with the step they happened in, bounded so a progression file cannot grow without end. A tool rerun after a repair keeps its LAST verdict and the count of its runs — showing both without distinction would read a repair as a lasting failure. --- FR --- [ADD] migration : « t » montre où en est la migration Une migration traverse six paliers, lance des centaines de commandes et dure des heures. Le journal disait ce qui avait été LANCÉ, jamais ce que cela avait donné. Trois heures plus tard on relit deux cents lignes sans savoir laquelle a échoué, ni ce que le test de fumée a conclu. « v » ne pouvait pas porter cela : il veut déjà dire « voir les différences » dans trois invites, et une lettre qui signifie deux choses est pire qu'une lettre qui ne signifie rien. « t » était libre, et il est le même partout — une seule chaîne porte les deux raccourcis. Regarder n'est pas répondre : la même question revient ensuite. Ce qui manquait, c'étaient les données. Les échecs et les verdicts d'outils sont désormais retenus avec l'étape où ils se sont produits. Assisted-by: Claude Opus 5
2026-08-19 03:53:56 -04:00
"Migration state": {
"fr": "État de la migration",
"en": "Migration state",
},
"started": {
"fr": "démarrée",
"en": "started",
},
"last written": {
"fr": "dernière écriture",
"en": "last written",
},
"current step": {
"fr": "étape courante",
"en": "current step",
},
"Test results": {
"fr": "Résultat des tests",
"en": "Test results",
},
"No tool has run yet.": {
"fr": "Aucun outil n'a encore tourné.",
"en": "No tool has run yet.",
},
"Commands that failed": {
"fr": "Commandes en échec",
"en": "Commands that failed",
},
"What was done, step by step": {
"fr": "Ce qui a été fait, étape par étape",
"en": "What was done, step by step",
},
"runs": {
"fr": "passages",
"en": "runs",
},
"nothing to report": {
"fr": "rien à signaler",
"en": "nothing to report",
},
"findings to look at": {
"fr": "des trouvailles à regarder",
"en": "findings to look at",
},
"the tool itself failed": {
"fr": "l'outil lui-même a échoué",
"en": "the tool itself failed",
},
"unknown result": {
"fr": "résultat inconnu",
"en": "unknown result",
},
"before the first step": {
"fr": "avant la première étape",
"en": "before the first step",
},
"No migration in progress.": {
"fr": "Aucune migration en cours.",
"en": "No migration in progress.",
},
"show the migration state": {
"fr": "afficher l'état de la migration",
"en": "show the migration state",
},
"Nothing to show yet.": {
"fr": "Rien à montrer pour l'instant.",
"en": "Nothing to show yet.",
},
"(b = go back a step, t = show the migration state)": {
"fr": "(b = revenir à une étape précédente,"
" t = afficher l'état de la migration)",
"en": "(b = go back a step, t = show the migration state)",
},
[ADD] migration: reach the state screen from the statistics too The statistics screen answers « what was removed, and why ». The state screen answers « where are we, and what broke ». Two neighbouring questions asked at the same moment: separating them by a menu entry rather than by two commands to remember is what makes them usable. Same letter as the prompts. One that changes meaning from one screen to the next is not learned, it is looked up — and nobody looks it up. Adding it surfaced two ways that screen could not survive auto-run. The interface question is the FIRST one asked after auto-run is switched on, and it was a bare input(): an unattended migration stopped there before it had begun. The statistics menu was one too. And the state screen writes the progression before reading it — but the statistics screen runs before the progression is even loaded, so that write failed on an attribute that does not exist yet. --- FR --- [ADD] migration : atteindre l'état depuis les statistiques aussi L'écran de statistiques répond « qu'a-t-on supprimé, et pourquoi ». Celui de l'état répond « où en est-on, et qu'est-ce qui a cassé ». Deux questions voisines, posées au même moment : les séparer par une entrée de menu plutôt que par deux commandes à retenir est ce qui les rend utilisables. La même lettre que dans les invites. Une lettre qui change de sens d'un écran à l'autre ne s'apprend pas, elle se cherche — et personne ne la cherche. L'ajout a révélé deux façons dont cet écran ne survivait pas au mode auto : la question d'interface, PREMIÈRE posée après l'activation, était un input() nu, et le menu des statistiques aussi. Et l'écran d'état écrit la progression avant de la lire, alors que les statistiques s'ouvrent avant même qu'elle ne soit chargée. Assisted-by: Claude Opus 5
2026-08-19 04:05:03 -04:00
"Migration state (full screen)": {
"fr": "État de la migration (plein écran)",
"en": "Migration state (full screen)",
},
"Export a backup?": {
"fr": "Exporter une sauvegarde ?",
"en": "Export a backup?",
},
"or type filename.zip": {
"fr": "ou tapez nomfichier.zip",
"en": "or type filename.zip",
},
[FIX] smoke: judge the back office AFTER the repair, and say why it failed No timeout was hit and no test was skipped. The back office failed for the same reason the site did: the COW copy of website.submenu breaks /web/login exactly as it breaks the public pages — both go through the same layout. The pass simply ran BEFORE the reset, and unlike the URLs it was never looked at again. It is now, on the same second server. « Session expired » described the consequence and hid the cause. The login page returned 500 and nothing said so, so one looks at the password. The status of that page, and a missing CSRF token, are now named. Diagnosing this, I started Odoo 18 on a 17 database and got 500 on all thirty-seven URLs and on /web/login: a report that says the site is entirely broken when nothing is. database_cleanup already refused that; the smoke tools now share the same guard rather than reimplementing it. --- FR --- [FIX] smoke : juger le back-office APRÈS la réparation, et dire pourquoi Aucun délai n'a été atteint et aucun test n'a été sauté. Le back-office a échoué pour la raison même qui cassait le site : la copie COW de website.submenu casse /web/login comme elle casse les pages publiques — les deux passent par le même gabarit. La passe tournait avant la réinitialisation et, contrairement aux URL, n'était jamais revue. « Session expired » décrivait la conséquence et cachait la cause : la page de connexion rendait 500 sans que rien ne le dise, et l'on cherchait du côté du mot de passe. En diagnostiquant, j'ai lancé un Odoo 18 sur une base 17 : 500 partout, un rapport qui déclare le site entièrement cassé quand rien ne l'est. database_cleanup refusait déjà cela ; les outils de fumée partagent désormais sa garde au lieu d'en écrire une seconde. Assisted-by: Claude Opus 5
2026-08-19 04:53:43 -04:00
"The login page itself failed": {
"fr": "La page de connexion elle-même a échoué",
"en": "The login page itself failed",
},
"No CSRF token on the login page": {
"fr": "Aucun jeton CSRF sur la page de connexion",
"en": "No CSRF token on the login page",
},
"after the reset": {
"fr": "après la réinitialisation",
"en": "after the reset",
},
"The differential detection found nothing; resetting": {
"fr": "La détection différentielle n'a rien trouvé ; réinitialisation",
"en": "The differential detection found nothing; resetting",
},
"the requested key(s) anyway.": {
"fr": "de la ou des clés demandées malgré tout.",
"en": "the requested key(s) anyway.",
},
[ADD] migration state: l, p, r and the two resize keys Four keys, and one bug behind them. « l » separates the commands from the server log: the first says what was LAUNCHED, the second what it ANSWERED, and an update writing tens of thousands of lines buries three commands. « p » hides the summary panel, « - » and « + » move the split, « r » re-reads the disk — the migration writes while one watches, and closing the screen to see the next bump is what one ended up doing. The « missing logs » had two causes, both real. A tail was shown without saying it was one; it now names how many lines are hidden. And the first two steps run before the database is named, so their logs landed under « sans-nom » — outside the migration they belong to, invisible from the screen. They are brought back the moment the name is known, appended never overwritten. The step 4 bug was already fixed: its log held all six bumps because only print_step opened one, and the bump loop never calls it. --- FR --- [ADD] état de migration : l, p, r et les deux touches de taille Quatre touches, et un défaut derrière. « l » sépare les commandes du journal : la première dit ce qui a été LANCÉ, le second ce que cela a RÉPONDU, et une mise à jour qui écrit des dizaines de milliers de lignes enterre trois commandes. « p » cache le panneau de résumé, « - » et « + » déplacent la séparation, « r » relit le disque — la migration écrit pendant qu'on regarde. Les « logs manquants » avaient deux causes. On montrait une fin sans dire que c'en était une ; le nombre de lignes cachées est désormais nommé. Et les deux premières étapes tournent avant que la base ne soit nommée : leurs journaux atterrissaient sous « sans-nom », hors de la migration à laquelle ils appartiennent. Ils la rejoignent dès que le nom est connu. Assisted-by: Claude Opus 5
2026-08-19 06:23:20 -04:00
"lines": {
"fr": "lignes",
"en": "lines",
},
"press l to show": {
"fr": "l pour afficher",
"en": "press l to show",
},
"last": {
"fr": "dernières",
"en": "last",
},
"Refresh": {
"fr": "Rafraîchir",
"en": "Refresh",
},
"Logs": {
"fr": "Journaux",
"en": "Logs",
},
"Panel": {
"fr": "Panneau",
"en": "Panel",
},
"Wider": {
"fr": "Élargir",
"en": "Wider",
},
"Narrower": {
"fr": "Rétrécir",
"en": "Narrower",
},
"finished": {
"fr": "terminée",
"en": "finished",
},
"duration": {
"fr": "durée",
"en": "duration",
},
[ADD] migration: announce the countdown, and cycle three panel states Every prompt now carries « ⏱5s » before the question. One that looks like it waits forever does not invite you to step away, which is the whole point of auto-run. The same clock reports the answer afterwards: it announces the wait, then says what it decided. Enter on the auto-run question already meant no; it now says so, with an explicit default instead of a deduction. And the flag is cleared before asking — an ERPLIBRE_AUTO_EXECUTE inherited from the shell would have answered yes on someone's behalf, and that answer commits every later one. « p » cycles three states rather than toggling one: a plain toggle freed four lines, while what takes the room is the left COLUMN — on a server log every character of width counts. All → no summary → detail only, and back. The state is named in the sub-title, which stays visible in all three: a panel that vanishes without a word reads as a broken screen. --- FR --- [ADD] migration : annoncer le compte à rebours, et trois états de panneaux Chaque invite porte désormais « ⏱5s » devant la question. Une invite qui a l'air d'attendre indéfiniment n'invite pas à s'absenter, et c'est pourtant tout l'intérêt du mode auto. Le même chronomètre rend compte ensuite. Entrée sur la question d'auto-exécution voulait déjà dire non ; elle le DIT maintenant, par un défaut explicite plutôt qu'une déduction. Et le drapeau est effacé avant de demander : une variable héritée du shell aurait répondu « oui » à la place de quelqu'un. « p » enchaîne trois états au lieu d'en basculer un : ce qui prend la place, c'est la colonne de gauche. Tout → sans résumé → détail seul, et l'on revient. L'état est nommé dans le sous-titre. Assisted-by: Claude Opus 5
2026-08-19 06:57:19 -04:00
"Panels": {
"fr": "Panneaux",
"en": "Panels",
},
"summary + list + detail": {
"fr": "résumé + liste + détail",
"en": "summary + list + detail",
},
"list + detail": {
"fr": "liste + détail",
"en": "list + detail",
},
"detail only": {
"fr": "détail seul",
"en": "detail only",
},
[ADD] analyse: what a migration gained and lost, step by step A migration leaves one database per step and they all still exist, so the tool compares them side by side rather than replaying anything. Six Odoo starts would cost an hour, write to the databases and need the checkout switched at each step; the same inspection in SQL takes under half a second per database and touches nothing. Measured on a real migration: seven databases surveyed in 3.8 seconds. It reports what each step gained and lost — modules, models, views, menus, attachments — and ends on the comparison of the ENDS, which is not the sum of the steps: a module dropped at 15 and put back at 17 lost nothing, and adding the steps would count it twice. The signal that matters is rows. A module removed is visible; a table going from four thousand rows to zero is visible nowhere. Two rename heuristics were tried and rejected on real data before the one that holds. Row count alone paired an account tag table with dms_directory — both had seven rows. A shared five-letter word paired two cleanup tables. Overall name similarity separates them. And a renamed table stays in the loss list, annotated: removing it was the real danger, since a wrong pairing would have hidden a genuine loss. --- FR --- [ADD] analyse : ce qu'une migration a gagné et perdu, palier par palier Une migration laisse une base par palier et elles existent toutes encore : l'outil les compare côte à côte plutôt que de rejouer quoi que ce soit. Six démarrages d'Odoo coûteraient une heure et écriraient dans les bases ; la même inspection en SQL prend moins d'une demi-seconde par base. Mesuré sur une vraie migration : sept bases inspectées en 3,8 secondes. Le bilan final compare les EXTRÉMITÉS, ce qui n'est pas la somme des paliers : un module retiré en 15 puis remis en 17 n'a rien perdu. Le signal qui compte, ce sont les lignes. Un module en moins se voit ; une table qui passe de quatre mille lignes à zéro ne se voit nulle part. Deux rapprochements de renommage ont été essayés et rejetés sur des données réelles. Et une table renommée reste dans la liste des pertes, annotée : l'en retirer était le vrai danger. Assisted-by: Claude Opus 5
2026-08-19 08:21:12 -04:00
"errors in the log": {
"fr": "erreurs dans le journal",
"en": "errors in the log",
},
"Migration quality, step by step": {
"fr": "Qualité de la migration, palier par palier",
"en": "Migration quality, step by step",
},
"database not found": {
"fr": "base introuvable",
"en": "database not found",
},
"attachment files missing from the filestore": {
"fr": "fichiers de pièces jointes absents du filestore",
"en": "attachment files missing from the filestore",
},
"From start to finish": {
"fr": "Du départ à l'arrivée",
"en": "From start to finish",
},
"not comparable: a database is missing": {
"fr": "non comparable : une base manque",
"en": "not comparable: a database is missing",
},
"probable table rename(s)": {
"fr": "renommage(s) de table probable(s)",
"en": "probable table rename(s)",
},
"table(s) lost rows": {
"fr": "table(s) ont perdu des lignes",
"en": "table(s) lost rows",
},
"attachments": {
"fr": "pièces jointes",
"en": "attachments",
},
"probably renamed to": {
"fr": "probablement renommée en",
"en": "probably renamed to",
},
"steps": {
"fr": "paliers",
"en": "steps",
},
"Migration": {
"fr": "🚚 Migration",
"en": "🚚 Migration",
[ADD] analyse: what a migration gained and lost, step by step A migration leaves one database per step and they all still exist, so the tool compares them side by side rather than replaying anything. Six Odoo starts would cost an hour, write to the databases and need the checkout switched at each step; the same inspection in SQL takes under half a second per database and touches nothing. Measured on a real migration: seven databases surveyed in 3.8 seconds. It reports what each step gained and lost — modules, models, views, menus, attachments — and ends on the comparison of the ENDS, which is not the sum of the steps: a module dropped at 15 and put back at 17 lost nothing, and adding the steps would count it twice. The signal that matters is rows. A module removed is visible; a table going from four thousand rows to zero is visible nowhere. Two rename heuristics were tried and rejected on real data before the one that holds. Row count alone paired an account tag table with dms_directory — both had seven rows. A shared five-letter word paired two cleanup tables. Overall name similarity separates them. And a renamed table stays in the loss list, annotated: removing it was the real danger, since a wrong pairing would have hidden a genuine loss. --- FR --- [ADD] analyse : ce qu'une migration a gagné et perdu, palier par palier Une migration laisse une base par palier et elles existent toutes encore : l'outil les compare côte à côte plutôt que de rejouer quoi que ce soit. Six démarrages d'Odoo coûteraient une heure et écriraient dans les bases ; la même inspection en SQL prend moins d'une demi-seconde par base. Mesuré sur une vraie migration : sept bases inspectées en 3,8 secondes. Le bilan final compare les EXTRÉMITÉS, ce qui n'est pas la somme des paliers : un module retiré en 15 puis remis en 17 n'a rien perdu. Le signal qui compte, ce sont les lignes. Un module en moins se voit ; une table qui passe de quatre mille lignes à zéro ne se voit nulle part. Deux rapprochements de renommage ont été essayés et rejetés sur des données réelles. Et une table renommée reste dans la liste des pertes, annotée : l'en retirer était le vrai danger. Assisted-by: Claude Opus 5
2026-08-19 08:21:12 -04:00
},
"Quality of a migration, step by step": {
"fr": "📐 Qualité d'une migration, palier par palier",
"en": "📐 Quality of a migration, step by step",
[ADD] analyse: what a migration gained and lost, step by step A migration leaves one database per step and they all still exist, so the tool compares them side by side rather than replaying anything. Six Odoo starts would cost an hour, write to the databases and need the checkout switched at each step; the same inspection in SQL takes under half a second per database and touches nothing. Measured on a real migration: seven databases surveyed in 3.8 seconds. It reports what each step gained and lost — modules, models, views, menus, attachments — and ends on the comparison of the ENDS, which is not the sum of the steps: a module dropped at 15 and put back at 17 lost nothing, and adding the steps would count it twice. The signal that matters is rows. A module removed is visible; a table going from four thousand rows to zero is visible nowhere. Two rename heuristics were tried and rejected on real data before the one that holds. Row count alone paired an account tag table with dms_directory — both had seven rows. A shared five-letter word paired two cleanup tables. Overall name similarity separates them. And a renamed table stays in the loss list, annotated: removing it was the real danger, since a wrong pairing would have hidden a genuine loss. --- FR --- [ADD] analyse : ce qu'une migration a gagné et perdu, palier par palier Une migration laisse une base par palier et elles existent toutes encore : l'outil les compare côte à côte plutôt que de rejouer quoi que ce soit. Six démarrages d'Odoo coûteraient une heure et écriraient dans les bases ; la même inspection en SQL prend moins d'une demi-seconde par base. Mesuré sur une vraie migration : sept bases inspectées en 3,8 secondes. Le bilan final compare les EXTRÉMITÉS, ce qui n'est pas la somme des paliers : un module retiré en 15 puis remis en 17 n'a rien perdu. Le signal qui compte, ce sont les lignes. Un module en moins se voit ; une table qui passe de quatre mille lignes à zéro ne se voit nulle part. Deux rapprochements de renommage ont été essayés et rejetés sur des données réelles. Et une table renommée reste dans la liste des pertes, annotée : l'en retirer était le vrai danger. Assisted-by: Claude Opus 5
2026-08-19 08:21:12 -04:00
},
"Quality": {
"fr": "Qualité",
"en": "Quality",
},
[FIX] migration state: open the quality screen in its own process Pressing « k » crashed with « asyncio.run() cannot be called from a running event loop ». `suspend()` hands the terminal over but does NOT stop the loop, and `app.run()` calls `asyncio.run()` underneath. A subprocess has its own loop and paints the terminal it was given. My test asserted that suspend() was CALLED, and that structure was correct — the launch inside it was not. Structure is not behaviour, and the test that would have caught this is the one that presses the key. It exists now, and a guard refuses the nested start with a sentence instead of forty lines of traceback. The screen module also lost its shebang: it has no main, so the line claimed something that was not true, and the permission rule was right to say so. --- FR --- [FIX] état de migration : ouvrir l'écran de qualité dans son processus « k » plantait sur « asyncio.run() cannot be called from a running event loop ». `suspend()` rend le terminal mais n'arrête PAS la boucle, et `app.run()` appelle `asyncio.run()` en dessous. Un sous-processus a sa propre boucle. Mon test vérifiait que suspend() était APPELÉ — et cette structure était juste ; c'est le lancement à l'intérieur qui ne l'était pas. La structure n'est pas le comportement, et le test qui aurait attrapé cela est celui qui presse la touche. Il existe désormais, et un garde-fou refuse le lancement imbriqué par une phrase plutôt que par quarante lignes de trace. Le module d'écran perd aussi son shebang : sans `main`, cette ligne annonçait une intention qui n'existait pas. Assisted-by: Claude Opus 5
2026-08-19 10:52:49 -04:00
"Already inside a running screen: open it in its own": {
"fr": "Déjà dans un écran en cours : l'ouvrir dans son propre",
"en": "Already inside a running screen: open it in its own",
},
"process instead.": {
"fr": "processus.",
"en": "process instead.",
},
[ADD] migration quality: name the missing files, and show every delta « 254 attachment files missing » does not say which. « m » now lists them, grouped by model AND FIELD — the field is the most useful thing in the row, because it says which field lost its image. On a real migration it splits into 234 res.country/image flags, 13 payment.provider logos, and a handful of scattered attachments including one 255 kB event photo: the first two are module data a reinstall restores, the last is not, and the raw list put them on the same footing. The metadata is read ONLY when asked. One more query per database would lengthen a survey that runs in four seconds, for something one looks at by asking for it. And every figure of a step now carries its change against the previous one. « 2283 views » is a number; « +61 » is information. The first step gets none: inventing a zero there would claim a comparison that does not exist. --- FR --- [ADD] qualité de migration : nommer les fichiers absents, et montrer les écarts « 254 fichiers de pièces jointes absents » ne dit pas lesquels. « m » en donne la liste, groupée par modèle ET PAR CHAMP — le champ est le renseignement le plus utile de la ligne, car il dit lequel a perdu son image. Sur une vraie migration : 234 drapeaux res.country/image, 13 logos de fournisseurs de paiement, et une poignée de pièces jointes éparses dont une photo d'événement de 255 ko. Les deux premiers groupes sont des données de module qu'une réinstallation restaure, le dernier non. Les métadonnées ne sont lues QU'À LA DEMANDE : une requête de plus par base allongerait un parcours qui tient en quatre secondes. Et chaque chiffre d'un palier porte son écart avec le précédent. « 2283 vues » est un nombre, « +61 » est une information. Le premier palier n'en a pas : y inventer un zéro annoncerait une comparaison qui n'existe pas. Assisted-by: Claude Opus 5
2026-08-20 01:52:35 -04:00
"Missing files": {
"fr": "Fichiers absents",
"en": "Missing files",
},
"every attachment file is present": {
"fr": "tous les fichiers de pièces jointes sont présents",
"en": "every attachment file is present",
},
"Could not read their metadata.": {
"fr": "Impossible de lire leurs métadonnées.",
"en": "Could not read their metadata.",
},
"by model and field": {
"fr": "par modèle et par champ",
"en": "by model and field",
},
"one by one": {
"fr": "un par un",
"en": "one by one",
},
"press m to list them": {
"fr": "m pour en voir la liste",
"en": "press m to list them",
},
"Pick a step to see its missing files.": {
"fr": "Choisissez un palier pour voir ses fichiers absents.",
"en": "Pick a step to see its missing files.",
},
[ADD] migration quality: tell Odoo's own redesigns from real losses « 81 tables lost rows » buried the questions worth asking. The largest of them — ir_translation, 32 984 rows — is a redesign Odoo made in 16, when translations moved into jsonb columns. Putting redesigns and real losses on the same footing is the surest way not to see the second kind. A map now names what Odoo moves or retires, and the report splits the list in two: unexplained first, then what Odoo did on purpose. On a real 12 → 18 the split is 57 and 14, and the partition is exact — an explained loss is annotated, never removed, because removing it was the one thing that could hide a real one. Every entry was VERIFIED on that migration by counting both sides, not written from memory: crm_lead_tag 6 → crm_tag +6, mail_channel 26 → discuss_channel +26, account_invoice 651 → account_move +441. A map written from memory would explain losses that are not losses. And a merge whose target gained nothing says so: the explanation does not hold, and that is exactly what the map was meant to surface. --- FR --- [ADD] qualité de migration : distinguer les refontes d'Odoo des vraies pertes « 81 tables ont perdu des lignes » enterrait les questions qui valaient la peine. La plus grosse — ir_translation, 32 984 lignes — est une refonte d'Odoo 16, quand les traductions sont passées en colonnes jsonb. Une carte nomme désormais ce qu'Odoo déplace ou retire, et le rapport sépare la liste en deux : l'inexpliqué d'abord, puis ce qu'Odoo a fait exprès. Sur une vraie 12 → 18 : 57 et 14, et la partition est exacte — une perte expliquée est annotée, jamais retirée. Chaque entrée a été VÉRIFIÉE sur cette migration en comptant les deux côtés : crm_lead_tag 6 → crm_tag +6, mail_channel 26 → discuss_channel +26, account_invoice 651 → account_move +441. Et une fusion dont la table d'accueil n'a rien reçu le dit : l'explication ne tient pas, et c'est précisément ce que la carte devait révéler. Assisted-by: Claude Opus 5
2026-08-20 02:42:18 -04:00
"table(s) lost rows, unexplained": {
"fr": "table(s) ont perdu des lignes, sans explication",
"en": "table(s) lost rows, unexplained",
},
"table(s) explained by an Odoo change": {
"fr": "table(s) expliquées par un changement d'Odoo",
"en": "table(s) explained by an Odoo change",
},
"Cannot read the database: ": {
"fr": "Lecture impossible de la base : ",
"en": "Cannot read the database: ",
},
"Cannot read the package file: ": {
"fr": "Lecture impossible du fichier de packages : ",
"en": "Cannot read the package file: ",
},
"Modules missing from the default package": {
"fr": "📦 Modules absents du package par défaut",
"en": "📦 Modules missing from the default package",
},
"Show every entry": {
"fr": "Tout afficher",
"en": "Show every entry",
},
"List the known packages": {
"fr": "Lister les packages connus",
"en": "List the known packages",
},
"Already repaired: the group exists.": {
"fr": "Déjà réparé : le groupe existe.",
"en": "Already repaired: the group exists.",
},
"DMS access groups:": {
"fr": "Groupes d'accès DMS :",
"en": "DMS access groups:",
},
"DMS documents in the database": {
"fr": "Documents DMS dans la base",
"en": "DMS documents in the database",
},
"No DMS user to test visibility with.": {
"fr": "Aucun utilisateur DMS pour éprouver la visibilité.",
"en": "No DMS user to test visibility with.",
},
"Nothing written. Re-run with --apply.": {
"fr": "Rien d'écrit. Relancer avec --apply.",
"en": "Nothing written. Re-run with --apply.",
},
"Now visible to": {
"fr": "Désormais visibles pour",
"en": "Now visible to",
},
"Visible to": {
"fr": "Visibles pour",
"en": "Visible to",
},
"Would create one access group over": {
"fr": "Créerait un groupe d'accès sur",
"en": "Would create one access group over",
},
"file(s)": {
"fr": "fichier(s)",
"en": "file(s)",
},
"folder(s)": {
"fr": "dossier(s)",
"en": "folder(s)",
},
"root folder(s)": {
"fr": "dossier(s) racine",
"en": "root folder(s)",
},
[ADD] migration: repair DMS at the 13 bump, and catch the whole class The repair now runs by itself where the breakage happens: MuK becomes OCA DMS at 13 and nowhere else, so the hook is gated on that bump. It stays quiet on databases without DMS and on ones already repaired, and a successful --apply now exits 0 instead of 1. The wider hole stays open otherwise: data PRESENT that a global rule hides entirely. Row counts said everything was there — true. The smoke test served every page — true. Nobody could reach the records. The new check runs at every bump and names any model with rows that no active internal user can see a single one of. Proven both ways on a copy: 85 models clean after the repair, dms.file and dms.directory named before it. --- FR --- La réparation s'exécute désormais là où la casse a lieu : MuK devient OCA DMS au palier 13 et nulle part ailleurs, d'où le garde. Elle se tait sur une base sans DMS et sur une base déjà réparée, et un --apply réussi rend 0 au lieu de 1. Le trou plus large restait ouvert : des données PRÉSENTES qu'une règle globale masque entièrement. Les comptages disaient « tout est là » — vrai. Le test de fumée servait les pages — vrai. Personne n'atteignait les données. Le nouveau test tourne à chaque palier et nomme tout modèle dont aucun utilisateur interne actif ne voit une seule ligne. Éprouvé dans les deux sens sur une copie : 85 modèles sains après réparation, dms.file et dms.directory nommés avant. Assisted-by: Claude Opus 5
2026-08-21 05:25:23 -04:00
"No DMS in this database, nothing to repair.": {
"fr": "Pas de DMS dans cette base, rien à réparer.",
"en": "No DMS in this database, nothing to repair.",
},
"Every one of them is visible to someone.": {
"fr": "Chacun est visible par quelqu'un.",
"en": "Every one of them is visible to someone.",
},
"No internal user to test visibility with.": {
"fr": "Aucun utilisateur interne pour éprouver la visibilité.",
"en": "No internal user to test visibility with.",
},
"The data is there; a global rule hides all of it.": {
"fr": "Les données sont là ; une règle globale les masque entièrement.",
"en": "The data is there; a global rule hides all of it.",
},
"checked against": {
"fr": "éprouvés avec",
"en": "checked against",
},
"internal user(s)": {
"fr": "utilisateur(s) interne(s)",
"en": "internal user(s)",
},
"model(s) nobody can see a single row of": {
"fr": "modèle(s) dont personne ne voit une seule ligne",
"en": "model(s) nobody can see a single row of",
},
"model(s) with a global rule and some data,": {
"fr": "modèle(s) avec une règle globale et des données,",
"en": "model(s) with a global rule and some data,",
},
"row(s)": {
"fr": "ligne(s)",
"en": "row(s)",
},
"Against OpenUpgrade": {
"fr": "Face à OpenUpgrade",
"en": "Against OpenUpgrade",
},
"NOT declared by OpenUpgrade": {
"fr": "NON déclarés par OpenUpgrade",
"en": "NOT declared by OpenUpgrade",
},
"No OpenUpgrade analysis for this step.": {
"fr": "Pas d'analyse OpenUpgrade pour ce palier.",
"en": "No OpenUpgrade analysis for this step.",
},
"became a per-company jsonb column": {
"fr": "passés en colonne jsonb par société",
"en": "became a per-company jsonb column",
},
"computed now — the field remains, the column does not": {
"fr": "devenus calculés — le champ demeure, la colonne non",
"en": "computed now — the field remains, the column does not",
},
"core module(s) analysed": {
"fr": "module(s) cœur analysé(s)",
"en": "core module(s) analysed",
},
"declared obsolete": {
"fr": "déclarés obsolètes",
"en": "declared obsolete",
},
"declared removed": {
"fr": "déclarés supprimés",
"en": "declared removed",
},
"field(s) lost": {
"fr": "champ(s) perdu(s)",
"en": "field(s) lost",
},
"model(s) lost": {
"fr": "modèle(s) perdu(s)",
"en": "model(s) lost",
},
"moved to another module": {
"fr": "déplacés vers un autre module",
"en": "moved to another module",
},
"renamed by Odoo": {
"fr": "renommés par Odoo",
"en": "renamed by Odoo",
},
"their model went away too": {
"fr": "leur modèle a disparu aussi",
"en": "their model went away too",
},
"in a module OpenUpgrade does not analyse": {
"fr": "dans un module qu'OpenUpgrade n'analyse pas",
"en": "in a module OpenUpgrade does not analyse",
},
"model(s)": {
"fr": "modèle(s)",
"en": "model(s)",
},
"other field name(s)": {
"fr": "autre(s) nom(s) de champ",
"en": "other field name(s)",
},
[ADD] migration: repair views whose stored type contradicts their parent Odoo refused to load a database: it validated a <search> arch with tree rules. The view is not a COW copy -- the COW tools were right to say they had nothing to reset -- its stored `type` simply lies. Nothing would ever have fixed it. In ir_ui_view.py, `type` is filled in `create`, and only when absent; `write` never recomputes it. A wrong value stays wrong, and `-u module` fails on the validation it causes before it could rewrite anything. Plain SQL, no ORM: the registry is what will not load, and a repair that needed Odoo to fix what stops Odoo would be useless. The rule is Odoo's own -- an inherited view takes its parent's type -- and it holds: zero disagreement on a pristine 18 install and on four migrated databases, exactly one on the database that refused to load. --- FR --- Odoo refusait de charger une base : il validait un arch <search> avec les règles d'un tree. La vue n'est pas une copie COW — les outils COW avaient raison de dire qu'ils n'avaient rien à réinitialiser — c'est son `type` stocké qui ment. Rien ne l'aurait jamais réparé. Dans ir_ui_view.py, `type` est rempli dans `create`, et seulement s'il est absent ; `write` ne le recalcule jamais. Une valeur fausse le reste, et `-u module` échoue sur la validation qu'elle provoque avant de pouvoir réécrire quoi que ce soit. En SQL, sans ORM : c'est le registre qui ne charge plus, et une réparation qui aurait besoin d'Odoo ne servirait à rien. La règle est celle d'Odoo — une vue héritée prend le type de son parent — et elle tient : zéro écart sur une 18 neuve et sur quatre bases migrées, exactement un sur celle qui refusait de charger. Assisted-by: Claude Opus 5
2026-08-22 00:46:42 -04:00
"Filestore of": {
"fr": "Filestore de",
"en": "Filestore of",
},
"file(s) missing": {
"fr": "fichier(s) absent(s)",
"en": "file(s) missing",
},
"file(s) on disk": {
"fr": "fichier(s) sur disque",
"en": "file(s) on disk",
},
"file(s) sit in nested filestores Odoo never reads.": {
"fr": "fichier(s) échoué(s) dans des filestores imbriqués qu'Odoo ne lit jamais.",
"en": "file(s) sit in nested filestores Odoo never reads.",
},
"intact in another database's filestore": {
"fr": "intacts dans le filestore d'une autre base",
"en": "intact in another database's filestore",
},
"its field no longer exists — nothing reads it": {
"fr": "leur champ n'existe plus — rien ne les lit",
"en": "its field no longer exists — nothing reads it",
},
"nowhere to be found — truly lost": {
"fr": "introuvables — réellement perdus",
"en": "nowhere to be found — truly lost",
},
"still in a backup zip": {
"fr": "encore dans une sauvegarde zip",
"en": "still in a backup zip",
},
"stored attachment(s)": {
"fr": "pièce(s) jointe(s) stockée(s)",
"en": "stored attachment(s)",
},
"stranded in a nested filestore Odoo never reads": {
"fr": "échoués dans un filestore imbriqué qu'Odoo ne lit pas",
"en": "stranded in a nested filestore Odoo never reads",
},
"Files": {
"fr": "Fichiers",
"en": "Files",
},
"Attachment files missing from the filestore": {
"fr": "🗄 Fichiers de pièces jointes absents du filestore",
"en": "🗄 Attachment files missing from the filestore",
},
"Scanning filestores and backups…": {
"fr": "Examen des filestores et des sauvegardes…",
"en": "Scanning filestores and backups…",
},
"Filestore restored:": {
"fr": "Filestore restauré :",
"en": "Filestore restored:",
},
"file(s) in place": {
"fr": "fichier(s) en place",
"en": "file(s) in place",
},
"Filestore restore looks wrong for": {
"fr": "Restauration du filestore suspecte pour",
"en": "Filestore restore looks wrong for",
},
"landed in a nested filestore Odoo never reads": {
"fr": "atterris dans un filestore imbriqué qu'Odoo ne lit jamais",
"en": "landed in a nested filestore Odoo never reads",
},
"To fix:": {
"fr": "Pour corriger :",
"en": "To fix:",
},
"never landed at all": {
"fr": "ne sont arrivés nulle part",
"en": "never landed at all",
},
"record still exists": {
"fr": "l'enregistrement existe encore",
"en": "record still exists",
},
"record is gone — nothing will miss it": {
"fr": "l'enregistrement a disparu — personne ne la cherchera",
"en": "record is gone — nothing will miss it",
},
"Delete these": {
"fr": "Supprimer ces",
"en": "Delete these",
},
"Directory:": {
"fr": "Dossier :",
"en": "Directory:",
},
"No nested filestore to tidy.": {
"fr": "Pas de filestore imbriqué à ranger.",
"en": "No nested filestore to tidy.",
},
"Nothing to purge.": {
"fr": "Rien à purger.",
"en": "Nothing to purge.",
},
"Nothing was deleted.": {
"fr": "Rien n'a été supprimé.",
"en": "Nothing was deleted.",
},
"Nothing was moved.": {
"fr": "Rien n'a été déplacé.",
"en": "Nothing was moved.",
},
"The purge failed.": {
"fr": "La purge a échoué.",
"en": "The purge failed.",
},
"attachment row(s) deleted.": {
"fr": "ligne(s) de pièce jointe supprimée(s).",
"en": "attachment row(s) deleted.",
},
"attachment row(s) for good?": {
"fr": "ligne(s) de pièce jointe, définitivement ?",
"en": "attachment row(s) for good?",
},
"duplicate(s) removed": {
"fr": "doublon(s) supprimé(s)",
"en": "duplicate(s) removed",
},
"file(s) to move up": {
"fr": "fichier(s) à remonter",
"en": "file(s) to move up",
},
"moved up": {
"fr": "remonté(s)",
"en": "moved up",
},
"pure duplicate(s) to delete": {
"fr": "doublon(s) pur(s) à supprimer",
"en": "pure duplicate(s) to delete",
},
"🧹 Purge attachments whose field no longer exists": {
"fr": "🧹 Purger les pièces jointes dont le champ n'existe plus",
"en": "🧹 Purge attachments whose field no longer exists",
},
"🧹 Tidy the nested filestore Odoo never reads": {
"fr": "🧹 Ranger le filestore imbriqué qu'Odoo ne lit pas",
"en": "🧹 Tidy the nested filestore Odoo never reads",
},
"The purge ran but said nothing.": {
"fr": "La purge a tourné sans rien annoncer.",
"en": "The purge ran but said nothing.",
},
"row(s) whose field is gone still hold their file": {
"fr": "ligne(s) à champ disparu retiennent encore leur fichier",
"en": "row(s) whose field is gone still hold their file",
},
"file(s) sit in a nested filestore Odoo never reads.": {
"fr": "fichier(s) échoué(s) dans un filestore imbriqué qu'Odoo ne lit jamais.",
"en": "file(s) sit in a nested filestore Odoo never reads.",
},
"such file(s) sit in OTHER databases filestores.": {
"fr": "fichier(s) de ce genre dorment dans les filestores d'AUTRES bases.",
"en": "such file(s) sit in OTHER databases filestores.",
},
"Could not inspect the attachments: ": {
"fr": "Inspection des pièces jointes impossible : ",
"en": "Could not inspect the attachments: ",
},
"Delete them?": {
"fr": "Les supprimer ?",
"en": "Delete them?",
},
"No attachment points at a field that is gone.": {
"fr": "Aucune pièce jointe ne pointe vers un champ disparu.",
"en": "No attachment points at a field that is gone.",
},
"attachment(s) point at a field that no longer exists": {
"fr": "pièce(s) jointe(s) pointent vers un champ qui n'existe plus",
"en": "attachment(s) point at a field that no longer exists",
},
"Corrected.": {
"fr": "Corrigé.",
"en": "Corrected.",
},
"No module update will fix this: the type is set once,": {
"fr": "Aucune mise à jour de module ne corrigera cela : le type est posé une fois,",
"en": "No module update will fix this: the type is set once,",
},
"The correction failed.": {
"fr": "La correction a échoué.",
"en": "The correction failed.",
},
"at creation. Re-run with --apply.": {
"fr": "à la création. Relancer avec --apply.",
"en": "at creation. Re-run with --apply.",
},
"their inheritance": {
"fr": "leur héritage",
"en": "their inheritance",
},
"view(s) whose stored type contradicts": {
"fr": "vue(s) dont le type stocké contredit",
"en": "view(s) whose stored type contradicts",
},
"Fix views whose type contradicts their": {
"fr": "Corriger les vues dont le type contredit leur",
"en": "Fix views whose type contradicts their",
},
"inheritance": {
"fr": "héritage",
"en": "inheritance",
},
"Correct them?": {
"fr": "Les corriger ?",
"en": "Correct them?",
},
[ADD] migration: offer the theme uninstall where the theme is blamed The question was asked once, at step one, and a flag kept it from ever returning. But a theme only becomes incompatible at a given bump, hours later -- asking at the start could not cover it. Entry [6] appears when TWO conditions hold: a theme is installed, and the recent output names it. One condition alone would offer to wreck the site's design over an error that has nothing to do with it. Also, the 17 to 18 rename OpenUpgrade declares and never applies: forum.post / tag_ids : column1 is now 'forum_post_id' ('forum_id') website_forum/18.0.1.2/ holds only an analysis, no script, so the load dies on a foreign key to a column that does not exist and leaves the database half migrated. --- FR --- La question était posée une fois, à l'étape 1, et un drapeau l'empêchait de revenir. Or un thème ne devient incompatible qu'à un palier donné, des heures plus tard : la poser au départ ne pouvait pas suffire. L'entrée [6] paraît quand DEUX conditions tiennent — un thème installé, et la sortie récente qui le nomme. Une seule offrirait de casser le design du site pour une panne étrangère. Et le renommage 17 → 18 qu'OpenUpgrade déclare sans jamais l'appliquer : forum.post / tag_ids : column1 is now 'forum_post_id' ('forum_id') website_forum/18.0.1.2/ ne porte qu'une analyse, aucun script : le chargement meurt sur une clé étrangère vers une colonne absente et laisse la base à moitié migrée. Assisted-by: Claude Opus 5
2026-08-22 01:52:53 -04:00
"Uninstall the theme(s) the error names": {
"fr": "Désinstaller le(s) thème(s) que l'erreur nomme",
"en": "Uninstall the theme(s) the error names",
},
[ADD] migration: convert the <tree> tags Odoo 18 renamed to <list> OpenUpgrade converts the COLUMN and never the ARCH: def _fix_list_view_type(cr): cr.execute("UPDATE ir_ui_view SET type='list' WHERE type='tree'") so the load dies on "the root node of a list view should be <list>". A module view recovers at its next update, which rewrites the arch from XML. A view with no xmlid -- a hand-made list, a website copy -- is rewritten by nothing and stays broken forever. The report says which is which, because the two call for different work. Every occurrence, not just the root: a <tree> nested in a form is a one2many list and is refused just the same. Odoo 18's own addons hold none, so nothing legitimate is at risk. Gated on version 18: before it, the tag is correct and "fixing" it would break healthy views. Four tests run the SQL against a real PostgreSQL. A text assertion cannot see what a query does -- it took a live run to prove every language of the jsonb arch is converted, not just en_US. --- FR --- OpenUpgrade convertit la COLONNE et jamais l'ARCH, d'où l'échec sur « le nœud racine d'une vue list devrait être <list> ». Une vue de module s'en remet à sa prochaine mise à jour ; une vue sans xmlid — liste faite main, copie de site — n'est réécrite par rien. Le rapport distingue les deux, car le travail diffère. Toutes les occurrences, pas seulement la racine : un <tree> imbriqué dans un formulaire est une liste one2many, refusée pareillement. Les addons d'Odoo 18 n'en portent aucun. Bridé à la 18 : avant, la balise est juste. Quatre tests exécutent le SQL contre un vrai PostgreSQL. Une assertion sur du texte ne voit pas ce qu'une requête fait — il a fallu l'exécuter pour prouver que toutes les langues du jsonb sont converties. Assisted-by: Claude Opus 5
2026-08-22 02:34:22 -04:00
"Every view agrees with what Odoo expects.": {
"fr": "Toutes les vues s'accordent avec ce qu'Odoo attend.",
"en": "Every view agrees with what Odoo expects.",
},
"custom — nothing will ever rewrite it": {
"fr": "faite main — rien ne la réécrira jamais",
"en": "custom — nothing will ever rewrite it",
},
"from a module — a module update also fixes it": {
"fr": "d'un module — une mise à jour la répare aussi",
"en": "from a module — a module update also fixes it",
},
"renamed to <list>": {
"fr": "a renommée en <list>",
"en": "renamed to <list>",
},
"view(s) still carry a <tree> tag Odoo 18": {
"fr": "vue(s) portent encore une balise <tree> qu'Odoo 18",
"en": "view(s) still carry a <tree> tag Odoo 18",
},
[FIX] smoke: name the URL that actually failed, keep the previous log The report named the sitemap URL, never the one the redirect chain ended on. On this site every page goes through two or three hops -- measured, 146 for 55 pages, between the language prefix and the canonical slug -- so a 500 at the end was reported against a page that answers perfectly well. One goes and checks it, finds it healthy, and concludes the tool is wrong. urllib carries the answer: HTTPError.url is the URL that produced the error, not the one requested. fetch now returns it and the report shows it when it differs. A timeout was never reported as 500 -- fetch returns 0 and the report writes "no answer" -- but the evidence for a real 500 did evaporate: the Odoo log was opened with "w", so replaying the test erased the trace of the failure one had just seen. One generation is kept now. --- FR --- Le rapport nommait l'URL du sitemap, jamais celle où la chaîne de redirections aboutit. Sur ce site chaque page en traverse deux ou trois — mesuré, 146 pour 55 pages — donc un 500 au bout était imputé à une page qui répond très bien. On va la vérifier, on la trouve saine, et l'on conclut que l'outil se trompe. urllib porte la réponse : HTTPError.url est l'URL qui a produit l'erreur. `fetch` la rend, et le rapport l'affiche quand elle diffère. Un dépassement de délai n'a jamais été rendu comme un 500 — `fetch` rend 0, écrit « aucune réponse » — mais la preuve d'un vrai 500 s'évaporait : le journal Odoo était ouvert en « w », donc rejouer le test effaçait la trace qu'on venait de voir. Une génération est gardée. Assisted-by: Claude Opus 5
2026-08-22 03:19:45 -04:00
"failed at": {
"fr": "a échoué sur",
"en": "failed at",
},
"Census": {
"fr": "Recensement",
"en": "Census",
},
"Compared against nothing — census only.": {
"fr": "Comparé à rien — recensement seul.",
"en": "Compared against nothing — census only.",
},
"Default package:": {
"fr": "Package par défaut :",
"en": "Default package:",
},
"Every suggested module is installed.": {
"fr": "Tous les modules suggérés sont installés.",
"en": "Every suggested module is installed.",
},
"Installed modules by author": {
"fr": "Modules installés, par auteur",
"en": "Installed modules by author",
},
"Modules of": {
"fr": "Modules de",
"en": "Modules of",
},
"No default package known for this version": {
"fr": "Aucun package par défaut connu pour cette version",
"en": "No default package known for this version",
},
"Odoo": {
"fr": "Odoo",
"en": "Odoo",
},
"absent from the addons path — sync the repo first": {
"fr": "hors du chemin des addons — synchroniser le dépôt d'abord",
"en": "absent from the addons path — sync the repo first",
},
"also needs": {
"fr": "réclame aussi",
"en": "also needs",
},
"half-way — finish the pending update": {
"fr": "à mi-chemin — finir la mise à jour en attente",
"en": "half-way — finish the pending update",
},
"installed": {
"fr": "déjà en place",
"en": "installed",
},
"known": {
"fr": "connus",
"en": "known",
},
"known to the database — install it": {
"fr": "dans la base — à installer",
"en": "known to the database — install it",
},
"present but broken — fix the dependency": {
"fr": "en panne de dépendance — à réparer",
"en": "present but broken — fix the dependency",
},
"suggested module(s)": {
"fr": "module(s) suggéré(s)",
"en": "suggested module(s)",
},
"suggested module(s) not installed": {
"fr": "module(s) suggéré(s) non installé(s)",
"en": "suggested module(s) not installed",
},
"unknown author": {
"fr": "auteur inconnu",
"en": "unknown author",
},
"rows dropped, the table remains": {
"fr": "lignes supprimées, la table demeure",
"en": "rows dropped, the table remains",
[ADD] migration quality: tell Odoo's own redesigns from real losses « 81 tables lost rows » buried the questions worth asking. The largest of them — ir_translation, 32 984 rows — is a redesign Odoo made in 16, when translations moved into jsonb columns. Putting redesigns and real losses on the same footing is the surest way not to see the second kind. A map now names what Odoo moves or retires, and the report splits the list in two: unexplained first, then what Odoo did on purpose. On a real 12 → 18 the split is 57 and 14, and the partition is exact — an explained loss is annotated, never removed, because removing it was the one thing that could hide a real one. Every entry was VERIFIED on that migration by counting both sides, not written from memory: crm_lead_tag 6 → crm_tag +6, mail_channel 26 → discuss_channel +26, account_invoice 651 → account_move +441. A map written from memory would explain losses that are not losses. And a merge whose target gained nothing says so: the explanation does not hold, and that is exactly what the map was meant to surface. --- FR --- [ADD] qualité de migration : distinguer les refontes d'Odoo des vraies pertes « 81 tables ont perdu des lignes » enterrait les questions qui valaient la peine. La plus grosse — ir_translation, 32 984 lignes — est une refonte d'Odoo 16, quand les traductions sont passées en colonnes jsonb. Une carte nomme désormais ce qu'Odoo déplace ou retire, et le rapport sépare la liste en deux : l'inexpliqué d'abord, puis ce qu'Odoo a fait exprès. Sur une vraie 12 → 18 : 57 et 14, et la partition est exacte — une perte expliquée est annotée, jamais retirée. Chaque entrée a été VÉRIFIÉE sur cette migration en comptant les deux côtés : crm_lead_tag 6 → crm_tag +6, mail_channel 26 → discuss_channel +26, account_invoice 651 → account_move +441. Et une fusion dont la table d'accueil n'a rien reçu le dit : l'explication ne tient pas, et c'est précisément ce que la carte devait révéler. Assisted-by: Claude Opus 5
2026-08-20 02:42:18 -04:00
},
"retired from the database": {
"fr": "retirée de la base",
"en": "retired from the database",
},
"but it gained nothing": {
"fr": "mais elle n'a rien reçu",
"en": "but it gained nothing",
},
"there": {
"fr": "là-bas",
"en": "there",
},
[ADD] migration quality: biggest losses first, and the full lists behind « d » Fifty-seven losses are read from the top, not in alphabetical order. They are now sorted by rows LOST, not by rows there before: a table of ten thousand that drops two matters less than one of a thousand that empties. « d » cycles the pane through the complete lists — modules, models, fields, COW copies, tables — and back to the summary. The summary cuts at eight entries and is right to; but when one is looking for whether ONE module survived, the truncated list does not answer, and that is exactly when it is needed. Two inventories were missing and both matter. FIELDS: a lost field is a lost column of data, finer than a model, which can survive emptied of half of its own. COW COPIES by key: it is the key one resets, and by the key one finds them again across versions. On a real 12 → 18 they read 11 036 → 15 069 fields and 64 → 131 copies, of which 28 disappeared — customisations nothing was reporting until now. One mode replaces two flags: « show missing files » and « show the model list » cannot both be true, and two booleans let that impossible state be written. The mode is named in the sub-title, because a pane that changes without saying why reads as a broken screen. --- FR --- [ADD] qualité de migration : les plus grosses pertes d'abord, les listes derrière « d » Cinquante-sept pertes se lisent par le haut. Elles sont triées sur le volume PERDU, non sur le volume présent avant : une table de dix mille qui en perd deux compte moins qu'une de mille qui se vide. « d » fait défiler le panneau à travers les listes entières — modules, modèles, champs, copies COW, tables — puis revient au résumé. Deux inventaires manquaient. Les CHAMPS : un champ perdu est une colonne de données perdue. Les COPIES COW par leur clé : sur une vraie 12 → 18, 28 ont disparu — des personnalisations que rien ne signalait. Un seul mode remplace deux drapeaux, qui laissaient écrire un état impossible. Il est nommé dans le sous-titre. Assisted-by: Claude Opus 5
2026-08-20 03:47:02 -04:00
"lost": {
"fr": "perdus",
"en": "lost",
},
"gained": {
"fr": "gagnés",
"en": "gained",
},
"fields": {
"fr": "champs",
"en": "fields",
},
"cow": {
"fr": "copies COW",
"en": "cow",
},
"tables": {
"fr": "tables",
"en": "tables",
},
"Details": {
"fr": "Détails",
"en": "Details",
},
"summary": {
"fr": "résumé",
"en": "summary",
},
[ADD] migration: run the OCA database cleanup before testing the pages The eight purges are not independent: purging a model frees the columns that referenced it, purging a table frees the data pointing at it. One pass is never enough, so the requested order runs again until a full pass repairs nothing new. Refusals are expected — a foreign key still holds, a module says no. Purging a list at once loses everything to the first one, so each entry purges inside its own savepoint: a refusal rolls back that entry alone. What one pass could not take, the next may, once its neighbours are gone. Leftovers are reported as a warning: a database can carry some that nothing removes, and stopping there would help no one. It refuses a version mismatch. Measured while building it: a shell on Odoo 14 opened against a 17.0 database went rewriting ir_model before dying on a jsonb it did not know. An older Odoo does not merely fail on a newer database — it writes on the way. --- FR --- [ADD] migration : lancer le nettoyage OCA avant de tester les pages Les huit purges ne sont pas indépendantes : purger un modèle libère les colonnes qui le référençaient, purger une table libère les données qui la visaient. Une passe ne suffit jamais, l'ordre demandé est donc rejoué jusqu'à ce qu'une passe entière ne répare plus rien. Les refus sont attendus — une clé étrangère tient, un module dit non. Purger d'un bloc perdrait tout au premier : chaque entrée passe dans son propre point de reprise, un refus n'emporte que la sienne. Ce qu'une passe n'a pu prendre, la suivante le peut, une fois les voisines parties. Les restes sont un avertissement : une base peut en porter que rien ne retire, et s'arrêter là n'aiderait personne. Il refuse une version qui ne correspond pas. Mesuré en le construisant : un shell en Odoo 14 ouvert sur une base 17.0 est parti réécrire ir_model avant de mourir sur un jsonb inconnu de lui. Un Odoo plus ancien n'échoue pas simplement sur une base plus récente — il écrit en chemin. Assisted-by: Claude Opus 5
2026-08-17 09:05:03 -04:00
"Clean the database before testing the pages?": {
"fr": "Nettoyer la base avant de tester les pages ?",
"en": "Clean the database before testing the pages?",
},
[ADD] migration: an auto-run that takes the default after five seconds A migration asks dozens of questions whose answer is almost always the one offered, and someone has to stand there pressing Enter for hours. Asked before the version choice — so it covers that choice too, now that it has a default — and off by default: it decides in your place. Every prompt of the migration goes through one reader, seventeen of them. A prompt left as a bare input() would know nothing of auto mode and would block the run with nothing to say why; a test reads the function and rejects any that appears. click.prompt had to go: it cannot hand back control after a delay, so auto would have stopped at the first question. select() rather than a thread or an alarm — a thread would leave a blocked input() behind it, stealing the next keystroke. --- FR --- [ADD] migration : une auto-exécution qui prend le défaut après cinq secondes Une migration pose des dizaines de questions dont la réponse est presque toujours celle proposée, et il faut rester devant à taper Entrée pendant des heures. Posée avant le choix de version — donc elle vaut aussi pour lui, maintenant qu'il a un défaut — et éteinte par défaut : elle décide à votre place. Toutes les invites de la migration passent par une seule lecture, dix-sept d'un coup. Une invite laissée en input() nu ne saurait rien du mode auto et bloquerait la migration sans rien dire ; un test lit la fonction et refuse toute apparition. click.prompt devait partir : il ne sait pas rendre la main après un délai, l'auto se serait arrêté à la première question. select() plutôt qu'un fil ou une alarme — un fil laisserait un input() bloqué derrière lui, qui volerait la frappe suivante. Assisted-by: Claude Opus 5
2026-08-17 17:35:31 -04:00
"Auto-run: take the default answer after": {
"fr": "Auto-exécution : prendre la réponse par défaut après",
"en": "Auto-run: take the default answer after",
},
"seconds?": {
"fr": "secondes ?",
"en": "seconds?",
},
"Auto-run on: each prompt waits": {
"fr": "Auto-exécution activée : chaque invite attend",
"en": "Auto-run on: each prompt waits",
},
"seconds, then takes its default.": {
"fr": "secondes, puis prend son défaut.",
"en": "seconds, then takes its default.",
},
"(default)": {
"fr": "(défaut)",
"en": "(default)",
},
[FIX] migration: the cleanup installs its own module, and survives a refusal Three defects, all mine, all found on a real run. Creating a wizard could fail and leave the transaction ABORTED. The next name read died on it, outside any guard, and the whole script stopped with no report at all — only a traceback. Creating and reading the names now happen inside the savepoint, and a report is printed whatever happens: knowing what was done matters more than the trace of what broke. « No orphaned models found » is a UserError: the module signals the EMPTY by raising. Counting it as a failure made a healthy database look broken, four warnings out of five kinds. The module was installed at step 3, after being used at step 2, so the first run had no wizard at all and answered « nothing to do ». It is now installed by the tool itself, and step 3 no longer asks to redo by hand what has just been done automatically. --- FR --- [FIX] migration : le nettoyage pose son module, et survit à un refus Trois défauts, tous à moi, tous trouvés sur une vraie exécution. Créer un assistant pouvait échouer en laissant la transaction AVORTÉE. La lecture de nom suivante mourait dessus, hors de tout garde, et le script s'arrêtait sans aucun rapport — juste une trace. La création et la lecture des noms sont désormais dans le point de reprise, et un rapport est imprimé quoi qu'il arrive : savoir ce qui a été fait vaut mieux que la trace de ce qui a cassé. « No orphaned models found » est une UserError : le module signale le VIDE en levant. Le compter comme un échec faisait passer une base saine pour cassée, quatre avertissements sur cinq catégories. Le module était installé à l'étape 3, après avoir servi à l'étape 2 : le premier passage n'avait donc aucun assistant et répondait « rien à faire ». L'outil le pose lui-même, et l'étape 3 ne demande plus de refaire à la main ce qui vient d'être fait. Assisted-by: Claude Opus 5
2026-08-17 17:45:04 -04:00
"database_cleanup is not installed on this base;": {
"fr": "database_cleanup n'est pas installé sur cette base ;",
"en": "database_cleanup is not installed on this base;",
},
"installing it first.": {
"fr": "installation préalable.",
"en": "installing it first.",
},
"Could not install database_cleanup.": {
"fr": "Impossible d'installer database_cleanup.",
"en": "Could not install database_cleanup.",
},
"Nothing to decide yet": {
"fr": "Rien à décider pour l'instant",
"en": "Nothing to decide yet",
},
"the migration will offer to neutralize these copies": {
"fr": "la migration proposera de neutraliser ces copies",
"en": "the migration will offer to neutralize these copies",
},
"at the version bump itself, showing what each one holds.": {
"fr": "au moment du palier lui-même, en montrant ce que chacune"
" contient.",
"en": "at the version bump itself, showing what each one holds.",
},
[ADD] migration: go back a step from a prompt, and name the DB Realising at a prompt that an earlier step deserved another answer had one way out: Ctrl+C. That leaves the progression as it stands and makes you find the resume screen again to rewind. « b » does it properly — it shows the steps, rewinds the state, writes it, and says what to relaunch. Cancelling that must not stop the migration, which is the trap: it returns to the same prompt, exactly where you were. Removing that guard makes two tests fail. The COW warning printed « -d DB -t odooXX.0 » for commands meant to be pasted. It knows both values, so it prints them, and offers --shape as well since that is half the answer. Checked on the four outcomes: a normal answer passes through, « b » then a step rewinds and stops, cancelling and an unknown step both continue. 15 tests. --- FR --- S'apercevoir à une invite qu'une étape antérieure méritait un autre choix n'avait qu'une issue : Ctrl+C. Cela laisse la progression telle quelle et oblige à retrouver l'écran de reprise pour rembobiner. « b » le fait proprement — il montre les étapes, rembobine l'état, l'écrit, et dit quoi relancer. Y renoncer ne doit pas arrêter la migration, et c'est le piège : on revient à la même invite, exactement là où l'on était. Retirer ce garde-fou fait tomber deux tests. L'avertissement COW affichait « -d DB -t odooXX.0 » pour des commandes faites pour être collées. Il connaît les deux valeurs, donc il les écrit, et propose aussi --shape puisque c'est la moitié de la réponse. Vérifié sur les quatre issues : une réponse normale passe, « b » puis une étape rembobine et arrête, annuler et un choix inconnu continuent. 15 tests. Assisted-by: Claude Opus 5
2026-08-12 01:47:44 -04:00
"Press to continue": {
"fr": "Appuyez pour continuer",
"en": "Press to continue",
},
"(b = go back to a previous step)": {
"fr": "(b = revenir à une étape précédente)",
"en": "(b = go back to a previous step)",
},
"Neutralize database, press to continue": {
"fr": "Neutraliser la base, appuyez pour continuer",
"en": "Neutralize database, press to continue",
},
"Replay from which step? (empty to cancel)": {
"fr": "Rejouer à partir de quelle étape ? (vide pour annuler)",
"en": "Replay from which step? (empty to cancel)",
},
"Rewound.": {"fr": "Rembobiné.", "en": "Rewound."},
"Relaunch the migration to resume from there.": {
"fr": "Relancez la migration pour reprendre à partir de là.",
"en": "Relaunch the migration to resume from there.",
},
[ADD] migration: offer « go back to a step » on the resume screen The screen could already replay from any step -- Enter on a row does it -- but that lived in a hint line under four named buttons. A capability you have to guess is not offered, and it was reported as missing. It now has a key, a button and a message, like the four others. The b key added earlier went to the line-by-line prompts during the run, not to this screen. Same letter on both, so going back is one key wherever you are. Two defects behind it. Replaying the chosen step actually SKIPPED it, resuming at the next one. And a replayed step did not rewind the variables it owns, so the second run started from the first one's leftovers. Checked in a simulated terminal: b focuses the table, two arrows and Enter return step 2, and the button focuses without choosing. --- FR --- L'écran savait déjà rejouer depuis n'importe quelle étape — Entrée sur une rangée le fait — mais cela vivait dans une ligne d'indication sous quatre boutons nommés. Une capacité qu'il faut deviner n'est pas offerte, et elle a été signalée manquante. Elle a maintenant une touche, un bouton et un message, comme les quatre autres. La touche b ajoutée plus tôt s'adressait aux invites ligne à ligne pendant l'exécution, pas à cet écran. Même lettre sur les deux : revenir en arrière est une seule touche, où qu'on soit. Deux défauts derrière cela. Rejouer l'étape choisie la SAUTAIT en fait, reprenant à la suivante. Et une étape rejouée ne remettait pas à zéro les variables qui lui appartiennent, si bien que la seconde exécution partait des restes de la première. Vérifié en terminal simulé : b donne le focus au tableau, deux flèches et Entrée ramènent l'étape 2, et le bouton met le focus sans choisir. Assisted-by: Claude Opus 5
2026-08-12 01:58:38 -04:00
"Go back to a step": {
"fr": "Revenir à une étape",
"en": "Go back to a step",
},
"Choose a step, Enter replays from there.": {
"fr": "Choisissez une étape, Entrée rejoue à partir de là.",
"en": "Choose a step, Enter replays from there.",
},
# --- script/analyse : vues personnalisées ---
"Customised views": {
"fr": "Vues personnalisées",
"en": "Customised views",
},
"Views": {"fr": "Vues", "en": "Views"},
"why": {"fr": "pourquoi", "en": "why"},
"more": {"fr": "de plus", "en": "more"},
"From an installed theme": {
"fr": "Posée par un thème installé",
"en": "From an installed theme",
},
"Website copy (COW)": {
"fr": "Copie de site web (COW)",
"en": "Website copy (COW)",
},
"Made with Studio": {"fr": "Faite avec Studio", "en": "Made with Studio"},
"Imported or exported": {
"fr": "Importée ou exportée",
"en": "Imported or exported",
},
"Created from the interface": {
"fr": "Créée depuis l'interface",
"en": "Created from the interface",
},
"From a module, flagged as touched": {
"fr": "D'un module, signalée comme retouchée",
"en": "From a module, flagged as touched",
},
"Straight from a module": {
"fr": "Telle quelle depuis un module",
"en": "Straight from a module",
},
"Every view comes straight from a module.": {
"fr": "Toutes les vues viennent telles quelles d'un module.",
"en": "Every view comes straight from a module.",
},
"Views that did not come straight from a module": {
"fr": "Vues qui ne viennent pas telles quelles d'un module",
"en": "Views that did not come straight from a module",
},
"Website copies are user data: Odoo copies a view instead of editing it."
" Whether they will survive the next version is another question, and"
" these tools answer it:": {
"fr": "Les copies de site web sont des données utilisateur : Odoo"
" copie une vue au lieu de la modifier. Savoir si elles survivront à"
" la prochaine version est une autre question, et ces outils y"
" répondent :",
"en": "Website copies are user data: Odoo copies a view instead of"
" editing it. Whether they will survive the next version is another"
" question, and these tools answer it:",
},
"Flags say a view was touched, not how. They are incomplete both ways: a"
" direct SQL write does not set arch_updated, and reset_arch clears it."
" Comparing with the module source is what settles it.": {
"fr": "Les drapeaux disent qu'une vue a été touchée, pas comment. Ils"
" sont incomplets dans les deux sens : un write SQL direct n'arme pas"
" arch_updated, et reset_arch l'efface. Seule la comparaison avec la"
" source du module tranche.",
"en": "Flags say a view was touched, not how. They are incomplete"
" both ways: a direct SQL write does not set arch_updated, and"
" reset_arch clears it. Comparing with the module source is what"
" settles it.",
},
"List the views of an Odoo database that did not come straight from a"
" module, website copies included (read-only).": {
"fr": "Lister les vues d'une base Odoo qui ne viennent pas telles"
" quelles d'un module, copies de site web comprises (lecture seule).",
"en": "List the views of an Odoo database that did not come straight"
" from a module, website copies included (read-only).",
},
"only show this category": {
"fr": "n'afficher que cette catégorie",
"en": "only show this category",
},
"how many views to show (default: 20)": {
"fr": "nombre de vues à afficher (défaut : 20)",
"en": "how many views to show (default: 20)",
},
"list every view": {
"fr": "afficher toutes les vues",
"en": "list every view",
},
"which views to compare (default: flagged)": {
"fr": "quelles vues comparer (défaut : celles qui sont signalées)",
"en": "which views to compare (default: flagged)",
},
"From a module, silently drifted": {
"fr": "D'un module, dérivée en silence",
"en": "From a module, silently drifted",
},
"In --scope all, a difference is a lead, not a verdict:"
" read_arch_from_file returns the raw file, while the database holds the"
" arch AFTER load-time processing. Measured on a freshly installed 18.0"
" database, 160 of its 974 views already differ this way.": {
"fr": "En --scope all, un écart est une piste, pas un verdict :"
" read_arch_from_file rend le fichier brut, alors que la base porte"
" l'arch APRÈS traitement au chargement. Mesuré sur une base 18.0"
" fraîchement installée, 160 de ses 974 vues diffèrent déjà ainsi.",
"en": "In --scope all, a difference is a lead, not a verdict:"
" read_arch_from_file returns the raw file, while the database holds"
" the arch AFTER load-time processing. Measured on a freshly installed"
" 18.0 database, 160 of its 974 views already differ this way.",
},
"Compare with the module source? (Y/n): ": {
"fr": "Comparer avec la source du module ? (O/n) : ",
"en": "Compare with the module source? (Y/n): ",
},
# --- Menu Analyse : les actions « aller plus loin ». Ce qui était
# conseillé en options de ligne de commande est devenu des entrées :
# dire « utilisez -v » à quelqu'un qui est dans un menu, c'est lui
# demander d'en sortir pour obtenir ce que le menu pouvait offrir.
"Browse the differences (TUI)": {
"fr": "🖥 Naviguer dans les écarts (plein écran)",
"en": "🖥 Browse the differences (TUI)",
},
"Compare every view (slower, noisier)": {
"fr": "🔬 Comparer toutes les vues (plus long, plus bruyant)",
"en": "🔬 Compare every view (slower, noisier)",
},
"Compare first, then browse.": {
"fr": "Comparez d'abord, vous naviguerez ensuite.",
"en": "Compare first, then browse.",
},
"Compare the flagged views with the module source": {
"fr": "🔍 Comparer les vues signalées avec la source du module",
"en": "🔍 Compare the flagged views with the module source",
},
"Count rows exactly (full scan)": {
"fr": "🔢 Compter les lignes exactement (balayage complet)",
"en": "🔢 Count rows exactly (full scan)",
},
"Counting rows exactly, one scan per table…": {
"fr": "Comptage exact des lignes, un balayage par table…",
"en": "Counting rows exactly, one scan per table…",
},
"Export as JSON": {"fr": "💾 Exporter en JSON", "en": "💾 Export as JSON"},
"Go further": {"fr": "🔎 Aller plus loin", "en": "🔎 Go further"},
"Loading the Odoo registry, this takes a moment…": {
"fr": "Chargement du registre Odoo, cela prend un moment…",
"en": "Loading the Odoo registry, this takes a moment…",
},
"No view differs from its module source.": {
"fr": "Aucune vue ne diffère de la source de son module.",
"en": "No view differs from its module source.",
},
"Show every table": {
"fr": "📜 Afficher toutes les tables",
"en": "📜 Show every table",
},
"Show every view": {
"fr": "📜 Afficher toutes les vues",
"en": "📜 Show every view",
},
"Written to: ": {"fr": "Écrit dans : ", "en": "Written to: "},
"Flags say a view was touched, not how: only comparing with the module"
" source settles it.": {
"fr": "Les drapeaux disent qu'une vue a été touchée, pas comment :"
" seule la comparaison avec la source du module tranche.",
"en": "Flags say a view was touched, not how: only comparing with the"
" module source settles it.",
},
"blocking": {"fr": "bloquants", "en": "blocking"},
"Studio and hand-made x_ fields": {
"fr": "🧩 Champs x_ (Studio et faits à la main)",
"en": "🧩 Studio and hand-made x_ fields",
},
"Show every field": {
"fr": "📜 Afficher tous les champs",
"en": "📜 Show every field",
},
"A database": {"fr": "💾 Une base de données", "en": "💾 A database"},
"A backup .zip, without restoring it": {
"fr": "🗜 Une sauvegarde .zip, sans la restaurer",
"en": "🗜 A backup .zip, without restoring it",
},
"Path to the backup .zip (empty to cancel): ": {
"fr": "Chemin de la sauvegarde .zip (vide pour annuler) : ",
"en": "Path to the backup .zip (empty to cancel): ",
},
"No such file: ": {"fr": "Fichier introuvable : ", "en": "No such file: "},
"from a backup": {"fr": "depuis une sauvegarde", "en": "from a backup"},
"Odoo backup .zip to inspect, without restoring it": {
"fr": "sauvegarde Odoo .zip à examiner, sans la restaurer",
"en": "Odoo backup .zip to inspect, without restoring it",
},
"Not an Odoo backup (no manifest.json): ": {
"fr": "Pas une sauvegarde Odoo (aucun manifest.json) : ",
"en": "Not an Odoo backup (no manifest.json): ",
},
"Cannot read the backup: ": {
"fr": "Lecture impossible de la sauvegarde : ",
"en": "Cannot read the backup: ",
},
"This backup holds no dump.sql: ": {
"fr": "Cette sauvegarde ne contient aucun dump.sql : ",
"en": "This backup holds no dump.sql: ",
},
# --- script/analyse : champs et modèles hors module ---
"Nothing declares these in a file, so no module will "
"recreate them. What a version upgrade keeps is what "
"someone carried over.": {
"fr": "Rien ne les déclare dans un fichier, donc aucun module "
"ne les recréera. Ce qu'une montée de version conserve "
"est ce que quelqu'un a reporté.",
"en": "Nothing declares these in a file, so no module will "
"recreate them. What a version upgrade keeps is what "
"someone carried over.",
},
"A stored field without its column stops the registry "
"from loading, so the upgrade will not even start. Settle "
"these before anything else.": {
"fr": "Un champ stocké sans sa colonne empêche le registre de "
"charger, donc la montée de version ne démarrera même "
"pas. Réglez cela avant tout le reste.",
"en": "A stored field without its column stops the registry "
"from loading, so the upgrade will not even start. Settle "
"these before anything else.",
},
"Use -v to list them all, --json for the raw data.": {
"fr": "Utilisez -v pour tout afficher, --json pour la donnée "
"brute.",
"en": "Use -v to list them all, --json for the raw data.",
},
"List the fields and models added outside a module — "
"Studio or by hand (read-only).": {
"fr": "Lister les champs et modèles ajoutés hors module — "
"Studio ou faits à la main (lecture seule).",
"en": "List the fields and models added outside a module — "
"Studio or by hand (read-only).",
},
"how many to show (default: 30)": {
"fr": "nombre à afficher (défaut : 30)",
"en": "how many to show (default: 30)",
},
"list every one": {"fr": "tout afficher", "en": "list every one"},
"Studio": {"fr": "Studio", "en": "Studio"},
"Made by hand": {"fr": "Fait à la main", "en": "Made by hand"},
"Declared by a module": {
"fr": "Déclaré par un module",
"en": "Declared by a module",
},
"stored, but its column is missing": {
"fr": "stocké, mais sa colonne manque",
"en": "stored, but its column is missing",
},
"points at a model that no longer exists": {
"fr": "pointe vers un modèle qui n'existe plus",
"en": "points at a model that no longer exists",
},
"its model no longer exists": {
"fr": "son modèle n'existe plus",
"en": "its model no longer exists",
},
"its table could not be resolved": {
"fr": "sa table n'a pas pu être résolue",
"en": "its table could not be resolved",
},
"origin": {"fr": "provenance", "en": "origin"},
"Fields added outside a module": {
"fr": "Champs ajoutés hors module",
"en": "Fields added outside a module",
},
"Custom fields": {"fr": "Champs personnalisés", "en": "Custom fields"},
"Custom models": {"fr": "Modèles personnalisés", "en": "Custom models"},
"No field or model was added outside a module.": {
"fr": "Aucun champ ni modèle n'a été ajouté hors module.",
"en": "No field or model was added outside a module.",
},
"Blocking": {"fr": "Bloquant", "en": "Blocking"},
"To carry over by hand": {
"fr": "À reporter à la main",
"en": "To carry over by hand",
},
# --- script/analyse : comparaison et navigation des écarts ---
"Differences only": {
"fr": "Écarts seuls",
"en": "Differences only",
},
"Ignore indentation": {
"fr": "Ignorer l'indentation",
"en": "Ignore indentation",
},
"Copy": {
"fr": "Copier",
"en": "Copy",
},
"Reset command": {
"fr": "Commande de réinitialisation",
"en": "Reset command",
},
"module (file)": {
"fr": "module (fichier)",
"en": "module (file)",
},
"database": {
"fr": "base",
"en": "database",
},
"Difference copied.": {
"fr": "Écart copié.",
"en": "Difference copied.",
},
"No Odoo configuration file found.": {
"fr": "Aucun fichier de configuration Odoo trouvé.",
"en": "No Odoo configuration file found.",
},
"odoo_bin.sh not found.": {
"fr": "odoo_bin.sh est introuvable.",
"en": "odoo_bin.sh not found.",
},
"The Odoo shell returned no result: ": {
"fr": "Le shell Odoo n'a rien renvoyé : ",
"en": "The Odoo shell returned no result: ",
},
"The Odoo shell exceeded the timeout (s): ": {
"fr": "Le shell Odoo a dépassé le délai imparti (s) : ",
"en": "The Odoo shell exceeded the timeout (s): ",
},
"Unreadable JSON from the Odoo shell: ": {
"fr": "JSON illisible en sortie du shell Odoo : ",
"en": "Unreadable JSON from the Odoo shell: ",
},
"To restore a view to what its module declares — this "
"WRITES to the database, so read the difference first:": {
"fr": "Pour rendre à une vue ce que déclare son module — ceci "
"ÉCRIT dans la base, lisez donc l'écart d'abord :",
"en": "To restore a view to what its module declares — this "
"WRITES to the database, so read the difference first:",
},
"compare with the module source (opens an Odoo shell)": {
"fr": "comparer avec la source du module (ouvre un shell Odoo)",
"en": "compare with the module source (opens an Odoo shell)",
},
"fail if the comparison could not be made": {
"fr": "échouer si la comparaison n'a pas pu être faite",
"en": "fail if the comparison could not be made",
},
"browse the differences in a full-screen view": {
"fr": "naviguer dans les écarts en plein écran",
"en": "browse the differences in a full-screen view",
},
"Flags say a view was touched, not how. They are incomplete "
"both ways: a direct SQL write does not set arch_updated, "
"and reset_arch clears it. Comparing with the module source "
"is what settles it — add --diff.": {
"fr": "Les drapeaux disent qu'une vue a été touchée, pas comment. "
"Ils sont incomplets dans les deux sens : un write SQL "
"direct n'arme pas arch_updated, et reset_arch l'efface. "
"Seule la comparaison avec la source du module tranche — "
"ajoutez --diff.",
"en": "Flags say a view was touched, not how. They are incomplete "
"both ways: a direct SQL write does not set arch_updated, "
"and reset_arch clears it. Comparing with the module source "
"is what settles it — add --diff.",
},
"Database is Odoo": {
"fr": "La base est en Odoo",
"en": "Database is Odoo",
},
"checkout is": {
"fr": "le checkout est en",
"en": "checkout is",
},
"views were flagged but hold exactly what their module "
"declares: only the comparison could tell.": {
"fr": "vues étaient signalées mais portent exactement ce que "
"déclare leur module : seule la comparaison pouvait le "
"dire.",
"en": "views were flagged but hold exactly what their module "
"declares: only the comparison could tell.",
},
"No reference arch, so nothing was compared: ": {
"fr": "Aucune arch de référence, donc rien n'a été comparé : ",
"en": "No reference arch, so nothing was compared: ",
},
"Not a terminal: showing the text report instead.": {
"fr": "Pas un terminal : affichage du rapport texte à la place.",
"en": "Not a terminal: showing the text report instead.",
},
"To restore this view to what its module declares:": {
"fr": "Pour rendre à cette vue ce que déclare son module :",
"en": "To restore this view to what its module declares:",
},
# --- script/analyse : erreurs du socle ---
"Invalid database name: ": {
"fr": "Nom de base invalide : ",
"en": "Invalid database name: ",
},
"Cannot read from the database: ": {
"fr": "Lecture impossible dans la base : ",
"en": "Cannot read from the database: ",
},
"is not an Odoo database.": {
"fr": "n'est pas une base Odoo.",
"en": "is not an Odoo database.",
},
"psql is not installed or not in PATH.": {
"fr": "psql n'est pas installé, ou absent du PATH.",
"en": "psql is not installed or not in PATH.",
},
"Query exceeded the timeout (s): ": {
"fr": "La requête a dépassé le délai imparti (s) : ",
"en": "Query exceeded the timeout (s): ",
},
"Unreadable JSON from psql: ": {
"fr": "JSON illisible en sortie de psql : ",
"en": "Unreadable JSON from psql: ",
},
"shared library, nothing to run here.": {
"fr": "bibliothèque partagée, rien à lancer ici.",
"en": "shared library, nothing to run here.",
},
"Runnable tools in this directory:": {
"fr": "Outils exécutables de ce répertoire :",
"en": "Runnable tools in this directory:",
},
"No analysis tool here yet.": {
"fr": "Aucun outil d'analyse ici pour l'instant.",
"en": "No analysis tool here yet.",
},
"From the menu:": {"fr": "Depuis le menu :", "en": "From the menu:"},
"in the dump": {"fr": "dans le dump", "en": "in the dump"},
"Weight in the dump": {
"fr": "Poids dans le dump",
"en": "Weight in the dump",
},
# --- script/analyse : poids du schéma ---
"Schema analysis": {
"fr": "Analyse du schéma",
"en": "Schema analysis",
},
"Database size": {
"fr": "Poids de la base",
"en": "Database size",
},
"Tables": {"fr": "Tables", "en": "Tables"},
"Models": {"fr": "Modèles", "en": "Models"},
"rows": {"fr": "lignes", "en": "rows"},
"Models without table": {
"fr": "Modèles sans table",
"en": "Models without table",
},
"abstract models have none, by design": {
"fr": "les modèles abstraits n'en ont pas, par construction",
"en": "abstract models have none, by design",
},
"ir_model_relation is absent, so m2m tables cannot be told apart from"
" orphans: the list below is unreliable.": {
"fr": "ir_model_relation est absente : impossible de distinguer une"
" table m2m d'une orpheline, la liste ci-dessous n'est pas fiable.",
"en": "ir_model_relation is absent, so m2m tables cannot be told apart"
" from orphans: the list below is unreliable.",
},
"Heaviest tables": {
"fr": "Tables les plus lourdes",
"en": "Heaviest tables",
},
"All tables, heaviest first": {
"fr": "Toutes les tables, de la plus lourde à la plus légère",
"en": "All tables, heaviest first",
},
"use -v to list them all": {
"fr": "utilisez -v pour toutes les afficher",
"en": "use -v to list them all",
},
"Every table belongs to an installed model.": {
"fr": "Chaque table appartient à un modèle installé.",
"en": "Every table belongs to an installed model.",
},
"Orphan tables": {
"fr": "Tables orphelines",
"en": "Orphan tables",
},
"No installed model claims these tables. They are usually left over from"
" modules uninstalled without DROP TABLE, and every version upgrade"
" carries them along.": {
"fr": "Aucun modèle installé ne réclame ces tables. Ce sont le plus"
" souvent des reliquats de modules désinstallés sans DROP TABLE, que"
" chaque montée de version transporte avec elle.",
"en": "No installed model claims these tables. They are usually left"
" over from modules uninstalled without DROP TABLE, and every version"
" upgrade carries them along.",
},
"Check what they hold before dropping anything.": {
"fr": "Regardez ce qu'elles contiennent avant de supprimer quoi que ce"
" soit.",
"en": "Check what they hold before dropping anything.",
},
"Row counts are estimates from the last ANALYZE. Use --exact for real"
" counts, at the cost of one full scan per table.": {
"fr": "Les nombres de lignes sont estimés d'après le dernier ANALYZE."
" Utilisez --exact pour un comptage réel, au prix d'un balayage"
" complet par table.",
"en": "Row counts are estimates from the last ANALYZE. Use --exact for"
" real counts, at the cost of one full scan per table.",
},
"Report the size of an Odoo database and the tables no installed model"
" claims (read-only).": {
"fr": "Rapporter le poids d'une base Odoo et les tables qu'aucun"
" modèle installé ne réclame (lecture seule).",
"en": "Report the size of an Odoo database and the tables no installed"
" model claims (read-only).",
},
"database to inspect": {
"fr": "base à examiner",
"en": "database to inspect",
},
"count rows exactly: one full scan per table": {
"fr": "compter les lignes exactement : un balayage complet par table",
"en": "count rows exactly: one full scan per table",
},
"how many tables to show (default: 20)": {
"fr": "nombre de tables à afficher (défaut : 20)",
"en": "how many tables to show (default: 20)",
},
"list every table": {
"fr": "afficher toutes les tables",
"en": "list every table",
},
"output JSON": {"fr": "sortie JSON", "en": "output JSON"},
"path to an Odoo config file": {
"fr": "chemin d'un fichier de configuration Odoo",
"en": "path to an Odoo config file",
},
# Courriel
"mail_menu": {
"fr": "Courriel - Lire et envoyer du courriel",
"en": "Mail - Read and send email",
},
"mail_ai_question": {
"fr": "Question IA - Poser une question à un modèle",
"en": "AI question - Ask a model a question",
},
"mail_open_tui": {
"fr": "Ouvrir le client courriel (TUI)",
"en": "Open the mail client (TUI)",
},
"mail_accounts_menu": {"fr": "Comptes", "en": "Accounts"},
"mail_sync_now": {
"fr": "Synchroniser maintenant",
"en": "Synchronise now",
},
"mail_cache_menu": {"fr": "Cache", "en": "Cache"},
"mail_account_list": {"fr": "Lister les comptes", "en": "List accounts"},
"mail_account_add": {"fr": "Ajouter un compte", "en": "Add an account"},
"mail_account_delete": {
"fr": "Supprimer un compte",
"en": "Delete an account",
},
"mail_account_template": {
"fr": "Générer un modèle accounts.json",
"en": "Generate an accounts.json template",
},
"mail_account_test": {
"fr": "Tester la connexion d'un compte",
"en": "Test an account connection",
},
"mail_cache_default_mode": {
"fr": "Mode de cache par défaut",
"en": "Default cache mode",
},
"mail_cache_account_mode": {
"fr": "Mode de cache d'un compte",
"en": "Cache mode of one account",
},
"mail_cache_size_purge": {
"fr": "Taille du cache et purge",
"en": "Cache size and purge",
},
"mail_no_account": {
"fr": "Aucun compte configuré. Ajoutez-en un d'abord.",
"en": "No account configured. Add one first.",
},
"mail_ask_name": {
"fr": "Nom court du compte : ",
"en": "Short account name: ",
},
"mail_ask_email": {"fr": "Adresse courriel : ", "en": "Email address: "},
"mail_ask_display_name": {
"fr": "Nom affiché (facultatif) : ",
"en": "Display name (optional): ",
},
"mail_ask_preset": {"fr": "Fournisseur : ", "en": "Provider: "},
"mail_ask_password": {"fr": "Mot de passe : ", "en": "Password: "},
"mail_ask_imap_host": {"fr": "Serveur IMAP : ", "en": "IMAP server: "},
"mail_ask_smtp_host": {"fr": "Serveur SMTP : ", "en": "SMTP server: "},
"mail_ask_account": {"fr": "Quel compte ? ", "en": "Which account? "},
"mail_ask_mode": {
"fr": "Mode (clear / encrypted / ephemeral) : ",
"en": "Mode (clear / encrypted / ephemeral): ",
},
"mail_app_password_note": {
"fr": "Ce fournisseur exige un mot de passe d'application.",
"en": "This provider requires an app password.",
},
"mail_account_saved": {"fr": "Compte enregistré.", "en": "Account saved."},
"mail_account_save": {"fr": "Enregistrer", "en": "Save"},
"mail_account_missing_fields": {
"fr": "Nom, adresse et mot de passe sont requis.",
"en": "Name, address and password are required.",
},
"mail_account_add_unavailable": {
"fr": "Ajout de compte indisponible dans ce contexte.",
"en": "Adding an account is unavailable in this context.",
},
"mail_account_deleted": {
"fr": "Compte supprimé.",
"en": "Account deleted.",
},
"mail_connection_ok": {
"fr": "Connexion réussie.",
"en": "Connection succeeded.",
},
"mail_connection_failed": {
"fr": "Connexion échouée :",
"en": "Connection failed:",
},
"mail_template_written": {
"fr": "Modèle écrit dans",
"en": "Template written to",
},
"mail_purge_confirm": {
"fr": "Effacer tout le cache de ce compte ? (o/N) ",
"en": "Erase this account's whole cache? (y/N) ",
},
"mail_purged": {"fr": "Cache effacé.", "en": "Cache erased."},
"mail_no_vault": {
"fr": "Aucun coffre disponible : installez pykeepass ou déverrouillez un trousseau système.",
"en": "No vault available: install pykeepass or unlock a system keyring.",
},
# Coffre KeePass, côté infrastructure partagée (`kdbx_manager`) : ces
# messages servent à TOUT le CLI, pas seulement au courriel.
"kdbx_vault_is": {
"fr": "Coffre KeePass :",
"en": "KeePass vault:",
},
"kdbx_ask_password": {
"fr": "Mot de passe du coffre (vide pour abandonner) : ",
"en": "Vault password (empty to give up): ",
},
"kdbx_wrong_password": {
"fr": "Mot de passe incorrect pour ce coffre KeePass.",
"en": "Wrong password for this KeePass vault.",
},
"kdbx_give_up": {
"fr": "Coffre non ouvert : on abandonne.",
"en": "Vault not opened: giving up.",
},
"mail_kdbx_none_configured": {
"fr": "Aucun fichier kdbx n'est configuré.",
"en": "No kdbx file is configured.",
},
"mail_kdbx_menu_create": {
"fr": "Créer un nouveau fichier .kdbx",
"en": "Create a new .kdbx file",
},
"mail_kdbx_menu_choose": {
"fr": "Choisir un fichier existant",
"en": "Choose an existing file",
},
"mail_kdbx_menu_cancel": {"fr": "Annuler", "en": "Cancel"},
"mail_kdbx_ask_choice": {"fr": "Votre choix : ", "en": "Your choice: "},
"mail_kdbx_ask_path_new": {
"fr": "Chemin du nouveau fichier kdbx",
"en": "Path for the new kdbx file",
},
"mail_kdbx_ask_path_existing": {
"fr": "Chemin du fichier kdbx existant : ",
"en": "Path to the existing kdbx file: ",
},
"mail_kdbx_ask_password": {
"fr": "Mot de passe du coffre : ",
"en": "Vault password: ",
},
"mail_kdbx_ask_password_confirm": {
"fr": "Confirmez le mot de passe : ",
"en": "Confirm the password: ",
},
"mail_kdbx_password_mismatch": {
"fr": "Les mots de passe ne correspondent pas.",
"en": "Passwords do not match.",
},
"mail_kdbx_path_not_found": {
"fr": "Ce fichier n'existe pas :",
"en": "This file does not exist:",
},
"mail_kdbx_created": {
"fr": "Fichier kdbx créé :",
"en": "Kdbx file created:",
},
"mail_kdbx_path_recorded": {
"fr": "Fichier kdbx configuré :",
"en": "Kdbx file configured:",
},
"mail_no_password_stored": {
"fr": "Aucun mot de passe enregistré pour ce compte.",
"en": "No password stored for this account.",
},
"mail_install_textual": {
"fr": "Installez textual pour le client courriel (pip).",
"en": "Install textual for the mail client (pip).",
},
"mail_accounts": {"fr": "Comptes", "en": "Accounts"},
"mail_search": {"fr": "Rechercher…", "en": "Search…"},
"mail_search_clear": {
"fr": "Effacer la recherche",
"en": "Clear search",
},
"mail_from": {"fr": "De :", "en": "From:"},
"mail_to": {"fr": "À :", "en": "To:"},
"mail_cc": {"fr": "Cc :", "en": "Cc:"},
"mail_subject": {"fr": "Objet :", "en": "Subject:"},
"mail_date": {"fr": "Date", "en": "Date"},
"mail_send": {"fr": "Envoyer", "en": "Send"},
"mail_attachments": {"fr": "Pièces jointes :", "en": "Attachments:"},
"mail_attachments_paths": {
"fr": "Pièces jointes (chemins séparés par des points-virgules)",
"en": "Attachments (semicolon-separated paths)",
},
"mail_browse": {"fr": "Parcourir…", "en": "Browse…"},
"mail_browse_failed": {
"fr": "Sélecteur de fichiers impossible :",
"en": "File browser unavailable:",
},
"mail_no_subject": {"fr": "(sans objet)", "en": "(no subject)"},
"mail_body_needs_network": {
"fr": "Corps non téléchargé — connexion requise.",
"en": "Body not downloaded — connection required.",
},
"mail_body_error": {
"fr": "Lecture du corps impossible :",
"en": "Cannot read the body:",
},
"mail_flag_error": {
"fr": "Drapeau non transmis au serveur :",
"en": "Flag not sent to the server:",
},
"mail_syncing": {"fr": "Synchronisation de", "en": "Synchronising"},
"mail_new_messages": {"fr": "nouveaux messages", "en": "new messages"},
"mail_errors": {"fr": "erreurs", "en": "errors"},
"mail_folders_resynced": {
"fr": "dossiers resynchronisés (UIDVALIDITY changé) :",
"en": "folders resynchronised (UIDVALIDITY changed):",
},
"mail_offline_cannot_send": {
"fr": "Compte hors ligne : envoi impossible.",
"en": "Account offline: cannot send.",
},
"mail_sent_to": {"fr": "Envoyé à", "en": "Sent to"},
"mail_sent_not_filed": {
"fr": "envoyé, mais pas classé dans Envoyés",
"en": "sent, but not filed in Sent",
},
"mail_nothing_to_reply_to": {
"fr": "Aucun message sélectionné.",
"en": "No message selected.",
},
"mail_nothing_to_forward": {
"fr": "Aucun message à transférer.",
"en": "No message to forward.",
},
"mail_attachment_not_found": {
"fr": "Pièce jointe introuvable.",
"en": "Attachment not found.",
},
"mail_no_attachment": {
"fr": "Ce message n'a pas de pièce jointe.",
"en": "This message has no attachment.",
},
"mail_save_failed": {
"fr": "Enregistrement impossible :",
"en": "Cannot save:",
},
"mail_saved_to": {"fr": "Enregistré dans", "en": "Saved to"},
"mail_log_binding": {"fr": "Journal", "en": "Log"},
"mail_log_close": {"fr": "Fermer", "en": "Close"},
"mail_log_tail_heading": {
"fr": "Journal (fin) :",
"en": "Log (tail):",
},
"mail_log_errors_heading": {
"fr": "Erreurs de synchronisation (session en cours) :",
"en": "Sync errors (current session):",
},
"mail_log_missing": {
"fr": "journal introuvable",
"en": "log not found",
},
"mail_log_empty": {"fr": "journal vide", "en": "log empty"},
"mail_log_unreadable": {
"fr": "journal illisible",
"en": "log unreadable",
},
"mail_log_no_errors": {
"fr": "Aucune erreur de synchronisation dans cette session.",
"en": "No sync errors in this session.",
},
"mail_layout_binding": {"fr": "Vue", "en": "View"},
"mail_layout_switched": {"fr": "Disposition :", "en": "Layout:"},
"mail_layout_columns": {"fr": "Colonnes", "en": "Columns"},
"mail_layout_split": {"fr": "Partagée", "en": "Split"},
"mail_layout_stacked": {"fr": "Empilée", "en": "Stacked"},
"mail_pane_grow_binding": {"fr": "Agrandir volet", "en": "Grow pane"},
"mail_pane_shrink_binding": {
"fr": "Rétrécir volet",
"en": "Shrink pane",
},
"mail_pane_reset_binding": {
"fr": "Tailles par défaut",
"en": "Reset sizes",
},
"mail_pane_reset_done": {
"fr": "Tailles des volets réinitialisées.",
"en": "Pane sizes reset.",
},
"mail_pane_splitter_tooltip": {
"fr": "Glisser pour redimensionner",
"en": "Drag to resize",
},
"mail_fullscreen_binding": {"fr": "Plein écran", "en": "Full screen"},
# -- Libellés des raccourcis de MailApp (suffixe _binding) -------------
# Ce sont les descriptions des `Binding` de `MailApp` : elles s'affichent
# au pied d'écran ET, depuis la tâche 26, dans la fenêtre d'aide (`h`),
# qui les lit directement dans `MailApp.BINDINGS`. Le français est repris
# MOT POUR MOT de ce qui était écrit en dur avant cette tâche — le pied
# d'écran d'un utilisateur francophone ne change pas.
"mail_quit_binding": {"fr": "Quitter", "en": "Quit"},
"mail_sync_current_binding": {"fr": "Sync", "en": "Sync"},
"mail_sync_all_binding": {"fr": "Sync tout", "en": "Sync all"},
"mail_back_binding": {"fr": "Retour", "en": "Back"},
"mail_search_binding": {"fr": "Rechercher", "en": "Search"},
"mail_mark_seen_binding": {"fr": "Lu", "en": "Read"},
"mail_mark_unseen_binding": {"fr": "Non lu", "en": "Unread"},
"mail_save_attachment_binding": {
"fr": "Enregistrer PJ",
"en": "Save attachment",
},
"mail_compose_binding": {"fr": "Écrire", "en": "Compose"},
"mail_reply_binding": {"fr": "Répondre", "en": "Reply"},
"mail_reply_all_binding": {"fr": "Répondre à tous", "en": "Reply all"},
"mail_forward_binding": {"fr": "Transférer", "en": "Forward"},
"mail_add_account_binding": {
"fr": "Nouveau compte",
"en": "New account",
},
# -- Fenêtre d'aide (touche h) -----------------------------------------
# La liste des touches n'est PAS ici : elle est engendrée depuis
# `MailApp.BINDINGS` (voir `HelpScreen`), avec les libellés ci-dessus.
# Seul ce qu'une liste de touches ne peut pas dire est rédigé ici.
"mail_help_binding": {"fr": "Aide", "en": "Help"},
"mail_help_close": {"fr": "Fermer", "en": "Close"},
"mail_help_title": {
"fr": "Aide — client courriel",
"en": "Help — mail client",
},
"mail_help_keys_heading": {
"fr": "Raccourcis clavier :",
"en": "Keyboard shortcuts:",
},
"mail_help_notes_heading": {"fr": "Bon à savoir :", "en": "Good to know:"},
"mail_help_mouse": {
"fr": (
"Souris : glisser une barre entre deux volets les redimensionne."
" Au clavier, + et - font de même sur le volet qui a le focus, et"
" 0 remet les tailles par défaut. Les tailles sont retenues par"
" disposition."
),
"en": (
"Mouse: drag a bar between two panes to resize them. From the"
" keyboard, + and - do the same to the focused pane, and 0 resets"
" the sizes. Sizes are remembered per layout."
),
},
"mail_help_layouts": {
"fr": (
"Dispositions : v passe de colonnes à partagée, puis empilée, puis"
" revient à colonnes."
),
"en": (
"Layouts: v cycles columns, split, stacked, then back to columns."
),
},
"mail_help_sync": {
"fr": (
"Synchronisation : r synchronise le compte du dossier sélectionné"
" (tous ses dossiers), R synchronise tous les comptes. La"
" synchronisation automatique ne tourne QUE tant que le client est"
" ouvert, à l'intervalle mail_refresh_sec (300 s par défaut ; 0 la"
" désactive)."
),
"en": (
"Sync: r syncs the account of the selected folder (all its"
" folders), R syncs every account. The automatic refresh runs ONLY"
" while the client is open, at the mail_refresh_sec interval (300 s"
" by default; 0 disables it)."
),
},
"mail_help_files": {
"fr": (
"Fichiers : le journal est dans ~/.erplibre/mail.log (la touche l"
" en montre la fin), les comptes dans"
" ~/.erplibre/mail/accounts.json. Les mots de passe n'y sont JAMAIS"
" écrits : ils vivent dans le coffre kdbx ou le trousseau du"
" système."
),
"en": (
"Files: the log lives in ~/.erplibre/mail.log (the l key shows its"
" tail), the accounts in ~/.erplibre/mail/accounts.json. Passwords"
" are NEVER written there: they live in the kdbx vault or in the"
" system keyring."
),
},
"mail_help_close_hint": {
"fr": "Échap ferme cette fenêtre.",
"en": "Esc closes this window.",
},
# -- Exceptions internes au paquet courriel (préfixe mail_err_) --------
# Label traduit, données dynamiques (chemins, texte serveur, valeurs de
# config) concaténées crues : jamais de traduction d'un message serveur
# ou d'un chemin de fichier.
#
# Quelques messages ont la donnée dynamique AU MILIEU de la phrase : ils
# sont donc assemblés à partir de deux clés (voire trois), dans un ordre
# FIXE codé au site d'appel plutôt que par une seule clé avec un
# emplacement — mail_err_unknown_security + mail_err_expected,
# mail_err_key_wrong_length + mail_err_octets_unit,
# mail_err_mode_prefix + mail_err_mode_requires_key(_no_vault),
# mail_err_imap_connection_prefix / mail_err_smtp_connection_prefix +
# mail_err_connection_refused_suffix, et mail_err_keyring_plaintext +
# mail_err_keyring_plaintext_hint. L'ordre des mots vit donc dans le
# code, pas dans les chaînes traduisibles : une langue à l'ordre des
# mots différent devra remplacer ces paires par une convention à
# emplacement (ex. `.format()`), pas par une simple concaténation.
"mail_err_envelope_too_short": {
"fr": "enveloppe trop courte",
"en": "envelope too short",
},
"mail_err_sealed_in_clear_mode": {
"fr": "donnée chiffrée lue en mode clair : la clé du compte manque",
"en": "encrypted data read in clear mode: the account key is missing",
},
"mail_err_unknown_envelope": {
"fr": "enveloppe inconnue :",
"en": "unknown envelope:",
},
"mail_err_key_wrong_length": {
"fr": "la clé doit faire",
"en": "the key must be",
},
"mail_err_octets_unit": {"fr": "octets", "en": "bytes"},
"mail_err_cryptography_not_installed": {
"fr": "le paquet cryptography n'est pas installé",
"en": "the cryptography package is not installed",
},
"mail_err_decrypt_refused": {
"fr": "déchiffrement refusé : clé fausse ou donnée altérée",
"en": "decryption refused: wrong key or corrupted data",
},
"mail_err_envelope_unreadable": {
"fr": "enveloppe illisible :",
"en": "unreadable envelope:",
},
"mail_err_mode_prefix": {"fr": "le mode", "en": "mode"},
"mail_err_mode_requires_key": {
"fr": "exige une clé",
"en": "requires a key",
},
"mail_err_unknown_cache_mode": {
"fr": "mode de cache inconnu :",
"en": "unknown cache mode:",
},
"mail_err_file_already_exists": {
"fr": "le fichier existe déjà :",
"en": "the file already exists:",
},
"mail_err_invalid_secret_ref": {
"fr": "référence de secret invalide :",
"en": "invalid secret reference:",
},
# Notes par fournisseur. Affichées à l'ajout d'un compte ET juste avant
# de redemander un mot de passe après un refus : elles doivent donner
# l'adresse EXACTE, pas un chemin de menu — Google et Apple déplacent
# régulièrement ces pages, et Google cache la sienne.
"mail_preset_note_gmail": {
"fr": (
"Générez-le sur https://myaccount.google.com/apppasswords"
" (16 caractères, les espaces sont acceptés). La validation en"
" deux étapes doit être active, sinon la page est vide."
),
"en": (
"Generate one at https://myaccount.google.com/apppasswords"
" (16 characters, spaces are accepted). Two-step verification"
" must be on, otherwise the page is empty."
),
},
"mail_preset_note_outlook": {
"fr": (
"Générez-le sur https://account.microsoft.com/security."
" Microsoft ferme l'authentification simple sur les comptes"
" grand public : sans mot de passe d'application, il faudra"
" OAuth (phase 2, non implémentée)."
),
"en": (
"Generate one at https://account.microsoft.com/security."
" Microsoft is closing basic authentication on consumer"
" accounts: without an app password this needs OAuth (phase 2,"
" not implemented)."
),
},
"mail_preset_note_icloud": {
"fr": (
"Générez-le sur https://account.apple.com, section « Connexion"
" et sécurité ». L'authentification à deux facteurs doit être"
" active."
),
"en": (
'Generate one at https://account.apple.com, under "Sign-In and'
' Security". Two-factor authentication must be on.'
),
},
"mail_preset_note_generic": {
"fr": "Saisissez les serveurs de votre fournisseur.",
"en": "Enter your provider's servers.",
},
"mail_ask_app_password": {
"fr": "Mot de passe d'application : ",
"en": "App password: ",
},
"mail_err_password_not_ascii": {
"fr": (
"le mot de passe contient un caractère non ASCII, que ce client"
" IMAP ne sait pas transmettre — il n'a pas été envoyé au"
" serveur"
),
"en": (
"the password contains a non-ASCII character this IMAP client"
" cannot transmit — it was never sent to the server"
),
},
"mail_err_no_kdbx_configured": {
"fr": "aucun fichier kdbx configuré",
"en": "no kdbx file configured",
},
"mail_err_kdbx_unreadable": {
"fr": "le fichier kdbx n'a pas pu être ouvert",
"en": "the kdbx file could not be opened",
},
"mail_err_no_vault_available": {
"fr": "aucun coffre disponible : ni kdbx, ni trousseau système",
"en": "no vault available: neither kdbx nor system keyring",
},
"mail_err_keyring_plaintext": {
"fr": (
"le trousseau du système écrirait le mot de passe en clair"
" (backend"
),
"en": (
"the system keyring would store the password in plaintext"
" (backend"
),
},
"mail_err_keyring_plaintext_hint": {
"fr": "Utilisez un fichier kdbx, ou déverrouillez un vrai trousseau.",
"en": "Use a kdbx file, or unlock a real keyring.",
},
"mail_err_unknown_security": {
"fr": "sécurité inconnue :",
"en": "unknown security:",
},
"mail_err_expected": {"fr": "(attendu", "en": "(expected"},
"mail_err_account_needs_name": {
"fr": "un compte doit avoir un nom",
"en": "an account must have a name",
},
"mail_err_invalid_account_name": {
"fr": "nom de compte invalide :",
"en": "invalid account name:",
},
"mail_err_account_name_reason": {
"fr": "(il sert de nom de dossier et de référence de coffre)",
"en": "(it is used as a folder name and vault reference)",
},
"mail_err_account_unreadable": {
"fr": "compte illisible :",
"en": "unreadable account:",
},
"mail_err_unknown_preset": {
"fr": "préréglage inconnu :",
"en": "unknown preset:",
},
"mail_err_not_valid_json": {
"fr": "n'est pas du JSON valide :",
"en": "is not valid JSON:",
},
"mail_err_should_contain_json_object": {
"fr": "devrait contenir un objet JSON",
"en": "should contain a JSON object",
},
"mail_err_duplicate_account_names": {
"fr": "noms de compte en double :",
"en": "duplicate account names:",
},
"mail_err_already_exists_relaunch": {
"fr": "existe déjà — relancez avec l'option de remplacement",
"en": "already exists — rerun with the overwrite option",
},
"mail_err_symlink_refused": {
"fr": "est un lien symbolique : cache refusé",
"en": "is a symlink: cache refused",
},
"mail_err_owned_by_other_user": {
"fr": "appartient à un autre utilisateur : cache refusé",
"en": "belongs to another user: cache refused",
},
"mail_err_cache_unreadable": {
"fr": "cache illisible, purgez-le et resynchronisez :",
"en": "unreadable cache, purge it and resynchronise:",
},
"mail_err_mode_requires_key_no_vault": {
"fr": "exige une clé : aucun coffre fourni",
"en": "requires a key: no vault provided",
},
"mail_err_cache_not_open": {
"fr": "cache non ouvert : appelez open() d'abord",
"en": "cache not open: call open() first",
},
"mail_err_unknown_folder_fields": {
"fr": "champs de dossier inconnus :",
"en": "unknown folder fields:",
},
"mail_err_unterminated_ampersand": {
"fr": "séquence & non terminée",
"en": "unterminated & sequence",
},
"mail_err_server_replied": {
"fr": ": le serveur a répondu",
"en": ": the server replied",
},
"mail_err_no_body_for_uid": {
"fr": "aucun corps rendu pour l'UID",
"en": "no body returned for UID",
},
"mail_err_imap_connection_prefix": {
"fr": "connexion IMAP à",
"en": "IMAP connection to",
},
"mail_err_smtp_connection_prefix": {
"fr": "connexion SMTP à",
"en": "SMTP connection to",
},
"mail_err_connection_refused_suffix": {
"fr": "refusée :",
"en": "refused:",
},
"mail_err_message_needs_recipient": {
"fr": "un message doit avoir au moins un destinataire",
"en": "a message must have at least one recipient",
},
"mail_err_attachment_missing": {
"fr": "pièce jointe introuvable :",
"en": "attachment not found:",
},
"mail_err_no_recipient_nothing_sent": {
"fr": "aucun destinataire : rien n'a été envoyé",
"en": "no recipient: nothing was sent",
},
"mail_err_send_refused": {"fr": "envoi refusé :", "en": "send refused:"},
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions A graphical VM was a desktop and nothing else: every developer tool had to be installed by hand afterwards. A check list now carries them, filtered per machine — Android Studio is x86_64 only, Google publishes no Linux aarch64 build — and their disk cost reaches the plan before any qcow2 is created. They are installed BEFORE the clone: PyCharm writes the .idea/ of the repository, and the install that follows is what runs pycharm_configuration.py, through update_env_version.pycharm_update(). Its launcher is named studio, which is enough to conclude the install failed; it now answers to android-studio too. GNOME extensions come from the site by UUID, per running Shell: the same endpoint serves gTile v59 for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and the install's exit code still wins, 45 tests. --- FR --- Une VM graphique n'était qu'un bureau : chaque outil de développement restait à poser à la main. Une liste à cocher les porte, filtrés machine par machine — Android Studio n'existe qu'en x86_64, Google ne publiant aucune archive Linux aarch64 — et leur place disque atteint le plan avant qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance pycharm_configuration.py, via update_env_version.pycharm_update(). Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il répond désormais aussi à android-studio. Les extensions GNOME viennent du site par UUID, selon le Shell qui tourne : le même point d'entrée sert gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque outil peut échouer sans que le code de sortie de l'installation ne change, 45 tests. Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
# QEMU — outils de développement des VM graphiques
"Python IDE, ERPLibre project pre-configured": {
"fr": "IDE Python, projet ERPLibre pré-configuré",
"en": "Python IDE, ERPLibre project pre-configured",
},
"ERPLibre mobile development (x86_64 only)": {
"fr": "développement mobile ERPLibre (x86_64 seulement)",
"en": "ERPLibre mobile development (x86_64 only)",
},
"GNOME extensions": {
"fr": "Extensions GNOME",
"en": "GNOME extensions",
},
"suggested extensions + extension manager": {
"fr": "extensions suggérées + gestionnaire d'extensions",
"en": "suggested extensions + extension manager",
},
"Development tools for the graphical VMs:": {
"fr": "Outils de développement des VM graphiques :",
"en": "Development tools for the graphical VMs:",
},
"Development tools (graphical VMs):": {
"fr": "Outils de développement (VM graphiques) :",
"en": "Development tools (graphical VMs):",
},
"Development tools:": {
"fr": "Outils de développement :",
"en": "Development tools:",
},
"Disk needed:": {"fr": "Disque nécessaire :", "en": "Disk needed:"},
"Numbers separated by spaces, [all], blank = none:": {
"fr": "Numéros séparés par des espaces, [tous], vide = aucun :",
"en": "Numbers separated by spaces, [all], blank = none:",
},
"No graphical VM: these tools are not installed.": {
"fr": "Aucune VM graphique : ces outils ne seront pas installés.",
"en": "No graphical VM: these tools are not installed.",
},
"Partly skipped (arch or desktop):": {
"fr": "En partie écartés (architecture ou bureau) :",
"en": "Partly skipped (arch or desktop):",
},
"Installing PyCharm (long)": {
"fr": "Installation de PyCharm (long)",
"en": "Installing PyCharm (long)",
},
"no JetBrains build for": {
"fr": "aucune archive JetBrains pour",
"en": "no JetBrains build for",
},
"PyCharm: project config skipped": {
"fr": "PyCharm : configuration du projet non écrite",
"en": "PyCharm: project config skipped",
},
"PyCharm installed:": {
"fr": "PyCharm installé :",
"en": "PyCharm installed:",
},
"PyCharm not installed (see above)": {
"fr": "PyCharm non installé (voir ci-dessus)",
"en": "PyCharm not installed (see above)",
},
"Installing Android Studio (long)": {
"fr": "Installation d'Android Studio (long)",
"en": "Installing Android Studio (long)",
},
"Android Studio: Google publishes x86_64 only": {
"fr": "Android Studio : Google ne publie que du x86_64",
"en": "Android Studio: Google publishes x86_64 only",
},
"SDK shared through ANDROID_HOME:": {
"fr": "SDK partagé via ANDROID_HOME :",
"en": "SDK shared through ANDROID_HOME:",
},
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions A graphical VM was a desktop and nothing else: every developer tool had to be installed by hand afterwards. A check list now carries them, filtered per machine — Android Studio is x86_64 only, Google publishes no Linux aarch64 build — and their disk cost reaches the plan before any qcow2 is created. They are installed BEFORE the clone: PyCharm writes the .idea/ of the repository, and the install that follows is what runs pycharm_configuration.py, through update_env_version.pycharm_update(). Its launcher is named studio, which is enough to conclude the install failed; it now answers to android-studio too. GNOME extensions come from the site by UUID, per running Shell: the same endpoint serves gTile v59 for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and the install's exit code still wins, 45 tests. --- FR --- Une VM graphique n'était qu'un bureau : chaque outil de développement restait à poser à la main. Une liste à cocher les porte, filtrés machine par machine — Android Studio n'existe qu'en x86_64, Google ne publiant aucune archive Linux aarch64 — et leur place disque atteint le plan avant qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance pycharm_configuration.py, via update_env_version.pycharm_update(). Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il répond désormais aussi à android-studio. Les extensions GNOME viennent du site par UUID, selon le Shell qui tourne : le même point d'entrée sert gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque outil peut échouer sans que le code de sortie de l'installation ne change, 45 tests. Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
"Android Studio installed:": {
"fr": "Android Studio installé :",
"en": "Android Studio installed:",
},
"Android Studio not installed (see above)": {
"fr": "Android Studio non installé (voir ci-dessus)",
"en": "Android Studio not installed (see above)",
},
"no nested KVM: the emulator will not run": {
"fr": "pas de KVM imbriqué : l'émulateur ne démarrera pas",
"en": "no nested KVM: the emulator will not run",
},
"Suggested GNOME extensions": {
"fr": "Extensions GNOME suggérées",
"en": "Suggested GNOME extensions",
},
"not in the repos, skipped:": {
"fr": "absent des dépôts, ignoré :",
"en": "not in the repos, skipped:",
},
"Enable them from Extension Manager, or:": {
"fr": "À activer depuis le gestionnaire d'extensions, ou :",
"en": "Enable them from Extension Manager, or:",
},
"GNOME Shell not found, site extensions skipped": {
"fr": "GNOME Shell introuvable, extensions du site ignorées",
"en": "GNOME Shell not found, site extensions skipped",
},
"installed and enabled:": {
"fr": "installée et activée :",
"en": "installed and enabled:",
},
"not available for this GNOME, skipped:": {
"fr": "indisponible pour ce GNOME, ignorée :",
"en": "not available for this GNOME, skipped:",
},
"log out and back in to load them": {
"fr": "se déconnecter puis se reconnecter pour les charger",
"en": "log out and back in to load them",
},
"open the project once and close PyCharm; the .idea it writes is what the install configures": {
"fr": "ouvrir le projet une fois puis fermer PyCharm : le .idea qu'il écrit est ce que l'installation configure",
"en": "open the project once and close PyCharm; the .idea it writes is what the install configures",
},
"command": {"fr": "commande", "en": "command"},
[FIX] todo qemu: open the PyCharm project without a screen, and unlicensed Two measurements, one after the other. The unified PyCharm that code=PCC&latest now serves stops on its licence: the log says NoValidIdeLicense then "Get licenses: request requires authentication", and no project is ever opened — so no .idea, so nothing for the install to configure. The Community line asks for no account and is still patched (2025.2.6.2 on 2026-07-29); it is resolved from the release feed, so no version is frozen here. That build then froze 1.3 s after startup: the trust dialog, invisible under Xvfb and waiting for a click. idea.trust.all.projects unblocks it. Checked on an Ubuntu 26.04 VM: .idea complete in 195 s, and pycharm_configuration.py writes its exclusions into erplibre.iml. --- FR --- Deux mesures, l'une après l'autre. Le PyCharm unifié que sert désormais code=PCC&latest s'arrête sur sa licence : le journal dit NoValidIdeLicense puis « Get licenses: request requires authentication », et aucun projet ne s'ouvre — donc pas de .idea, donc rien à configurer pour l'installation. La ligne Community ne demande aucun compte et reste corrigée (2025.2.6.2 le 2026-07-29) ; elle est résolue depuis le flux des versions, sans qu'aucun numéro ne soit figé ici. Ce build se figeait ensuite 1,3 s après le démarrage : la fenêtre de confiance, invisible sous Xvfb et attendant un clic. idea.trust.all.projects la lève. Vérifié sur une VM Ubuntu 26.04 : .idea complet en 195 s, et pycharm_configuration.py écrit ses exclusions dans erplibre.iml. Assisted-by: Claude Opus 5
2026-08-17 23:22:38 -04:00
"release feed unreachable: unified build, it will ask for a JetBrains account": {
"fr": "flux des versions injoignable : build unifié, il demandera un compte JetBrains",
"en": "release feed unreachable: unified build, it will ask for a JetBrains account",
},
"Creating the PyCharm project (first open)": {
"fr": "Création du projet PyCharm (première ouverture)",
"en": "Creating the PyCharm project (first open)",
},
"PyCharm missing, step skipped": {
"fr": "PyCharm absent, étape sautée",
"en": "PyCharm missing, step skipped",
},
"no Xvfb here, open PyCharm by hand": {
"fr": "pas de Xvfb ici : ouvrir PyCharm à la main",
"en": "no Xvfb here, open PyCharm by hand",
},
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks A VM was declared ready without anything proving it. The mobile option now adds the repository to the manifest (additive, so it rides along an Odoo 18 install), runs the mobile repository's own install-android.sh — licences accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug and npm test. A failure fails the VM: the exit code reaches the dashboard. Two gaps in that upstream installer had to be filled: unzip and wget, which no cloud image ships, and the SDK platform — it installs android-34 while variables.gradle asks for compileSdk 36, so the number is read from the file rather than frozen. Gradle writes tens of megabytes and hundreds of harmless lines carrying the word error: that output goes to a file of its own, and the log gets the named cause instead. --- FR --- Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute maintenant le dépôt au manifeste (additif, donc il accompagne une installation Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences acceptées comprises —, puis npm ci, vite build, cap sync, gradlew assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie remonte au tableau de bord. Deux manques de cet installateur amont ont dû être comblés : unzip et wget, qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34 quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des centaines de lignes anodines portant le mot error : cette sortie part dans un fichier à part, et le journal reçoit la cause nommée. Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
"ERPLibre mobile (build)": {
"fr": "ERPLibre mobile (compilation)",
"en": "ERPLibre mobile (build)",
},
"APK debug + Vitest, validates the VM": {
"fr": "APK debug + Vitest, valide la VM",
"en": "APK debug + Vitest, validates the VM",
},
"Building ERPLibre mobile (long)": {
"fr": "Compilation d'ERPLibre mobile (long)",
"en": "Building ERPLibre mobile (long)",
},
"detailed log in the VM:": {
"fr": "journal détaillé dans la VM :",
"en": "detailed log in the VM:",
},
"FAILED:": {"fr": "ÉCHEC :", "en": "FAILED:"},
"probable cause:": {"fr": "cause probable :", "en": "probable cause:"},
"no known pattern, last lines:": {
"fr": "aucun motif connu, dernières lignes :",
"en": "no known pattern, last lines:",
},
"a failed mobile build marks the VM as failed": {
"fr": "une compilation mobile en échec marque la VM en échec",
"en": "a failed mobile build marks the VM as failed",
},
[ADD] todo qemu: add an Android emulator, and fix what the real run exposed Running it on a VM was the only way to find these. install_os never installed python3-venv, so .venv.erplibre was born crippled — bin/python but no pip, no activate — and everything downstream failed on "No module named git"; one line of dependency fixes it. Capacitor 8 needs a JDK 21 where the mobile repository's installer puts 17, and Gradle must RUN on it. That installer is not idempotent either, so it is replayed only when something is missing. The new emulator option creates an AVD from the SDK's own device list — the newest plain Pixel, smallest screen — with software rendering written into its config so ssh -X does not open a black screen, and adds the user to the kvm group, without which it refuses to start. Checked on a VM: boot completed in 10 s, adb sees emulator-5554, Android 16 x86_64, no KVM refusal. Vitest: 1938 tests pass. The APK still fails, upstream: sentencepiece builds protoc for the target then runs it on the host. --- FR --- Seule l'exécution sur une VM pouvait trouver ceci. install_os n'installait pas python3-venv, si bien que .venv.erplibre naissait infirme — bin/python mais ni pip ni activate — et tout ce qui en dépend tombait sur « No module named git » ; une ligne de dépendance suffit. Capacitor 8 réclame un JDK 21 là où l'installateur du dépôt mobile pose un 17, et Gradle doit TOURNER dessus. Cet installateur n'est pas idempotent non plus : il n'est rejoué que s'il manque quelque chose. La nouvelle option crée un AVD depuis la liste de profils du SDK — le Pixel simple le plus récent, plus petit écran —, écrit le rendu logiciel dans sa configuration pour qu'ssh -X n'ouvre pas un écran noir, et ajoute l'utilisateur au groupe kvm, sans quoi il refuse de démarrer. Vérifié sur une VM : boot en 10 s, adb voit emulator-5554, Android 16 x86_64, aucun refus de KVM. Vitest : 1938 tests passent. L'APK échoue encore, en amont : sentencepiece bâtit protoc pour la cible puis l'exécute sur l'hôte. Assisted-by: Claude Opus 5
2026-08-18 02:17:08 -04:00
"ERPLibre venv (everything below needs it)": {
"fr": "venv ERPLibre (tout ce qui suit en dépend)",
"en": "ERPLibre venv (everything below needs it)",
},
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks A VM was declared ready without anything proving it. The mobile option now adds the repository to the manifest (additive, so it rides along an Odoo 18 install), runs the mobile repository's own install-android.sh — licences accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug and npm test. A failure fails the VM: the exit code reaches the dashboard. Two gaps in that upstream installer had to be filled: unzip and wget, which no cloud image ships, and the SDK platform — it installs android-34 while variables.gradle asks for compileSdk 36, so the number is read from the file rather than frozen. Gradle writes tens of megabytes and hundreds of harmless lines carrying the word error: that output goes to a file of its own, and the log gets the named cause instead. --- FR --- Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute maintenant le dépôt au manifeste (additif, donc il accompagne une installation Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences acceptées comprises —, puis npm ci, vite build, cap sync, gradlew assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie remonte au tableau de bord. Deux manques de cet installateur amont ont dû être comblés : unzip et wget, qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34 quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des centaines de lignes anodines portant le mot error : cette sortie part dans un fichier à part, et le journal reçoit la cause nommée. Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
"mobile repository (additive manifest)": {
"fr": "dépôt mobile (manifeste additif)",
"en": "mobile repository (additive manifest)",
},
"prerequisites of the upstream installer": {
"fr": "prérequis de l'installateur amont",
"en": "prerequisites of the upstream installer",
},
"Android SDK, licences, NDK": {
"fr": "SDK Android, licences, NDK",
"en": "Android SDK, licences, NDK",
},
"SDK platform required by the project": {
"fr": "plateforme SDK réclamée par le projet",
"en": "SDK platform required by the project",
},
"npm dependencies": {
"fr": "dépendances npm",
"en": "npm dependencies",
},
"web bundle (vite build)": {
"fr": "bundle web (vite build)",
"en": "web bundle (vite build)",
},
"native sync (capacitor)": {
"fr": "synchronisation native (capacitor)",
"en": "native sync (capacitor)",
},
"debug APK (gradle)": {
"fr": "APK debug (gradle)",
"en": "debug APK (gradle)",
},
"Vitest tests": {"fr": "tests Vitest", "en": "Vitest tests"},
"APK built:": {"fr": "APK construit :", "en": "APK built:"},
"browser debugging (no Android):": {
"fr": "déverminage au navigateur (sans Android) :",
"en": "browser debugging (no Android):",
},
"no APK produced": {
"fr": "aucun APK produit",
"en": "no APK produced",
},
"disk full": {"fr": "disque plein", "en": "disk full"},
"SDK platform missing": {
"fr": "plateforme SDK absente",
"en": "SDK platform missing",
},
"SDK not found (ANDROID_HOME)": {
"fr": "SDK introuvable (ANDROID_HOME)",
"en": "SDK not found (ANDROID_HOME)",
},
"SDK licences not accepted": {
"fr": "licences SDK non acceptées",
"en": "SDK licences not accepted",
},
"NDK missing": {"fr": "NDK absent", "en": "NDK missing"},
[ADD] todo qemu: add an Android emulator, and fix what the real run exposed Running it on a VM was the only way to find these. install_os never installed python3-venv, so .venv.erplibre was born crippled — bin/python but no pip, no activate — and everything downstream failed on "No module named git"; one line of dependency fixes it. Capacitor 8 needs a JDK 21 where the mobile repository's installer puts 17, and Gradle must RUN on it. That installer is not idempotent either, so it is replayed only when something is missing. The new emulator option creates an AVD from the SDK's own device list — the newest plain Pixel, smallest screen — with software rendering written into its config so ssh -X does not open a black screen, and adds the user to the kvm group, without which it refuses to start. Checked on a VM: boot completed in 10 s, adb sees emulator-5554, Android 16 x86_64, no KVM refusal. Vitest: 1938 tests pass. The APK still fails, upstream: sentencepiece builds protoc for the target then runs it on the host. --- FR --- Seule l'exécution sur une VM pouvait trouver ceci. install_os n'installait pas python3-venv, si bien que .venv.erplibre naissait infirme — bin/python mais ni pip ni activate — et tout ce qui en dépend tombait sur « No module named git » ; une ligne de dépendance suffit. Capacitor 8 réclame un JDK 21 là où l'installateur du dépôt mobile pose un 17, et Gradle doit TOURNER dessus. Cet installateur n'est pas idempotent non plus : il n'est rejoué que s'il manque quelque chose. La nouvelle option crée un AVD depuis la liste de profils du SDK — le Pixel simple le plus récent, plus petit écran —, écrit le rendu logiciel dans sa configuration pour qu'ssh -X n'ouvre pas un écran noir, et ajoute l'utilisateur au groupe kvm, sans quoi il refuse de démarrer. Vérifié sur une VM : boot en 10 s, adb voit emulator-5554, Android 16 x86_64, aucun refus de KVM. Vitest : 1938 tests passent. L'APK échoue encore, en amont : sentencepiece bâtit protoc pour la cible puis l'exécute sur l'hôte. Assisted-by: Claude Opus 5
2026-08-18 02:17:08 -04:00
"JDK required by the project missing": {
"fr": "JDK réclamé par le projet absent",
"en": "JDK required by the project missing",
},
"Gradle running on too old a JDK": {
"fr": "Gradle tourne sur un JDK trop ancien",
"en": "Gradle running on too old a JDK",
},
"SDK already there (upstream installer replays)": {
"fr": "SDK déjà là (l'installateur amont se rejoue)",
"en": "SDK already there (upstream installer replays)",
},
"cross-compiled protoc run on the host": {
"fr": "protoc compilé pour la cible, exécuté sur l'hôte",
"en": "cross-compiled protoc run on the host",
},
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks A VM was declared ready without anything proving it. The mobile option now adds the repository to the manifest (additive, so it rides along an Odoo 18 install), runs the mobile repository's own install-android.sh — licences accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug and npm test. A failure fails the VM: the exit code reaches the dashboard. Two gaps in that upstream installer had to be filled: unzip and wget, which no cloud image ships, and the SDK platform — it installs android-34 while variables.gradle asks for compileSdk 36, so the number is read from the file rather than frozen. Gradle writes tens of megabytes and hundreds of harmless lines carrying the word error: that output goes to a file of its own, and the log gets the named cause instead. --- FR --- Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute maintenant le dépôt au manifeste (additif, donc il accompagne une installation Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences acceptées comprises —, puis npm ci, vite build, cap sync, gradlew assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie remonte au tableau de bord. Deux manques de cet installateur amont ont dû être comblés : unzip et wget, qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34 quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des centaines de lignes anodines portant le mot error : cette sortie part dans un fichier à part, et le journal reçoit la cause nommée. Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
"JDK/Gradle mismatch": {
"fr": "JDK et Gradle incompatibles",
"en": "JDK/Gradle mismatch",
},
"Gradle dependency unreachable (network?)": {
"fr": "dépendance Gradle injoignable (réseau ?)",
"en": "Gradle dependency unreachable (network?)",
},
"Vitest tests failed": {
"fr": "tests Vitest en échec",
"en": "Vitest tests failed",
},
[ADD] todo qemu: add an Android emulator, and fix what the real run exposed Running it on a VM was the only way to find these. install_os never installed python3-venv, so .venv.erplibre was born crippled — bin/python but no pip, no activate — and everything downstream failed on "No module named git"; one line of dependency fixes it. Capacitor 8 needs a JDK 21 where the mobile repository's installer puts 17, and Gradle must RUN on it. That installer is not idempotent either, so it is replayed only when something is missing. The new emulator option creates an AVD from the SDK's own device list — the newest plain Pixel, smallest screen — with software rendering written into its config so ssh -X does not open a black screen, and adds the user to the kvm group, without which it refuses to start. Checked on a VM: boot completed in 10 s, adb sees emulator-5554, Android 16 x86_64, no KVM refusal. Vitest: 1938 tests pass. The APK still fails, upstream: sentencepiece builds protoc for the target then runs it on the host. --- FR --- Seule l'exécution sur une VM pouvait trouver ceci. install_os n'installait pas python3-venv, si bien que .venv.erplibre naissait infirme — bin/python mais ni pip ni activate — et tout ce qui en dépend tombait sur « No module named git » ; une ligne de dépendance suffit. Capacitor 8 réclame un JDK 21 là où l'installateur du dépôt mobile pose un 17, et Gradle doit TOURNER dessus. Cet installateur n'est pas idempotent non plus : il n'est rejoué que s'il manque quelque chose. La nouvelle option crée un AVD depuis la liste de profils du SDK — le Pixel simple le plus récent, plus petit écran —, écrit le rendu logiciel dans sa configuration pour qu'ssh -X n'ouvre pas un écran noir, et ajoute l'utilisateur au groupe kvm, sans quoi il refuse de démarrer. Vérifié sur une VM : boot en 10 s, adb voit emulator-5554, Android 16 x86_64, aucun refus de KVM. Vitest : 1938 tests passent. L'APK échoue encore, en amont : sentencepiece bâtit protoc pour la cible puis l'exécute sur l'hôte. Assisted-by: Claude Opus 5
2026-08-18 02:17:08 -04:00
"ERPLibre venv incomplete (no pip: python3-venv)": {
"fr": "venv ERPLibre incomplet (pas de pip : python3-venv)",
"en": "ERPLibre venv incomplete (no pip: python3-venv)",
},
"mobile repository missing": {
"fr": "dépôt mobile absent",
"en": "mobile repository missing",
},
"ERPLibre venv missing (incomplete install)": {
"fr": "venv ERPLibre absent (installation incomplète)",
"en": "ERPLibre venv missing (incomplete install)",
},
[ADD] todo qemu: offer the adb tunnel for scrcpy, X11-free X11 carries raw pixels: 0.62 megapixel per frame even after shrinking the screen, rendered in software. scrcpy receives an H.264 stream encoded BY the device, so the emulator runs with no window at all — no X11 anywhere. The tunnel menu gained a fourth kind, reusing the target list it already reads from ~/.ssh/config, ProxyJump included: the hard part was already solved there. It forwards 5555, the emulator's own adb port, not 5037: tunnelling the adb server would force the user to kill the one on their workstation, which holds the same port. Verified through the tunnel from the hypervisor: an adb CNXN handshake answers device::ro.product.name=sdk_gphone64_x86, exactly what adb connect does. ss -ltn in the VM confirms 5554, 5555 and 5037 all listen on 127.0.0.1. --- FR --- X11 transporte des pixels bruts : 0,62 mégapixel par image même après réduction de l'écran, en rendu logiciel. scrcpy reçoit un flux H.264 encodé PAR l'appareil, si bien que l'émulateur tourne sans aucune fenêtre — plus de X11 nulle part. Le menu de tunnel gagne un quatrième type, qui réutilise la liste de cibles qu'il lit déjà dans ~/.ssh/config, ProxyJump compris : le plus dur y était déjà résolu. Il redirige 5555, le port adb de l'émulateur lui-même, et non 5037 : tunneliser le serveur adb obligerait à tuer celui du poste, qui occupe le même port. Vérifié à travers le tunnel depuis l'hyperviseur : une poignée de main adb CNXN répond device::ro.product.name=sdk_gphone64_x86, ce que fait exactement adb connect. « ss -ltn » dans la VM confirme que 5554, 5555 et 5037 écoutent tous sur 127.0.0.1. Assisted-by: Claude Opus 5
2026-08-19 02:39:10 -04:00
"smoother, without X11:": {
"fr": "plus fluide, sans X11 :",
"en": "smoother, without X11:",
},
[ADD] script todo: lancer l'émulateur Android et son tunnel adb Le tunnel n'était que du texte à recopier, et le démarrage restait manuel : un second émulateur sur le même AVD tue le premier (« Running multiple emulators »), rencontré deux fois. Le menu le détecte donc avant de lancer. La fenêtre décide du reste. Sans elle, todo.py démarre à distance, détaché par setsid dans le groupe kvm, et enchaîne sur scrcpy ; avec elle, la commande doit partir du poste qui possède l'affichage, pas d'ici. Mesuré : l'émulateur n'écoute que sur le 127.0.0.1 de la VM — l'hyperviseur est refusé sur IP:5555 — d'où le -J qui met la VM en dernier saut. Vérifié sur erplibre-mobile-proof : poignée de main adb CNXN rendant sdk_gphone64_x86_64, enveloppe détachée survivant au ssh ; 30 tests. --- EN --- The tunnel was only text to copy, and starting the emulator stayed manual: a second emulator on the same AVD kills the first ("Running multiple emulators"), hit twice. The menu now detects it before starting. The window decides the rest. Without one, todo.py starts it remotely, detached by setsid in the kvm group, and chains into scrcpy; with one, the command must run from the workstation that owns the display, not from here. Measured: the emulator only listens on the VM's 127.0.0.1 — the hypervisor is refused on IP:5555 — hence the -J putting the VM last. Verified on erplibre-mobile-proof: an adb CNXN handshake returning sdk_gphone64_x86_64, a detached wrapper outliving the ssh; 30 tests. Assisted-by: Claude Opus 5
2026-08-19 03:35:31 -04:00
"Android emulator": {"fr": "Émulateur Android", "en": "Android emulator"},
"Android emulator (start, tunnel, scrcpy)": {
"fr": "Émulateur Android (démarrer, tunnel, scrcpy)",
"en": "Android emulator (start, tunnel, scrcpy)",
},
"An emulator is already running on this VM.": {
"fr": "Un émulateur tourne déjà sur cette VM.",
"en": "An emulator is already running on this VM.",
},
"Only one per AVD; close it first:": {
"fr": "Un seul par AVD ; le fermer d'abord :",
"en": "Only one per AVD; close it first:",
},
"Close it now? (y/N): ": {
"fr": "Le fermer maintenant ? (o/N) : ",
"en": "Close it now? (y/N): ",
},
"Closed.": {"fr": "Fermé.", "en": "Closed."},
"Show a window?": {
"fr": "Afficher une fenêtre ?",
"en": "Show a window?",
},
"No window - stream with scrcpy (smoother)": {
"fr": "Sans fenêtre — flux par scrcpy (plus fluide)",
"en": "No window - stream with scrcpy (smoother)",
},
"Window over ssh -X (raw pixels, slower)": {
"fr": "Fenêtre par ssh -X (pixels bruts, plus lent)",
"en": "Window over ssh -X (raw pixels, slower)",
},
"X11 compression is on (-XC); the screen is 540x1140.": {
"fr": "La compression X11 est active (-XC) ; l'écran fait 540x1140.",
"en": "X11 compression is on (-XC); the screen is 540x1140.",
},
"Starting the emulator without a window...": {
"fr": "Démarrage de l'émulateur sans fenêtre…",
"en": "Starting the emulator without a window...",
},
"Could not start it:": {
"fr": "Démarrage impossible :",
"en": "Could not start it:",
},
"Started. Boot takes about a minute; log in the VM:": {
"fr": "Démarré. L'amorçage prend environ une minute ; journal dans la VM :",
"en": "Started. Boot takes about a minute; log in the VM:",
},
"1. Emulator started, without a window.": {
"fr": "1. Émulateur démarré, sans fenêtre.",
"en": "1. Emulator started, without a window.",
},
"If scrcpy will run on THIS machine, I can open it.": {
"fr": "Si scrcpy tournera sur CETTE machine, je peux l'ouvrir.",
"en": "If scrcpy will run on THIS machine, I can open it.",
},
"Open the tunnel now? (y/N): ": {
"fr": "Ouvrir le tunnel maintenant ? (o/N) : ",
"en": "Open the tunnel now? (y/N): ",
},
"Port already in use here:": {
"fr": "Port déjà occupé ici :",
"en": "Port already in use here:",
},
"Close the other tunnel first:": {
"fr": "Fermer l'autre tunnel d'abord :",
"en": "Close the other tunnel first:",
},
"Tunnel failed:": {"fr": "Tunnel en échec :", "en": "Tunnel failed:"},
"Tunnel open on localhost:": {
"fr": "Tunnel ouvert sur localhost:",
"en": "Tunnel open on localhost:",
},
"Then:": {"fr": "Puis :", "en": "Then:"},
"To close it:": {"fr": "Pour le fermer :", "en": "To close it:"},
[ADD] todo qemu: offer the adb tunnel for scrcpy, X11-free X11 carries raw pixels: 0.62 megapixel per frame even after shrinking the screen, rendered in software. scrcpy receives an H.264 stream encoded BY the device, so the emulator runs with no window at all — no X11 anywhere. The tunnel menu gained a fourth kind, reusing the target list it already reads from ~/.ssh/config, ProxyJump included: the hard part was already solved there. It forwards 5555, the emulator's own adb port, not 5037: tunnelling the adb server would force the user to kill the one on their workstation, which holds the same port. Verified through the tunnel from the hypervisor: an adb CNXN handshake answers device::ro.product.name=sdk_gphone64_x86, exactly what adb connect does. ss -ltn in the VM confirms 5554, 5555 and 5037 all listen on 127.0.0.1. --- FR --- X11 transporte des pixels bruts : 0,62 mégapixel par image même après réduction de l'écran, en rendu logiciel. scrcpy reçoit un flux H.264 encodé PAR l'appareil, si bien que l'émulateur tourne sans aucune fenêtre — plus de X11 nulle part. Le menu de tunnel gagne un quatrième type, qui réutilise la liste de cibles qu'il lit déjà dans ~/.ssh/config, ProxyJump compris : le plus dur y était déjà résolu. Il redirige 5555, le port adb de l'émulateur lui-même, et non 5037 : tunneliser le serveur adb obligerait à tuer celui du poste, qui occupe le même port. Vérifié à travers le tunnel depuis l'hyperviseur : une poignée de main adb CNXN répond device::ro.product.name=sdk_gphone64_x86, ce que fait exactement adb connect. « ss -ltn » dans la VM confirme que 5554, 5555 et 5037 écoutent tous sur 127.0.0.1. Assisted-by: Claude Opus 5
2026-08-19 02:39:10 -04:00
"Android emulator (adb 5555, then scrcpy)": {
"fr": "Émulateur Android (adb 5555, puis scrcpy)",
"en": "Android emulator (adb 5555, then scrcpy)",
},
"Android emulator over adb + scrcpy": {
"fr": "Émulateur Android par adb + scrcpy",
"en": "Android emulator over adb + scrcpy",
},
"1. In the VM, start the emulator WITHOUT a window:": {
"fr": "1. Dans la VM, démarrer l'émulateur SANS fenêtre :",
"en": "1. In the VM, start the emulator WITHOUT a window:",
},
"2. Open the tunnel from YOUR workstation:": {
"fr": "2. Ouvrir le tunnel depuis VOTRE poste :",
"en": "2. Open the tunnel from YOUR workstation:",
},
"3. Then, still on your workstation:": {
"fr": "3. Puis, toujours sur votre poste :",
"en": "3. Then, still on your workstation:",
},
"scrcpy on Debian/Ubuntu:": {
"fr": "scrcpy sur Debian/Ubuntu :",
"en": "scrcpy on Debian/Ubuntu:",
},
[ADD] todo qemu: add an Android emulator, and fix what the real run exposed Running it on a VM was the only way to find these. install_os never installed python3-venv, so .venv.erplibre was born crippled — bin/python but no pip, no activate — and everything downstream failed on "No module named git"; one line of dependency fixes it. Capacitor 8 needs a JDK 21 where the mobile repository's installer puts 17, and Gradle must RUN on it. That installer is not idempotent either, so it is replayed only when something is missing. The new emulator option creates an AVD from the SDK's own device list — the newest plain Pixel, smallest screen — with software rendering written into its config so ssh -X does not open a black screen, and adds the user to the kvm group, without which it refuses to start. Checked on a VM: boot completed in 10 s, adb sees emulator-5554, Android 16 x86_64, no KVM refusal. Vitest: 1938 tests pass. The APK still fails, upstream: sentencepiece builds protoc for the target then runs it on the host. --- FR --- Seule l'exécution sur une VM pouvait trouver ceci. install_os n'installait pas python3-venv, si bien que .venv.erplibre naissait infirme — bin/python mais ni pip ni activate — et tout ce qui en dépend tombait sur « No module named git » ; une ligne de dépendance suffit. Capacitor 8 réclame un JDK 21 là où l'installateur du dépôt mobile pose un 17, et Gradle doit TOURNER dessus. Cet installateur n'est pas idempotent non plus : il n'est rejoué que s'il manque quelque chose. La nouvelle option crée un AVD depuis la liste de profils du SDK — le Pixel simple le plus récent, plus petit écran —, écrit le rendu logiciel dans sa configuration pour qu'ssh -X n'ouvre pas un écran noir, et ajoute l'utilisateur au groupe kvm, sans quoi il refuse de démarrer. Vérifié sur une VM : boot en 10 s, adb voit emulator-5554, Android 16 x86_64, aucun refus de KVM. Vitest : 1938 tests passent. L'APK échoue encore, en amont : sentencepiece bâtit protoc pour la cible puis l'exécute sur l'hôte. Assisted-by: Claude Opus 5
2026-08-18 02:17:08 -04:00
"Android emulator (Pixel)": {
"fr": "Émulateur Android (Pixel)",
"en": "Android emulator (Pixel)",
},
"AVD viewable over ssh -X": {
"fr": "AVD visible par ssh -X",
"en": "AVD viewable over ssh -X",
},
"Android emulator (AVD)": {
"fr": "Émulateur Android (AVD)",
"en": "Android emulator (AVD)",
},
"no /dev/kvm: nested virtualisation is off on the host": {
"fr": "pas de /dev/kvm : virtualisation imbriquée coupée sur l'hôte",
"en": "no /dev/kvm: nested virtualisation is off on the host",
},
"user added to the kvm group (effective at next login)": {
"fr": "utilisateur ajouté au groupe kvm (effectif à la prochaine session)",
"en": "user added to the kvm group (effective at next login)",
},
"emulator and system image": {
"fr": "émulateur et image système",
"en": "emulator and system image",
},
"Pixel profile, smallest screen": {
"fr": "profil Pixel, plus petit écran",
"en": "Pixel profile, smallest screen",
},
"create the AVD": {"fr": "création de l'AVD", "en": "create the AVD"},
"AVD ready:": {"fr": "AVD prêt :", "en": "AVD ready:"},
"open it from your workstation:": {
"fr": "à ouvrir depuis votre poste :",
"en": "open it from your workstation:",
},
"then install the APK:": {
"fr": "puis installer l'APK :",
"en": "then install the APK:",
},
"ERPLibre mobile, Android SDK (long)": {
"fr": "ERPLibre mobile, SDK Android (long)",
"en": "ERPLibre mobile, Android SDK (long)",
},
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks A VM was declared ready without anything proving it. The mobile option now adds the repository to the manifest (additive, so it rides along an Odoo 18 install), runs the mobile repository's own install-android.sh — licences accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug and npm test. A failure fails the VM: the exit code reaches the dashboard. Two gaps in that upstream installer had to be filled: unzip and wget, which no cloud image ships, and the SDK platform — it installs android-34 while variables.gradle asks for compileSdk 36, so the number is read from the file rather than frozen. Gradle writes tens of megabytes and hundreds of harmless lines carrying the word error: that output goes to a file of its own, and the log gets the named cause instead. --- FR --- Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute maintenant le dépôt au manifeste (additif, donc il accompagne une installation Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences acceptées comprises —, puis npm ci, vite build, cap sync, gradlew assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie remonte au tableau de bord. Deux manques de cet installateur amont ont dû être comblés : unzip et wget, qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34 quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des centaines de lignes anodines portant le mot error : cette sortie part dans un fichier à part, et le journal reçoit la cause nommée. Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
"Gradle task failed": {
"fr": "tâche Gradle en échec",
"en": "Gradle task failed",
},
[FIX] todo qemu: open the PyCharm project without a screen, and unlicensed Two measurements, one after the other. The unified PyCharm that code=PCC&latest now serves stops on its licence: the log says NoValidIdeLicense then "Get licenses: request requires authentication", and no project is ever opened — so no .idea, so nothing for the install to configure. The Community line asks for no account and is still patched (2025.2.6.2 on 2026-07-29); it is resolved from the release feed, so no version is frozen here. That build then froze 1.3 s after startup: the trust dialog, invisible under Xvfb and waiting for a click. idea.trust.all.projects unblocks it. Checked on an Ubuntu 26.04 VM: .idea complete in 195 s, and pycharm_configuration.py writes its exclusions into erplibre.iml. --- FR --- Deux mesures, l'une après l'autre. Le PyCharm unifié que sert désormais code=PCC&latest s'arrête sur sa licence : le journal dit NoValidIdeLicense puis « Get licenses: request requires authentication », et aucun projet ne s'ouvre — donc pas de .idea, donc rien à configurer pour l'installation. La ligne Community ne demande aucun compte et reste corrigée (2025.2.6.2 le 2026-07-29) ; elle est résolue depuis le flux des versions, sans qu'aucun numéro ne soit figé ici. Ce build se figeait ensuite 1,3 s après le démarrage : la fenêtre de confiance, invisible sous Xvfb et attendant un clic. idea.trust.all.projects la lève. Vérifié sur une VM Ubuntu 26.04 : .idea complet en 195 s, et pycharm_configuration.py écrit ses exclusions dans erplibre.iml. Assisted-by: Claude Opus 5
2026-08-17 23:22:38 -04:00
"project created, the install will configure it": {
"fr": "projet créé, l'installation va le configurer",
"en": "project created, the install will configure it",
},
"no .idea: open PyCharm once, then": {
"fr": "pas de .idea : ouvrir PyCharm une fois, puis",
"en": "no .idea: open PyCharm once, then",
},
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions A graphical VM was a desktop and nothing else: every developer tool had to be installed by hand afterwards. A check list now carries them, filtered per machine — Android Studio is x86_64 only, Google publishes no Linux aarch64 build — and their disk cost reaches the plan before any qcow2 is created. They are installed BEFORE the clone: PyCharm writes the .idea/ of the repository, and the install that follows is what runs pycharm_configuration.py, through update_env_version.pycharm_update(). Its launcher is named studio, which is enough to conclude the install failed; it now answers to android-studio too. GNOME extensions come from the site by UUID, per running Shell: the same endpoint serves gTile v59 for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and the install's exit code still wins, 45 tests. --- FR --- Une VM graphique n'était qu'un bureau : chaque outil de développement restait à poser à la main. Une liste à cocher les porte, filtrés machine par machine — Android Studio n'existe qu'en x86_64, Google ne publiant aucune archive Linux aarch64 — et leur place disque atteint le plan avant qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance pycharm_configuration.py, via update_env_version.pycharm_update(). Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il répond désormais aussi à android-studio. Les extensions GNOME viennent du site par UUID, selon le Shell qui tourne : le même point d'entrée sert gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque outil peut échouer sans que le code de sortie de l'installation ne change, 45 tests. Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
"project": {"fr": "projet", "en": "project"},
2026-03-07 02:59:07 -05:00
}
def get_lang() -> str:
2026-03-07 02:59:07 -05:00
global _current_lang
if _current_lang is not None:
return _current_lang
# 1. Check env_var.sh file
if os.path.exists(ENV_VAR_FILE):
2026-03-07 02:59:07 -05:00
try:
with open(ENV_VAR_FILE) as f:
2026-03-07 02:59:07 -05:00
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: str) -> None:
2026-03-07 02:59:07 -05:00
global _current_lang
_current_lang = lang
# Persist to env_var.sh
if os.path.exists(ENV_VAR_FILE):
2026-03-07 02:59:07 -05:00
try:
with open(ENV_VAR_FILE) as f:
2026-03-07 02:59:07 -05:00
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(ENV_VAR_FILE, "w") as f:
2026-03-07 02:59:07 -05:00
f.write(content)
def lang_is_configured() -> bool:
2026-03-07 02:59:07 -05:00
"""Check if a language has been explicitly set."""
if os.path.exists(ENV_VAR_FILE):
2026-03-07 02:59:07 -05:00
try:
with open(ENV_VAR_FILE) as f:
2026-03-07 02:59:07 -05:00
content = f.read()
return bool(re.search(r"^EL_LANG=", content, re.MULTILINE))
except OSError:
pass
return False
def t(key: str) -> str:
2026-03-07 02:59:07 -05:00
entry = TRANSLATIONS.get(key)
if entry is None:
return key
lang = get_lang()
return entry.get(lang, entry.get("fr", key))