From d2c5d92d115edffefb2ad061c00224c81ff87116 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 13 Aug 2026 04:01:00 -0400 Subject: [PATCH] [FIX] migration: the COW tools speak the system language MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit They run as subprocesses and had no i18n, so a French migration alternated languages from one line to the next. The database upgrade itself had the same gap. The driver read English sentences out of their output to decide whether to ask its question. Translating them would have made it mute -- no error, no trace. The link is now the exit code, which no language touches: 0 nothing, 1 copies concerned, 2 the tool failed. 87 strings, five tools. A test rejects any displayed sentence that skips t(), and proves itself on an untranslated one. --- FR --- Ils tournent en sous-processus et n'avaient aucun i18n : une migration en français alternait les deux langues d'une ligne à l'autre. La mise à niveau de la base elle-même souffrait du même manque. Le pilote lisait des phrases anglaises dans leur sortie pour décider de poser sa question. Les traduire l'aurait rendu muet — sans erreur, sans trace. Le lien est désormais le code de sortie, qu'aucune langue ne touche : 0 rien, 1 copies concernées, 2 l'outil a échoué. 87 chaînes, cinq outils. Un test refuse toute phrase affichée qui saute t(), et fait sa preuve sur une chaîne non traduite. Assisted-by: Claude Opus 5 --- script/odoo/migration/check_cow_views.py | 73 +++-- script/odoo/migration/cow_drift.py | 58 ++-- script/odoo/migration/neutralize_cow_views.py | 49 +-- .../odoo/migration/reset_stale_cow_views.py | 48 +-- script/odoo/migration/snapshot_cow_views.py | 30 +- script/todo/todo_upgrade.py | 281 ++++++++++++------ test/test_migration_tools_i18n.py | 244 +++++++++++++++ 7 files changed, 588 insertions(+), 195 deletions(-) create mode 100644 test/test_migration_tools_i18n.py diff --git a/script/odoo/migration/check_cow_views.py b/script/odoo/migration/check_cow_views.py index 3acca71..8526a2b 100755 --- a/script/odoo/migration/check_cow_views.py +++ b/script/odoo/migration/check_cow_views.py @@ -50,6 +50,18 @@ import subprocess import sys import xml.etree.ElementTree as ET +sys.path.append( + os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) +) + +try: + from script.todo.todo_i18n import t +except Exception: # pragma: no cover - repli si i18n indisponible + + def t(key: str) -> str: + return key + + # A view whose module counterpart cannot be found at all. MODE_UNKNOWN = "unknown" @@ -232,6 +244,9 @@ def analyse(database, target_version): # The decisive test: the target expects inheritance specs but the copy # holds a standalone template, or the reverse. if is_spec is not None and target_inherits != is_spec: + # La clé anglaise est stockée, la traduction se fait à + # l'affichage : cow_drift et neutralize la relisent, et une + # valeur déjà traduite les obligerait à traduire en sens inverse. reason = ( "target inherits, copy holds a standalone template" if target_inherits @@ -281,9 +296,10 @@ def main(): if not os.path.isdir(config.target_version): print( - f"❌ Target version directory '{config.target_version}' not found." + f"❌ {t('Target version directory not found')} :" + f" '{config.target_version}'" ) - return 1 + return 2 lst_at_risk, lst_module_absent, lst_no_counterpart = analyse( config.database, config.target_version @@ -293,14 +309,16 @@ def main(): if not lst_at_risk: print( - "✅ -> No website COW view changes mode in" + f"✅ -> {t('No website COW view changes shape in')}" f" {config.target_version}." ) else: print( - f"⚠️ {len(lst_at_risk)} website COW view(s) will break when moving" - f" to {config.target_version}: the copy keeps an arch whose shape" - " no longer matches what the target module view expects." + f"⚠️ {len(lst_at_risk)}" + f" {t('website COW view(s) will break when moving to')}" + f" {config.target_version} :" + f" {t('the copy keeps an arch whose shape no longer matches what')}" + f" {t('the target module view expects.')}" ) for ( view_id, @@ -312,52 +330,55 @@ def main(): ) in lst_at_risk: print( f" - id={view_id} website={website_id} {key}" - f" : {mode} -> {target_mode} ({reason})" + f" : {mode} -> {target_mode} ({t(reason)})" ) # La base et la version cible sont connues ici : les remplacer par # « DB » et « odooXX.0 » oblige à les retrouver, au moment précis où # l'on veut juste copier-coller la commande. print( - " The migration will offer to neutralize them at the bump, and" - " shows what each copy holds before you answer. To look now," - " without interrupting anything:" + f" {t('The migration offers to neutralize them, and shows what')}" + f" {t('each copy holds before you answer. To look now:')}" f"\n ./script/odoo/migration/cow_drift.py -d {database}" - f" -t {target_version} (what each copy holds)" + f" -t {target_version} ({t('what each copy holds')})" f"\n ./script/odoo/migration/cow_drift.py -d {database}" - f" -t {target_version} --shape (why it breaks)" + f" -t {target_version} --shape ({t('why it breaks')})" f"\n ./script/odoo/migration/neutralize_cow_views.py" f" -d {database} -t {target_version} --apply" - " (reversible with --restore)" + f" ({t('reversible with --restore')})" ) print( - " Or by hand. To neutralize a copy," - " rename its key (UPDATE ir_ui_view SET key='zz_cow_archive.'||key," - " active=false): an unmatched key is never paired with the module" - " view, so the copy never receives the new inherit_id. Setting" - " active=false alone is NOT enough -- an inactive copy that keeps" - " the same key still shadows the module view." + f" {t('Or by hand. To neutralize a copy, rename its key')}" + " (UPDATE ir_ui_view SET key='zz_cow_archive.'||key," + f" active=false) : {t('an unmatched key is never paired with the')}" + f" {t('module view, so the copy never receives the new')}" + f" inherit_id. {t('Setting active=false alone is NOT enough:')}" + f" {t('an inactive copy keeping the same key still shadows it.')}" ) if lst_module_absent: print( - f"ℹ {len(lst_module_absent)} COW view(s) belong to a module absent" - f" from {config.target_version}:" + f"ℹ {len(lst_module_absent)}" + f" {t('COW view(s) belong to a module absent from')}" + f" {config.target_version} :" ) for view_id, key, mode, website_id in lst_module_absent: print(f" - id={view_id} website={website_id} {key} ({mode})") if lst_no_counterpart: print( - f"ℹ {len(lst_no_counterpart)} COW view(s) are pages or records made" - " from the website editor (no module view of that name): not at" - " risk." + ("" if config.verbose else " Use -v to list them.") + f"ℹ {len(lst_no_counterpart)}" + f" {t('COW view(s) are pages or records made in the website')}" + f" {t('editor (no module view of that name): not at risk.')}" + + ("" if config.verbose else f" {t('Use -v to list them.')}") ) if config.verbose: for view_id, key, mode, website_id in lst_no_counterpart: print(f" - id={view_id} website={website_id} {key} ({mode})") - # Informative only: never fail the migration on a warning. - return 0 + # 0 = rien à signaler, 1 = des copies casseront, 2 = l'outil a échoué. + # Le pilote lisait le texte anglais de cette sortie pour savoir s'il + # devait poser sa question : traduire le message le rendait aveugle. + return 1 if lst_at_risk else 0 if __name__ == "__main__": diff --git a/script/odoo/migration/cow_drift.py b/script/odoo/migration/cow_drift.py index 5972d94..e96402c 100644 --- a/script/odoo/migration/cow_drift.py +++ b/script/odoo/migration/cow_drift.py @@ -31,7 +31,7 @@ import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from check_cow_views import analyse, find_module_dir # noqa: E402 +from check_cow_views import analyse, find_module_dir, t # noqa: E402 # The declaration of a template spans a few lines; showing the opening tag and # what follows is enough to see its shape, and short enough to compare two @@ -208,8 +208,8 @@ def render_diff(finding): ] if finding["module_id"] is None: lines += [ - " No module view carries this key, so there is nothing to compare", - " against: this copy is a page made in the website editor.", + f" {t('No module view carries this key, so there is nothing to')}", + f" {t('compare: this copy is a page made in the website editor.')}", ] return "\n".join(lines) @@ -227,7 +227,7 @@ def render_diff(finding): ) ] if len(diff) <= 2: - lines.append(" The copy is identical to the module view.") + lines.append(f" {t('The copy is identical to the module view.')}") return "\n".join(lines) lines += [f" {line}" for line in diff] n_plus = sum( @@ -238,8 +238,8 @@ def render_diff(finding): ) lines += [ "", - f" {n_plus} line(s) added, {n_minus} removed — this is what" - " neutralizing gives up.", + f" {n_plus} {t('line(s) added')}, {n_minus} {t('removed')} —" + f" {t('this is what neutralizing gives up.')}", ] return "\n".join(lines) @@ -249,7 +249,7 @@ def render_shape(finding): lines = [ f"── id={finding['id']} {finding['key']} ──", "", - f" {finding['reason']}", + f" {t(finding['reason'])}", "", ] for label, decl in ( @@ -259,7 +259,7 @@ def render_shape(finding): lines.append(f" ") if decl is None: lines += [ - " (the module no longer declares this template)", + f" ({t('the module no longer declares this template')})", "", ] continue @@ -267,19 +267,20 @@ def render_shape(finding): lines.append(f" ") lines += [f" {line}" for line in snippet.splitlines()] shape = ( - "inheritance specs (needs inherit_id)" + t("inheritance specs (needs inherit_id)") if re.search(r"inherit_id\s*=", snippet) - else "a standalone template" + else t("a standalone template") ) lines += [f" -> {shape}", ""] lines += [ - " Odoo changing the shape of its own template is NOT the problem: on a", - " database without a copy, the module upgrade rewrites the view and", - " nothing breaks. It breaks here because a COPY exists and froze the", - " old shape — the copy follows the module and becomes an extension,", - " while still holding a standalone template. Odoo then applies that", - " template as an inheritance spec and stops on « cannot be located in", - " parent view ».", + f" {t('Odoo changing the shape of its own template is NOT the')}", + f" {t('problem: on a database without a copy, the module upgrade')}", + f" {t('rewrites the view and nothing breaks. It breaks here because')}", + f" {t('a COPY exists and froze the old shape — the copy follows the')}", + f" {t('module and becomes an extension, while still holding a')}", + f" {t('standalone template. Odoo then applies that template as an')}", + f" {t('inheritance spec and stops on « cannot be located in parent')}", + f" {t('view ».')}", "", ] # Sans ceci, cette vue se lit comme « du code Odoo qui change », et l'on @@ -298,8 +299,8 @@ def _what_the_copy_holds(finding): """ if finding["module_id"] is None: return [ - " This copy has no module view of that name: it is a page made in", - " the website editor, and nothing else holds its content.", + f" {t('This copy has no module view of that name: it is a page')}", + f" {t('made in the website editor, and nothing holds its content.')}", ] left = finding["module_arch"].splitlines() right = finding["copy_arch"].splitlines() @@ -310,23 +311,23 @@ def _what_the_copy_holds(finding): ] if not diff: return [ - " This copy is IDENTICAL to the module view it shadows: it holds no", - " customization at all, so neutralizing it loses nothing.", + f" {t('This copy is IDENTICAL to the module view it shadows: it')}", + f" {t('holds no customization, so neutralizing it loses nothing.')}", ] n_plus = sum(1 for x in diff if x.startswith("+")) n_minus = sum(1 for x in diff if x.startswith("-")) return [ - f" This copy differs from the module view by +{n_plus}/-{n_minus}" - " line(s):", - " that is the customization, and all that neutralizing gives up.", - " Run without --shape to read it.", + f" {t('This copy differs from the module view by')}" + f" +{n_plus}/-{n_minus} {t('line(s):')}", + f" {t('that is the customization, and all neutralizing gives up.')}", + f" {t('Run without --shape to read it.')}", ] def render_all(lst_finding, shape=False): """The whole report, one block per finding.""" if not lst_finding: - return "✅ No website COW view is at risk.\n" + return f"✅ {t('No website COW view is at risk.')}\n" render = render_shape if shape else render_diff return "\n\n".join(render(f) for f in lst_finding) + "\n" @@ -363,7 +364,10 @@ def main(argv=None): config = parser.parse_args(argv) if not os.path.isdir(config.target_version): - print(f"❌ Target version '{config.target_version}' not found.") + print( + f"❌ {t('Target version directory not found')} :" + f" '{config.target_version}'" + ) return 2 try: lst_finding = collect( diff --git a/script/odoo/migration/neutralize_cow_views.py b/script/odoo/migration/neutralize_cow_views.py index 021cef8..5b83077 100755 --- a/script/odoo/migration/neutralize_cow_views.py +++ b/script/odoo/migration/neutralize_cow_views.py @@ -32,7 +32,7 @@ import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from check_cow_views import analyse # noqa: E402 +from check_cow_views import analyse, t # noqa: E402 DEFAULT_PREFIX = "zz_cow_archive" @@ -160,9 +160,15 @@ def _run(config, parser): if config.list_archived: lst_row = list_archived(config.database, config.prefix) if not lst_row: - print(f"✅ -> No '{config.prefix}.' view on '{config.database}'.") + print( + f"✅ -> {t('No view with this prefix on')}" + f" '{config.database}' : '{config.prefix}.'" + ) return 0 - print(f"ℹ {len(lst_row)} archived COW view(s) on '{config.database}':") + print( + f"ℹ {len(lst_row)} {t('archived COW view(s) on')}" + f" '{config.database}' :" + ) for row in lst_row: state = "active" if row["active"] else "inactive" print( @@ -170,54 +176,59 @@ def _run(config, parser): f" {row['key']} ({state}, {row['arch_bytes']} B)" ) print( - " Their arch is intact. Restore them all with --restore, once" - " the module view they shadow has the shape they expect." + f" {t('Their arch is intact. Restore them all with --restore,')}" + f" {t('once the module view they shadow has the right shape.')}" ) return 0 if config.restore: count = restore(config.database, config.prefix) - print(f"✅ -> {count} COW view(s) restored on '{config.database}'.") + print( + f"✅ -> {count} {t('COW view(s) restored on')} '{config.database}'." + ) return 0 if not config.target_version: parser.error("--target_version is required unless --restore is used") if not os.path.isdir(config.target_version): print( - f"❌ Target version directory '{config.target_version}' not found." + f"❌ {t('Target version directory not found')} :" + f" '{config.target_version}'" ) - return 1 + return 2 lst_at_risk, _, _ = analyse(config.database, config.target_version) if not lst_at_risk: - print("✅ -> No website COW view to neutralize.") + print(f"✅ -> {t('No website COW view to neutralize.')}") return 0 print( - f"⚠️ {len(lst_at_risk)} website COW view(s) would break the bump to" - f" {config.target_version}:" + f"⚠️ {len(lst_at_risk)} {t('website COW view(s) would break the bump')}" + f" {t('to')} {config.target_version} :" ) lst_view_id = [] for view_id, key, mode, target_mode, website_id, reason in lst_at_risk: lst_view_id.append(view_id) print( f" - id={view_id} website={website_id} {key}" - f" : {mode} -> {target_mode} ({reason})" + f" : {mode} -> {target_mode} ({t(reason)})" ) if not config.apply: print( - "ℹ Dry-run. Add --apply to rename their key to" - f" '{config.prefix}.' and deactivate them. Reversible with" - " --restore; the arch stays in database." + f"ℹ {t('Dry-run. Add --apply to rename their key to')}" + f" '{config.prefix}.' {t('and deactivate them. Reversible')}" + f" {t('with --restore; the arch stays in database.')}" ) - return 0 + # 1 = des copies sont à neutraliser. Le pilote lisait une phrase + # anglaise pour le savoir ; il devenait aveugle une fois traduite. + return 1 count = neutralize(config.database, lst_view_id, config.prefix) print( - f"✅ -> {count} COW view(s) neutralized (key prefixed with" - f" '{config.prefix}.', deactivated). The arch is kept as an archive;" - " use --restore to undo." + f"✅ -> {count} {t('COW view(s) neutralized (key prefixed with')}" + f" '{config.prefix}.', {t('deactivated). The arch is kept as an')}" + f" {t('archive; use --restore to undo.')}" ) return 0 diff --git a/script/odoo/migration/reset_stale_cow_views.py b/script/odoo/migration/reset_stale_cow_views.py index 13cd44e..8c7935e 100755 --- a/script/odoo/migration/reset_stale_cow_views.py +++ b/script/odoo/migration/reset_stale_cow_views.py @@ -61,6 +61,17 @@ import re import subprocess import sys +sys.path.append( + os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) +) + +try: + from script.todo.todo_i18n import t +except Exception: # pragma: no cover - repli si i18n indisponible + + def t(key: str) -> str: + return key + def run_psql(database, sql): """Run a statement and return stdout, raising on failure.""" @@ -142,7 +153,7 @@ def require_lxml(): return etree except ImportError: sys.exit( - "❌ lxml is required to resolve the xpath expressions.\n" + f"❌ {t('lxml is required to resolve the xpath expressions.')}\n" " Run this with an interpreter that has it, e.g.\n" " ./.venv.erplibre/bin/python3 " + os.path.relpath(__file__) ) @@ -294,37 +305,37 @@ def main(): findings = analyse(config.database) if not findings: - print("✅ No COW copy has drifted from its module view.") + print(f"✅ {t('No COW copy has drifted from its module view.')}") return 0 print( - f"⚠️ {len(findings)} COW copy(ies) drifted from their module view" - f" in {config.database}" + f"⚠️ {len(findings)} {t('COW copy(ies) drifted from their module')}" + f" {t('view in')} {config.database}" ) print( - " Odoo surfaces this when the module view is rewritten (a version" - " bump) or when the page is rendered.\n" + f" {t('Odoo surfaces this when the module view is rewritten (a')}" + f" {t('version bump) or when the page is rendered.')}\n" ) for cow_view, module_view, broken in findings: twin = ( f"module id={module_view['id']}" if module_view - else "NO module view with this key" + else t("NO module view with this key") ) print( f" id={cow_view['id']} key={cow_view['key']}" f" website_id={cow_view['website_id']} [{twin}]" ) for child_id, expr in broken: - print(f" child {child_id} cannot apply: {expr}") + print(f" {t('child')} {child_id} {t('cannot apply')}: {expr}") if module_view: show_diff(module_view, cow_view) print() if not config.reset: print( - "Nothing changed. Re-run with --reset --apply to reset a" - " copy onto its module view." + f"{t('Nothing changed. Re-run with --reset --apply to')}" + f" {t('reset a copy onto its module view.')}" ) return 1 @@ -338,25 +349,26 @@ def main(): continue if not module_view: print( - f"⏭ {cow_view['key']}: no module view to reset onto," - " skipped." + f"⏭ {cow_view['key']} :" + f" {t('no module view to reset onto, skipped.')}" ) continue if not config.apply: print( - f"[dry-run] would reset id={cow_view['id']}" - f" ({cow_view['key']}) onto id={module_view['id']}" + f"[{t('dry-run')}] {t('would reset')} id={cow_view['id']}" + f" ({cow_view['key']}) {t('onto')} id={module_view['id']}" ) continue path = backup(config.database, cow_view, directory) reset(config.database, cow_view, module_view) done += 1 - print(f"✅ reset id={cow_view['id']} ({cow_view['key']})") - print(f" previous arch saved to {path}") + print(f"✅ {t('reset')} id={cow_view['id']} ({cow_view['key']})") + print(f" {t('previous arch saved to')} {path}") if config.apply and done: print( - f"\n{done} copy(ies) reset. Re-apply any real customisation as an" - " INHERITING view, not a copy, so it cannot go stale again." + f"\n{done} {t('copy(ies) reset. Re-apply any real customisation')}" + f" {t('as an INHERITING view, not a copy, so it cannot go stale')}" + f" {t('again.')}" ) return 0 diff --git a/script/odoo/migration/snapshot_cow_views.py b/script/odoo/migration/snapshot_cow_views.py index a8f839b..efa57bc 100755 --- a/script/odoo/migration/snapshot_cow_views.py +++ b/script/odoo/migration/snapshot_cow_views.py @@ -32,6 +32,18 @@ import os import subprocess import sys +sys.path.append( + os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) +) + +try: + from script.todo.todo_i18n import t +except Exception: # pragma: no cover - repli si i18n indisponible + + def t(key: str) -> str: + return key + + # Columns worth recording. ir_ui_view does not expose the same set across 12.0 # to 18.0, so the query keeps only those that actually exist. WANTED_COLUMN = [ @@ -115,7 +127,7 @@ def save(database, label, output_dir): file_path = os.path.join(directory, f"{label}.json") with open(file_path, "w", encoding="utf-8") as f: json.dump(payload, f, indent=2, ensure_ascii=False) - print(f"✅ -> {len(lst_view)} COW view(s) recorded in {file_path}") + print(f"✅ -> {len(lst_view)} {t('COW view(s) recorded in')} {file_path}") return file_path @@ -136,18 +148,18 @@ def diff(path_before, path_after): common = sorted(set(map_before) & set(map_after)) print( - f"📊 {before.get('label')} ({before.get('count')} views)" - f" -> {after.get('label')} ({after.get('count')} views)" + f"📊 {before.get('label')} ({before.get('count')} {t('views')})" + f" -> {after.get('label')} ({after.get('count')} {t('views')})" ) if removed: - print(f"❌ {len(removed)} COW view(s) disappeared:") + print(f"❌ {len(removed)} {t('COW view(s) disappeared')} :") for view_id in removed: view = map_before[view_id] print(f" - id={view_id} {view.get('key')}") if added: - print(f"➕ {len(added)} COW view(s) appeared:") + print(f"➕ {len(added)} {t('COW view(s) appeared')} :") for view_id in added: view = map_after[view_id] print(f" - id={view_id} {view.get('key')} ({view.get('mode')})") @@ -160,8 +172,8 @@ def diff(path_before, path_after): if old.get(field) != new.get(field): if field == "arch_md5": lst_field.append( - f"arch rewritten ({old.get('arch_len')} ->" - f" {new.get('arch_len')} chars)" + f"{t('arch rewritten')} ({old.get('arch_len')} ->" + f" {new.get('arch_len')} {t('chars')})" ) else: lst_field.append( @@ -171,14 +183,14 @@ def diff(path_before, path_after): lst_changed.append((view_id, new.get("key"), lst_field)) if lst_changed: - print(f"✏️ {len(lst_changed)} COW view(s) changed:") + print(f"✏️ {len(lst_changed)} {t('COW view(s) changed')} :") for view_id, key, lst_field in lst_changed: print(f" - id={view_id} {key}") for change in lst_field: print(f" {change}") if not (removed or added or lst_changed): - print("✅ -> No change on the website COW views.") + print(f"✅ -> {t('No change on the website COW views.')}") return 0 diff --git a/script/todo/todo_upgrade.py b/script/todo/todo_upgrade.py index 5c6e72f..6b77a99 100755 --- a/script/todo/todo_upgrade.py +++ b/script/todo/todo_upgrade.py @@ -1160,7 +1160,7 @@ class TodoUpgrade: # TODO update dev environment for git project # TODO Redeploy new production after upgrade # 2 upgrades version = 5 environnement. 0-prod init, 1-dev init, 2-dev01, 3-dev02, 4-prod final - print("Welcome to Odoo database upgrade processus with ERPLibre 🤖") + print(t("Welcome to the Odoo database upgrade with ERPLibre") + " 🤖") self.lst_command_executed = [] self.dct_module_per_version = {} self.dct_module_per_dct_version_path = {} @@ -1195,10 +1195,12 @@ class TodoUpgrade: self.file_path = self.dct_progression["migration_file"] else: print("") - print("Select the zip file of you database backup.") + print(t("Select the zip file of your database backup.")) self.file_path = input( - "💬 Give the path of file, or empty to use a File Browser, or type 'remote' to download from production : " + f"💬 {t('Give the path of the file, or empty to use a file')}" + f" {t('browser, or type')} 'remote'" + f" {t('to download from production')} : " ) if not self.file_path.strip(): self.file_path = None @@ -1214,19 +1216,22 @@ class TodoUpgrade: ) if status: _logger.error( - "Cannot retrieve database from remote, please retry migration." + t( + "Cannot retrieve the database from remote, please" + " retry the migration." + ) ) return self.dct_progression["migration_file"] = self.file_path self.write_config() - print(f"✅ Open file {self.file_path}") + print(f"✅ {t('Open file')} {self.file_path}") with zipfile.ZipFile(self.file_path, "r") as zip_ref: manifest_file_1 = zip_ref.open("manifest.json") json_manifest_file_1 = json.load(manifest_file_1) odoo_actual_version = json_manifest_file_1.get("version") - print(f"✅ Detect version Odoo CE '{odoo_actual_version}'.") + print(f"✅ {t('Detected Odoo CE version')} '{odoo_actual_version}'.") # print("What is your actual Odoo version?") lst_version, lst_version_installed, odoo_installed_version = ( @@ -1243,7 +1248,7 @@ class TodoUpgrade: if "target_odoo_version" in self.dct_progression: odoo_target_version = self.dct_progression["target_odoo_version"] else: - print("💬 Which version do you want to upgrade to?") + print(f"💬 {t('Which version do you want to upgrade to?')}") odoo_target_version = None cmd_no_found = True while cmd_no_found: @@ -1258,7 +1263,7 @@ class TodoUpgrade: except ValueError: pass if cmd_no_found: - print("Commande non trouvée 🤖!") + print(t("Command not found") + " 🤖!") self.dct_progression["target_odoo_version"] = odoo_target_version self.write_config() @@ -1277,7 +1282,7 @@ class TodoUpgrade: self.dct_progression["lst_module_per_version_origin"] = lst_module # TODO need support minor version, example 18.2, the .2 (no need for OCE OCB) - print("✨ Show documentation version :") + print(f"✨ {t('Documentation for this version')} :") # TODO Generate it locally and show it if asked for next_version in range_version: @@ -1287,10 +1292,10 @@ class TodoUpgrade: # ⚠️ ℹ 💬 ❗ 🔷 ✨ 🟦 🔹 🔵 ⟳ ⧖ ⚙ ✔ ✅ ❌ ⏵ ⏸ ⏹ ◆ ◇ … ➤ ⚑ ★ ☆ ☰ ⬍ ⍟ ⊗ ⌘ ⏻ ⍰ msg = "0 - Inspect zip" - print(f"🔷 {msg}") + self.print_step(msg) self.add_comment_progression(msg) - print("✅ -> Search odoo version") - print("✅ -> Find good environment, read the .zip file") + print(f"✅ -> {t('Search the Odoo version')}") + print(f"✅ -> {t('Find the right environment, read the .zip file')}") is_state_4_reach_open_upgrade = self.dct_progression.get( "state_4_reach_open_upgrade" @@ -1311,7 +1316,8 @@ class TodoUpgrade: "odoo", "" ).replace(".0", "") want_continue = input( - f"💬 Would you like to install '{odoo_version_to_install}' (y/Y) : " + f"💬 {t('Would you like to install')}" + f" '{odoo_version_to_install}' (y/Y) : " ) if want_continue.strip().lower() != "y": return @@ -1321,7 +1327,8 @@ class TodoUpgrade: if not os.path.isfile(FILENAME_ODOO_VERSION): print( - "⚠️ You need an installed system before continue, check your Odoo installation." + f"⚠️ {t('You need an installed system before')}" + f" {t('continuing, check your Odoo installation.')}" ) return @@ -1333,7 +1340,7 @@ class TodoUpgrade: # self.dct_progression["state_0_switch_odoo"] = True # self.write_config() - print("✅ -> Install environment if missing") + print(f"✅ -> {t('Install the environment if missing')}") if not self.dct_progression.get("state_0_search_missing_module"): self.switch_odoo(odoo_actual_version) @@ -1374,15 +1381,16 @@ class TodoUpgrade: self.dct_progression["lst_module_duplicate"] = lst_module_duplicate self.write_config() if lst_module_missing or lst_module_duplicate: - print("Cannot setup environment to begin.") + print(t("Cannot set up the environment to begin.")) if lst_module_missing: - print("Missing module :") + print(f"{t('Missing module')} :") print(lst_module_missing) if lst_module_duplicate: - print("Duplicate module :") + print(f"{t('Duplicate module')} :") print(lst_module_duplicate) want_continue = input( - "💬 Detect error missing/duplicate module init, do you want to continue? (Y/N): " + f"💬 {t('Missing or duplicate module detected at init,')}" + f" {t('do you want to continue?')} (Y/N) : " ) if want_continue.strip().lower() != "y": return @@ -1393,21 +1401,23 @@ class TodoUpgrade: # TODO fill from config lst_module_missing = [] - print("✅ -> Search missing module") + print(f"✅ -> {t('Search missing modules')}") print( - "❌ -> Install missing module, do a research or ask to uninstall it (can break data)" + f"❌ -> {t('Install the missing modules, search for them or')}" + f" {t('ask to uninstall them (can break data)')}" ) msg = "1 - Import database from zip" - print(f"🔷 {msg}") + self.print_step(msg) self.add_comment_progression(msg) database_name = self.dct_progression.get("config_database_name") if not database_name: database_name = ( input( - f"💬 Witch database name do you want to work with? Default ({default_database_name}) : " + f"💬 {t('Which database name do you want to work with?')}" + f" {t('Default')} ({default_database_name}) : " ).strip() or default_database_name ) @@ -1416,7 +1426,7 @@ class TodoUpgrade: do_neutralize = False if not self.dct_progression.get("state_1_neutralize_database"): - print("[1] Ignore neutralize database") + print(f"[1] {t('Ignore the database neutralization')}") wait_continue = ( self.ask_gate( "💬 " @@ -1432,20 +1442,22 @@ class TodoUpgrade: self.dct_progression["config_database_name"] = database_name self.write_config() - print(f"★ Work with database '{database_name}'") + print(f"★ {t('Working with database')} '{database_name}'") if not self.dct_progression.get("state_1_restore_database"): file_name = os.path.basename(self.file_path) image_db_file_path = os.path.join("image_db", file_name) str_will_copy = ( - f"🤖 will copy '{self.file_path}' to '{image_db_file_path}'" + f"🤖 {t('will copy')} '{self.file_path}'" + f" {t('to')} '{image_db_file_path}'" ) if not shutil._samefile(self.file_path, image_db_file_path): do_copy = False if os.path.exists(image_db_file_path): status_overwrite_image_db = input( f"{str_will_copy}, " - f"a file already exist, do you want to continue (y/Y) : " + f"{t('a file already exists, do you want to')}" + f" {t('continue?')} (y/Y) : " ).strip() if status_overwrite_image_db.lower() == "y": do_copy = True @@ -1469,16 +1481,18 @@ class TodoUpgrade: self.dct_progression["state_1_restore_database"] = True self.write_config() - print("✅ -> Restore database") + print(f"✅ -> {t('Restore the database')}") already_update_state_1 = False if not self.dct_progression.get("state_1_update_all"): print( - "[1] Update all addons before neutralize (already neutralize by odoo if supported)" + f"[1] {t('Update all addons before neutralizing (already')}" + f" {t('neutralized by Odoo if supported)')}" ) wait_continue = ( input( - "💬 Do you need to upgrade before a database neutralization, press to ignore : " + f"💬 {t('Do you need to upgrade before neutralizing the')}" + f" {t('database? Press enter to ignore')} : " ) .strip() .lower() @@ -1501,9 +1515,11 @@ class TodoUpgrade: self.dct_progression["state_2_done_early"] = True self.write_config() - print("✅ -> Update database before neutralize by module") + print( + f"✅ -> {t('Update the database before neutralizing, by module')}" + ) - print("✅ -> Neutralize database") + print(f"✅ -> {t('Neutralize the database')}") if do_neutralize: status, cmd_executed = self.todo_upgrade_execute( f"./script/addons/update_prod_to_dev.sh {database_name}", @@ -1541,7 +1557,7 @@ class TodoUpgrade: self.read_uninstall_module_list(start_version, database_name) ) if lst_uninstall_reason: - print("✨ Modules to uninstall before migration :") + print(f"✨ {t('Modules to uninstall before the migration')} :") self.print_uninstall_reason(lst_uninstall_reason) if config_state_1_uninstall_module: @@ -1562,9 +1578,9 @@ class TodoUpgrade: self.write_config() - print("✅ -> Uninstall module") + print(f"✅ -> {t('Uninstall modules')}") - print("✅ -> install module") + print(f"✅ -> {t('Install modules')}") if not is_state_4_reach_open_upgrade: lst_module_to_install = [] if config_state_1_install_module: @@ -1583,7 +1599,7 @@ class TodoUpgrade: self.write_config() msg = "2 - Succeed update all addons" - print(f"🔷 {msg}") + self.print_step(msg) self.add_comment_progression(msg) if self.needs_update_all(self.dct_progression, already_update_state_1): @@ -1614,11 +1630,14 @@ class TodoUpgrade: # ne remplace pas la possibilité de regarder maintenant. Les copies # sont visibles ici, et les neutraliser ici vaut pour tous les paliers # — chaque base de palier est un clone de celle-ci. - if output and "will break when moving to" in "\n".join(output): + # Le code de sortie, pas le texte : 1 = des copies casseront. + # Chercher une phrase anglaise dans la sortie rendait cette invite + # muette dès que l'outil parlait français. + if status == 1: self.prompt_cow_prediction(database_name, start_version + 1) msg = "3 - Clean up database before data migration" - print(f"🔷 {msg}") + self.print_step(msg) self.add_comment_progression(msg) if not self.dct_progression.get("state_3_install_clean_database"): @@ -1632,7 +1651,8 @@ class TodoUpgrade: if not self.dct_progression.get("state_3_clean_database"): print( - "✨ Aller dans «configuration/Technique/Nettoyage.../Purger» les modules obsolètes" + f"✨ {t('Go to Settings / Technical / Cleanup... / Purge and')}" + f" {t('purge the obsolete modules')}" ) status = self.ask_gate( "💬 Did you finish to clean database? Press y/Y to open" @@ -1642,7 +1662,9 @@ class TodoUpgrade: if status.lower().strip() == "y": self.todo.prompt_execute_selenium_and_run_db(database_name) - status = input("💬 Press to continue state.3 : ").strip() + status = input( + f"💬 {t('Press enter to continue step 3')} : " + ).strip() self.dct_progression["state_3_clean_database"] = True self.write_config() @@ -1650,7 +1672,7 @@ class TodoUpgrade: self.install_OCA_odoo_module_migrator() msg = "4 - Upgrade version with OpenUpgrade" - print(f"🔷 {msg}") + self.print_step(msg) self.add_comment_progression(msg) self.dct_progression["state_4_reach_open_upgrade"] = True @@ -1748,7 +1770,9 @@ class TodoUpgrade: self.switch_odoo(next_version - 1) print( - f"⧖ -> Clone to odoo.'{next_version}', from '{database_name}' to '{database_name_upgrade}'." + f"⧖ -> {t('Cloning to Odoo')}{next_version}," + f" {t('from')} '{database_name}'" + f" {t('to')} '{database_name_upgrade}'." ) # Delete if exist database self.todo_upgrade_execute( @@ -1766,18 +1790,22 @@ class TodoUpgrade: # a missing or truncated database). if status: print( - f"❌ -> Clone to Odoo{next_version} FAILED (status" - f" {status}). Stopping: '{database_name_upgrade}' is" - " not usable." + f"❌ -> {t('Clone to Odoo')}{next_version}" + f" {t('FAILED (status')} {status})." + f" {t('Stopping:')} '{database_name_upgrade}'" + f" {t('is not usable.')}" ) return lst_clone_odoo[index] = True self.dct_progression["state_4_clone_odoo_lst"] = lst_clone_odoo self.write_config() - print(f"✅ -> Clone Odoo{next_version} done") + print(f"✅ -> {t('Clone done for Odoo')}{next_version}") else: - print(f"✅ -> Clone Odoo{next_version} - nothing") + print( + f"✅ -> {t('Clone already done for Odoo')}" + f"{next_version}" + ) option_comment += 1 msg = f"4.{index}.{chr(option_comment + 65)} - Uninstall module" @@ -1801,7 +1829,8 @@ class TodoUpgrade: ) if lst_detail: print( - f"✨ Modules to uninstall before Odoo{next_version} :" + f"✨ {t('Modules to uninstall before Odoo')}" + f"{next_version} :" ) self.print_uninstall_reason(lst_detail) lst_module_to_uninstall = list( @@ -1886,9 +1915,15 @@ class TodoUpgrade: lst_switch_odoo ) self.write_config() - print(f"✅ -> Switch Odoo{next_version} done with update") + print( + f"✅ -> {t('Switch done with update for Odoo')}" + f"{next_version}" + ) else: - print(f"✅ -> Switch Odoo{next_version} - nothing") + print( + f"✅ -> {t('Switch already done for Odoo')}" + f"{next_version}" + ) lst_state_4_module_migrate_code = self.dct_progression.get( "config_state_4_module_to_migrate_code", @@ -1954,17 +1989,19 @@ class TodoUpgrade: ) if lst_module_duplicate: - print(f"Duplicate module into odoo{next_version} : ") + print(f"{t('Duplicate module in Odoo')}{next_version} : ") print(lst_module_duplicate) input( - f"💬 Detect error duplicate module, manage this problem manually and press to continue." + f"💬 {t('Duplicate module error detected, handle it')}" + f" {t('manually then press enter to continue.')}" ) # if lst_module_missing_next_version and not lst_module_to_migrate: if lst_module_missing_next_version: # TODO support when lst_module_to_migrate is fill lst_module_to_migrate = [] print( - f"👹 Detect error missing module, missing module into odoo{next_version} :" + f"👹 {t('Missing module error detected, missing in')}" + f" Odoo{next_version} :" ) for index_missing_module, module_missing in enumerate( lst_module_missing_next_version @@ -1975,13 +2012,14 @@ class TodoUpgrade: print( f"[{index_missing_module}] {module_missing} - {old_path}" ) - print("[a] All list above") - print("[e] Add extra custom") + print(f"[a] {t('All of the list above')}") + print(f"[e] {t('Add an extra custom one')}") want_continue = ( input( - f"💬 Enumerate missing module separate by coma to delete it" - f". The others will be migrate : " + f"💬 {t('List the missing modules to delete,')}" + f" {t('separated by commas. The others will be')}" + f" {t('migrated')} : " ) .strip() .lower() @@ -2043,7 +2081,8 @@ class TodoUpgrade: if "e" in lst_want_continue: want_continue = ( input( - f"💬 Enumerate module name to delete, separate by coma : " + f"💬 {t('List the module names to delete,')}" + f" {t('separated by commas')} : " ) .strip() .lower() @@ -2096,15 +2135,17 @@ class TodoUpgrade: if lst_module_missing_last: print( - f"Error missing module : {lst_module_missing_last}" + f"{t('Missing module error')} :" + f" {lst_module_missing_last}" ) if lst_module_duplicate_last: print( - f"Error duplicate module : {lst_module_duplicate_last}" + f"{t('Duplicate module error')} :" + f" {lst_module_duplicate_last}" ) if lst_module_error_last: print( - f"Error error module : {lst_module_error_last}" + f"{t('Module error')} : {lst_module_error_last}" ) if lst_module_exist_last: @@ -2247,9 +2288,15 @@ class TodoUpgrade: ) self.write_config() - print(f"✅ -> Module upgrade Odoo{next_version} done") + print( + f"✅ -> {t('Module upgrade done for Odoo')}" + f"{next_version}" + ) else: - print(f"✅ -> Module upgrade Odoo{next_version} - nothing") + print( + f"✅ -> {t('Module upgrade already done for Odoo')}" + f"{next_version}" + ) option_comment += 1 msg = f"4.{index}.{chr(option_comment + 65)} - Fix migrate code" @@ -2292,8 +2339,8 @@ class TodoUpgrade: # problem the fix exists to prevent. if status: print( - f"❌ -> Fix migration Odoo{next_version} FAILED" - f" (status {status}):" + f"❌ -> {t('Migration fix for Odoo')}{next_version}" + f" {t('FAILED (status')} {status}) :" f" {file_path_fix_migration}" ) return @@ -2303,13 +2350,20 @@ class TodoUpgrade: lst_fix_migration_odoo ) self.write_config() - print(f"✅ -> Fix migration Odoo{next_version} done") + print( + f"✅ -> {t('Migration fix done for Odoo')}" + f"{next_version}" + ) else: print( - f"✅ -> Fix migration Odoo{next_version} - no fix to execute" + f"✅ -> {t('No migration fix to run for Odoo')}" + f"{next_version}" ) else: - print(f"✅ -> Fix migration Odoo{next_version} - nothing") + print( + f"✅ -> {t('Migration fix already done for Odoo')}" + f"{next_version}" + ) for path_git_clone_migrate in lst_path_git_clone_migrate: cmd = f"./script/code/git_commit_migration_addons_path.py --path {path_git_clone_migrate} --odoo_version {next_version}.0" @@ -2340,10 +2394,17 @@ class TodoUpgrade: ) self.todo_upgrade_execute(cmd_update_config) - print("🚸 Please, validate commits after code migration.") - print("ℹ To show repo status :\nmake repo_show_status") print( - f"🚸 Please, validate this path into config.conf : '{path_addons_openupgrade}'." + f"🚸 {t('Please validate the commits after the code')}" + f" {t('migration.')}" + ) + print( + f"ℹ {t('To show the repo status')} :" + "\nmake repo_show_status" + ) + print( + f"🚸 {t('Please validate this path in config.conf')} :" + f" '{path_addons_openupgrade}'." ) status = self.ask_gate( f"💬 {t('Press to continue')} {msg}" @@ -2401,15 +2462,17 @@ class TodoUpgrade: ) self.write_config() print( - f"\n❌ -> Database migration to Odoo{next_version}" - f" FAILED (status {status}).\n" - f" '{database_name_upgrade}' is now half migrated:" - " replaying the command on it would never recover, so" - " it is NOT offered.\n" - " The clone step has been reset. Fix the cause, then" - " relaunch the migration and answer [c] (continue):" - f" '{database_name_upgrade}' will be dropped and" - " rebuilt from the previous version before retrying." + f"\n❌ -> {t('Database migration to Odoo')}" + f"{next_version} {t('FAILED (status')} {status}).\n" + f" '{database_name_upgrade}'" + f" {t('is now half migrated: replaying the command on')}" + f" {t('it would never recover, so it is NOT offered.')}" + f"\n {t('The clone step has been reset. Fix the')}" + f" {t('cause, then relaunch the migration and answer')}" + f" [c] ({t('continue')}) :" + f" '{database_name_upgrade}'" + f" {t('will be dropped and rebuilt from the previous')}" + f" {t('version before retrying.')}" ) return @@ -2435,7 +2498,10 @@ class TodoUpgrade: status = ( input( - f"💬 Do you want to upgrade all{str_wait_next_version}? Press y/Y to upgrade all addons database : " + f"💬 {t('Do you want to upgrade all')}" + f"{str_wait_next_version} ?" + f" {t('Press y/Y to upgrade all addons of the')}" + f" {t('database')} : " ) .strip() .lower() @@ -2446,15 +2512,19 @@ class TodoUpgrade: f"./script/addons/update_addons_all.sh {database_name_upgrade}", ) - print(f"✅ -> Database upgrade Odoo{next_version} done") + print( + f"✅ -> {t('Database upgrade done for Odoo')}" + f"{next_version}" + ) # Update config without OCA_OpenUpgrade cmd_update_config = f"./script/git/git_repo_update_group.py && ./script/generate_config.sh" self.todo_upgrade_execute(cmd_update_config) - print("[y] Open server with Selenium") + print(f"[y] {t('Open the server with Selenium')}") status = ( input( - "💬 Do you want to test this upgrade? Choose or press to ignore it : " + f"💬 {t('Do you want to test this upgrade? Choose')}" + f" {t('or press enter to ignore it')} : " ) .strip() .lower() @@ -2465,39 +2535,44 @@ class TodoUpgrade: database_name_upgrade ) status = input( - f"💬 Press to continue 4.{index} : " + f"💬 {t('Press enter to continue')} 4.{index} : " ).strip() else: - print(f"✅ -> Database upgrade Odoo{next_version} - nothing") + print( + f"✅ -> {t('Database upgrade already done for Odoo')}" + f"{next_version}" + ) # # waiting_input = input("💬 Press any keyboard key to continue...") print("") msg = "5 - Cleaning up database after upgrade" - print(f"🔷 {msg}") + self.print_step(msg) self.add_comment_progression(msg) print( - "✨ Re-update i18n, purger data, tables (except mail_test and mail_test_full)" + f"✨ {t('Re-update i18n, purge the data and the tables')}" + f" ({t('except mail_test and mail_test_full')})" ) # waiting_input = input("💬print Press any keyboard key to continue...") msg = "6 - Migration finished" - print(f"🔷 {msg}") + self.print_step(msg) self.add_comment_progression(msg) cmd_backup_template = f"./odoo_bin.sh db --backup --database {database_name_upgrade} --restore_image" cmd_backup = f"{cmd_backup_template} {database_name_upgrade}_finish_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}" - print(f"✨ Can execute backup creation :\n{cmd_backup}") + print(f"✨ {t('A backup can be created')} :\n{cmd_backup}") status = input( - "💬 Press y/Y or write filename.zip to export or enter to continue : " + f"💬 {t('Press y/Y or type filename.zip to export, or')}" + f" {t('enter to continue')} : " ).strip() if status.lower(): if status.lower() != "y": cmd_backup = f"{cmd_backup_template} {status}" self.todo_upgrade_execute(cmd_backup) - status = input("💬 Test the migration, press y/Y : ") + status = input(f"💬 {t('Test the migration, press y/Y')} : ") if status.lower().strip() == "y": self.todo.prompt_execute_selenium_and_run_db(database_name_upgrade) @@ -2619,6 +2694,18 @@ class TodoUpgrade: wait_at_error=False, ) + def print_step(self, msg): + """Affiche un en-tête d'étape en traduisant son seul libellé. + + `msg` reste anglais : il part aussi dans le journal, que l'écran de + reprise relit. Traduire ce qui est ÉCRIT rendrait un journal + illisible pour l'autre langue, et l'étape 4 numérote ses en-têtes + (« 4.2.C - Install module ») — seule la partie après le tiret est + une phrase. + """ + prefix, sep, label = msg.partition(" - ") + print(f"🔷 {prefix}{sep}{t(label)}" if sep else f"🔷 {t(msg)}") + def show_cow_drift(self, database_name, next_version, mode="diff"): """Montre les copies COW à risque. Ne touche à rien. @@ -2719,7 +2806,9 @@ class TodoUpgrade: status, cmd_executed, output = self.todo_upgrade_execute( cmd, get_output=True, wait_at_error=False ) - if "No website COW view to neutralize" in "\n".join(output or []): + # 0 = rien à neutraliser. Lire une phrase anglaise dans la sortie + # faisait poser la question dès que l'outil parlait français. + if status == 0: return # « v » et « w » avant de répondre : la question demande de renoncer à @@ -3072,7 +3161,7 @@ class TodoUpgrade: if (status is None or status) and wait_at_error: database_name = self.database_from_command(cmd) while True: - print("[1] to redo the command") + print(f"[1] {t('to redo the command')}") if database_name: print( f"[2] {t('Check the COW views that drifted')}" @@ -3080,8 +3169,8 @@ class TodoUpgrade: ) wait_status = ( input( - "💬 Error detected, press to continue or ctrl+c to" - " stop : " + f"💬 {t('Error detected, press enter to continue or')}" + f" ctrl+c {t('to stop')} : " ) .strip() .lower() diff --git a/test/test_migration_tools_i18n.py b/test/test_migration_tools_i18n.py new file mode 100644 index 0000000..6d07c4a --- /dev/null +++ b/test/test_migration_tools_i18n.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python3 +# © 2021-2026 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +"""Les outils de migration parlaient anglais sur un système en français. + +Ils tournent en sous-processus depuis le pilote, et n'avaient aucun i18n : la +migration alternait donc les deux langues d'une ligne à l'autre. + +Deux choses à tenir, et la seconde est un piège : le pilote CHERCHAIT des +phrases anglaises dans leur sortie pour décider s'il devait poser une question +(« will break when moving to », « No website COW view to neutralize »). +Traduire ces phrases l'aurait rendu muet — sans erreur, sans trace. Le lien +passe désormais par le code de sortie, que la langue ne touche pas. +""" + +import ast +import os +import subprocess +import sys +import unittest + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# Les outils COW, ceux que la migration lance et dont la sortie est lue par un +# humain au milieu d'une migration de plusieurs heures. +TOOLS = ( + "check_cow_views.py", + "cow_drift.py", + "neutralize_cow_views.py", + "reset_stale_cow_views.py", + "snapshot_cow_views.py", +) + +# Ce qui n'est pas de la prose : du SQL, des chemins, des drapeaux. +NOT_PROSE = ("SELECT ", "UPDATE ", "./script/", "--", "id=", "