From 99092f165d5b3d93dfd7387042a229965694a461 Mon Sep 17 00:00:00 2001 From: Samuel Amen Ague Date: Tue, 23 Jun 2026 09:10:18 +0000 Subject: [PATCH 1/6] refactor(i18n): centralize shared i18n helper Signed-off-by: Samuel Amen Ague --- pycompiler_ark/Core/engine/registry.py | 10 ------- pycompiler_ark/Ui/Gui/UiConnection.py | 12 +------- pycompiler_ark/Ui/Gui/UiFeatures.py | 17 +++-------- pycompiler_ark/Ui/i18n.py | 15 ++++++++++ pycompiler_ark/engines/cx_freeze/__init__.py | 11 +------ pycompiler_ark/engines/nuitka/__init__.py | 11 +------ .../engines/pyinstaller/__init__.py | 11 +------ requirements.txt | 29 ++++++------------- todo.md | 5 +++- 9 files changed, 36 insertions(+), 85 deletions(-) diff --git a/pycompiler_ark/Core/engine/registry.py b/pycompiler_ark/Core/engine/registry.py index 20f5dbc..c78c916 100644 --- a/pycompiler_ark/Core/engine/registry.py +++ b/pycompiler_ark/Core/engine/registry.py @@ -53,16 +53,6 @@ _ENGINE_TR: dict[str, dict[str, Any]] = {} -def _declare_i18n(widget, **props) -> None: - if widget is None: - return - for key, value in props.items(): - try: - widget.setProperty(key, value) - except Exception: - pass - - def _iter_i18n_roots(engine: CompilerEngine): seen: set[int] = set() try: diff --git a/pycompiler_ark/Ui/Gui/UiConnection.py b/pycompiler_ark/Ui/Gui/UiConnection.py index 6144ea7..7fd5859 100644 --- a/pycompiler_ark/Ui/Gui/UiConnection.py +++ b/pycompiler_ark/Ui/Gui/UiConnection.py @@ -33,7 +33,7 @@ except Exception: QSvgRenderer = None # type: ignore[assignment] -from pycompiler_ark.Ui.i18n import show_language_dialog, translate +from pycompiler_ark.Ui.i18n import _declare_i18n, show_language_dialog, translate def _detect_system_color_scheme() -> str: @@ -217,16 +217,6 @@ def _setup_sidebar_logo(self) -> None: logo_label.setScaledContents(True) -def _declare_i18n(widget, **props) -> None: - if widget is None: - return - for key, value in props.items(): - try: - widget.setProperty(key, value) - except Exception: - pass - - def _auto_resize_for_screen(self) -> None: """Resize and center the window to fit the current screen safely.""" try: diff --git a/pycompiler_ark/Ui/Gui/UiFeatures.py b/pycompiler_ark/Ui/Gui/UiFeatures.py index 825d99c..818c85a 100644 --- a/pycompiler_ark/Ui/Gui/UiFeatures.py +++ b/pycompiler_ark/Ui/Gui/UiFeatures.py @@ -94,19 +94,10 @@ def select_nuitka_icon(self): def show_help_dialog(self): """Show the localized help dialog.""" try: - from pycompiler_ark.Ui.i18n import FALLBACK_EN, is_french_language - - if is_french_language(self): - tr = getattr(self, "_tr", None) - if isinstance(tr, dict): - help_title = tr.get("help_title", "Aide") - help_text = tr.get("help_text", FALLBACK_EN.get("help_text", "")) - else: - help_title = "Aide" - help_text = FALLBACK_EN.get("help_text", "") - else: - help_title = FALLBACK_EN.get("help_title", "Help") - help_text = FALLBACK_EN.get("help_text", "") + from pycompiler_ark.Ui.i18n import translate + + help_title = translate(self, "help_title", "Help") + help_text = translate(self, "help_text", "") except Exception: help_title = "Help" help_text = "" diff --git a/pycompiler_ark/Ui/i18n.py b/pycompiler_ark/Ui/i18n.py index a53526b..d0a2740 100644 --- a/pycompiler_ark/Ui/i18n.py +++ b/pycompiler_ark/Ui/i18n.py @@ -223,6 +223,21 @@ def translate(domain: object | None, key: str, default: str | None = None) -> st return default if default is not None else str(key) +def _declare_i18n(widget, **props) -> None: + """Attach i18n properties to a Qt object. + + This helper is centralised here so UI code, engines, and host widgets all use + the same property-setting behavior. + """ + if widget is None: + return + for key, value in props.items(): + try: + widget.setProperty(key, value) + except Exception: + pass + + # Public async Plugins with real-time caching and error handling diff --git a/pycompiler_ark/engines/cx_freeze/__init__.py b/pycompiler_ark/engines/cx_freeze/__init__.py index 3423576..961cf90 100644 --- a/pycompiler_ark/engines/cx_freeze/__init__.py +++ b/pycompiler_ark/engines/cx_freeze/__init__.py @@ -33,19 +33,10 @@ engine_register, translate, ) +from pycompiler_ark.Ui.i18n import _declare_i18n from pycompiler_ark.engine_sdk.utils import log_with_level -def _declare_i18n(widget, **props) -> None: - if widget is None: - return - for key, value in props.items(): - try: - widget.setProperty(key, value) - except Exception: - pass - - @engine_register class CXFreezeEngine(CompilerEngine): """ diff --git a/pycompiler_ark/engines/nuitka/__init__.py b/pycompiler_ark/engines/nuitka/__init__.py index 6dec765..ad32b7d 100644 --- a/pycompiler_ark/engines/nuitka/__init__.py +++ b/pycompiler_ark/engines/nuitka/__init__.py @@ -32,19 +32,10 @@ engine_register, translate, ) +from pycompiler_ark.Ui.i18n import _declare_i18n from pycompiler_ark.engine_sdk.utils import log_with_level -def _declare_i18n(widget, **props) -> None: - if widget is None: - return - for key, value in props.items(): - try: - widget.setProperty(key, value) - except Exception: - pass - - @engine_register class NuitkaEngine(CompilerEngine): """ diff --git a/pycompiler_ark/engines/pyinstaller/__init__.py b/pycompiler_ark/engines/pyinstaller/__init__.py index 0e19f8b..4038251 100644 --- a/pycompiler_ark/engines/pyinstaller/__init__.py +++ b/pycompiler_ark/engines/pyinstaller/__init__.py @@ -33,19 +33,10 @@ engine_register, translate, ) +from pycompiler_ark.Ui.i18n import _declare_i18n from pycompiler_ark.engine_sdk.utils import log_with_level -def _declare_i18n(widget, **props) -> None: - if widget is None: - return - for key, value in props.items(): - try: - widget.setProperty(key, value) - except Exception: - pass - - @engine_register class PyInstallerEngine(CompilerEngine): """ diff --git a/requirements.txt b/requirements.txt index 1902603..0ba2733 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,33 +1,22 @@ # PyCompiler ARK (ARK++) — Runtime requirements # Core GUI framework (Qt for Python) -# Matrix: -# - Python 3.11 => keep PySide6/shiboken6 < 6.5 as requested -# - Python 3.12 => stay on the Qt 6.7 line for better stability -# - Python >= 3.13 => PySide6/shiboken6 >= 6.8 (adds 3.13 support) -# Note: -# - I found official support for Python 3.11 on the 6.4 line. -# - I did not find a reliable official source proving that a given PyPI wheel avoids SSE/SSE2/SSE4.x requirements. -# - So the < 6.5 pin is a compatibility choice, not a hard guarantee for very old CPUs. -# - On such machines, prefer distro packages or a source build of PySide6/shiboken6. -PySide6>=6.8,<6.11; python_version >= "3.13" -shiboken6>=6.8,<6.11; python_version >= "3.13" -PySide6>=6.6,<6.8; python_version >= "3.12" and python_version < "3.13" -shiboken6>=6.6,<6.8; python_version >= "3.12" and python_version < "3.13" -PySide6>=6.4,<6.5; python_version >= "3.11" and python_version < "3.12" -shiboken6>=6.4,<6.5; python_version >= "3.11" and python_version < "3.12" +Pyside6 +shiboken6 +PySide6 +shiboken6 +PySide6 +shiboken6 # System utilities psutil -# Configuration and data formats -PyYAML>=5.4.1,<7.0.0 -# tomli only needed on Python < 3.11 (tomllib builtin on 3.11+) -tomli>=2.0.1,<3.0.0; python_version < "3.11" +# Data +PyYAML +tomli jsonschema # CLI - click rich colorama \ No newline at end of file diff --git a/todo.md b/todo.md index d96f033..5b4fb10 100644 --- a/todo.md +++ b/todo.md @@ -1,2 +1,5 @@ # refactor -- [] refactor chaque module pour plus de lisibilité et reduction du code pour une meilleur maintenance. \ No newline at end of file +- [] cnetraliser toute les `def _declare_i18n` dans Ui/i18n.py cat il sont tous identique.recherche dabord parotut avant de faire la centralisation. +- [] appliquer le i18n à help_text dans uifeatures. +- [] les engine ne doivent avoir access que a leur sdk de meme que les plugins. +- [] pour tous les i18n analyser pour voir si il nest pas possible de inteegre le declare i18n dna stranslate pour que on est plus beoin de declarer et on fera juste `translate(self.id, key, default)`. \ No newline at end of file From 9954e8284c5893615abff92cab4e477f7bee1d6e Mon Sep 17 00:00:00 2001 From: Samuel Amen Ague Date: Tue, 23 Jun 2026 10:28:04 +0000 Subject: [PATCH 2/6] i18n: centralize host translation flow Signed-off-by: Samuel Amen Ague --- .../Ui/Gui/IdeLikeGui/connections.py | 96 +++++-------- pycompiler_ark/Ui/Gui/UiConnection.py | 31 +---- pycompiler_ark/Ui/Gui/UiFeatures.py | 11 ++ pycompiler_ark/Ui/i18n.py | 130 +++++++++++++++--- 4 files changed, 153 insertions(+), 115 deletions(-) diff --git a/pycompiler_ark/Ui/Gui/IdeLikeGui/connections.py b/pycompiler_ark/Ui/Gui/IdeLikeGui/connections.py index 389268b..4d78e37 100644 --- a/pycompiler_ark/Ui/Gui/IdeLikeGui/connections.py +++ b/pycompiler_ark/Ui/Gui/IdeLikeGui/connections.py @@ -48,7 +48,6 @@ _apply_initial_theme, _auto_resize_for_screen, _connect_dialogs_to_app, - _declare_i18n, ) from pycompiler_ark.Ui.Gui.UiConnection import ( _connect_signals as _connect_classic_signals, @@ -59,6 +58,7 @@ show_theme_dialog, themed_svg_icon, ) +from pycompiler_ark.Ui.i18n import translate def _prime_expected_attrs(self) -> None: @@ -220,34 +220,6 @@ def _find(cls, name: str): ) except Exception: self.status_hint = None - _declare_i18n(self.btn_select_folder, i18n_text_key="select_folder", i18n_tooltip_key="tt_select_folder") - _declare_i18n(self.venv_button, i18n_text_key="venv_button", i18n_tooltip_key="tt_venv_button") - _declare_i18n(self.venv_label, i18n_text_key="venv_label", i18n_text_system_key="venv_label_system", i18n_system_attr="use_system_python") - _declare_i18n(self.label_folder, i18n_text_key="label_folder") - _declare_i18n(self.label_workspace_status, i18n_text_key="label_workspace_status", i18n_none_key="label_workspace_status_none", i18n_format_attr="workspace_dir") - _declare_i18n(self.label_workspace_section, i18n_text_key="label_workspace_section") - _declare_i18n(self.label_files_section, i18n_text_key="label_files_section") - _declare_i18n(self.label_tools, i18n_text_key="label_tools") - _declare_i18n(self.label_options_section, i18n_text_key="label_options_section") - _declare_i18n(self.label_logs_section, i18n_text_key="label_logs_section") - _declare_i18n(self.label_progress, i18n_text_key="label_progress") - _declare_i18n(self.btn_select_files, i18n_text_key="select_files", i18n_tooltip_key="tt_select_files") - _declare_i18n(self.btn_remove_file, i18n_text_key="btn_remove_file", i18n_tooltip_key="tt_remove_file") - _declare_i18n(self.btn_clear_workspace, i18n_text_key="btn_clear_workspace", i18n_tooltip_key="tt_clear_workspace") - _declare_i18n(self.btn_bc_loader, i18n_text_key="bc_loader", i18n_tooltip_key="tt_bc_loader") - _declare_i18n(self.compile_btn, i18n_text_key="build_all", i18n_tooltip_key="tt_build_all") - _declare_i18n(self.cancel_btn, i18n_text_key="cancel_all", i18n_tooltip_key="tt_cancel_all") - _declare_i18n(self.btn_help, i18n_text_key="help", i18n_tooltip_key="tt_help") - _declare_i18n(self.btn_suggest_deps, i18n_text_key="suggest_deps", i18n_tooltip_key="tt_suggest_deps") - _declare_i18n(self.activity_btn_deps, i18n_tooltip_key="tt_suggest_deps") - _declare_i18n(self.btn_bc_loader, i18n_tooltip_key="tt_bc_loader") - _declare_i18n(self.btn_acasl_loader, i18n_tooltip_key="tt_bc_loader") - _declare_i18n(self.btn_show_stats, i18n_text_key="show_stats", i18n_tooltip_key="tt_show_stats") - _declare_i18n(self.advanced_cfg_btn, i18n_text_key="advanced_config") - _declare_i18n(self.select_lang, i18n_text_key="choose_language_button", i18n_text_system_key="choose_language_system_button", i18n_system_attr="language_pref", i18n_tooltip_key="tt_select_lang") - _declare_i18n(self.select_theme, i18n_text_key="choose_theme_button", i18n_text_system_key="choose_theme_system_button", i18n_system_attr="theme", i18n_tooltip_key="tt_select_theme") - _declare_i18n(self.status_hint, i18n_text_key="status_ready") - _declare_i18n(self.file_filter_input, i18n_placeholder_key="file_filter_placeholder") _setup_status_bar(self) @@ -344,104 +316,98 @@ def _setup_more_tools_menu(self) -> None: return try: - _declare_i18n(more_btn, i18n_tooltip_key="tt_more_actions") + more_btn.setToolTip(translate(self, "tt_more_actions", more_btn.toolTip())) menu = QMenu(more_btn) self._ide_more_tools_menu = menu - act_workspace = QAction(menu) + act_workspace = QAction(translate(self, "action_select_workspace", "Select Workspace"), menu) + act_workspace.setObjectName("action_select_workspace") act_workspace.triggered.connect( lambda: getattr(self, "select_workspace", lambda: None)() ) - _declare_i18n(act_workspace, i18n_text_key="action_select_workspace") menu.addAction(act_workspace) - act_init = QAction(menu) + act_init = QAction(translate(self, "action_init_project", "Initialise Project"), menu) + act_init.setObjectName("action_init_project") act_init.triggered.connect( lambda: getattr(self, "open_init_workspace_dialog", lambda: None)() ) - _declare_i18n(act_init, i18n_text_key="action_init_project") menu.addAction(act_init) - act_venv = QAction(menu) + act_venv = QAction(translate(self, "action_select_venv", "Select Venv"), menu) + act_venv.setObjectName("action_select_venv") act_venv.triggered.connect( lambda: getattr(self, "select_venv_manually", lambda: None)() ) - _declare_i18n(act_venv, i18n_text_key="action_select_venv") menu.addAction(act_venv) - act_add_files = QAction(menu) + act_add_files = QAction(translate(self, "action_add_files", "Add Files"), menu) + act_add_files.setObjectName("action_add_files") act_add_files.triggered.connect( lambda: getattr(self, "select_files_manually", lambda: None)() ) - _declare_i18n(act_add_files, i18n_text_key="action_add_files") menu.addAction(act_add_files) - act_clear_workspace = QAction(menu) + act_clear_workspace = QAction(translate(self, "btn_clear_workspace", "Clear Workspace"), menu) + act_clear_workspace.setObjectName("btn_clear_workspace") act_clear_workspace.triggered.connect( lambda: getattr(self, "clear_workspace", lambda: None)() ) - _declare_i18n(act_clear_workspace, i18n_text_key="btn_clear_workspace") menu.addAction(act_clear_workspace) - act_stats = QAction(menu) + act_stats = QAction(translate(self, "show_stats", "Show Stats"), menu) + act_stats.setObjectName("show_stats") act_stats.triggered.connect( lambda: getattr(self, "show_statistics", lambda: None)() ) - _declare_i18n(act_stats, i18n_text_key="show_stats") menu.addAction(act_stats) menu.addSeparator() - act_language = QAction(menu) + act_language = QAction( + translate(self, "choose_language_button", "Language"), menu + ) + act_language.setObjectName("act_language") act_language.triggered.connect( lambda: getattr(self, "show_language_dialog", lambda: None)() ) - _declare_i18n( - act_language, - i18n_text_key="choose_language_button", - i18n_text_system_key="choose_language_system_button", - i18n_system_attr="language_pref", - ) menu.addAction(act_language) - act_theme = QAction(menu) + act_theme = QAction(translate(self, "choose_theme_button", "Theme"), menu) + act_theme.setObjectName("act_theme") act_theme.triggered.connect(lambda: _open_theme_dialog(self)) - _declare_i18n( - act_theme, - i18n_text_key="choose_theme_button", - i18n_text_system_key="choose_theme_system_button", - i18n_system_attr="theme", - ) menu.addAction(act_theme) menu.addSeparator() - act_advanced = QAction(menu) + act_advanced = QAction(translate(self, "advanced_config", "Advanced Config"), menu) + act_advanced.setObjectName("advanced_config") act_advanced.triggered.connect( lambda: getattr(self, "open_advanced_config_editor", lambda: None)() ) - _declare_i18n(act_advanced, i18n_text_key="advanced_config") menu.addAction(act_advanced) - act_lock = QAction(menu) + act_lock = QAction(translate(self, "lock_manager", "Lock Manager"), menu) + act_lock.setObjectName("lock_manager") act_lock.triggered.connect( lambda: getattr(self, "open_lock_dialog", lambda: None)() ) - _declare_i18n(act_lock, i18n_text_key="lock_manager") menu.addAction(act_lock) - act_save_engines = QAction(menu) + act_save_engines = QAction( + translate(self, "save_engine_configs", "Save Engine Configs"), menu + ) + act_save_engines.setObjectName("save_engine_configs") act_save_engines.triggered.connect( lambda: getattr(self, "save_all_engine_configs", lambda: None)() ) - _declare_i18n(act_save_engines, i18n_text_key="save_engine_configs") menu.addAction(act_save_engines) - act_help = QAction(menu) + act_help = QAction(translate(self, "help", "Help"), menu) + act_help.setObjectName("help") act_help.triggered.connect( lambda: getattr(self, "show_help_dialog", lambda: None)() ) - _declare_i18n(act_help, i18n_text_key="help") menu.addAction(act_help) self._ide_more_menu_actions = { @@ -640,7 +606,7 @@ def _setup_status_bar(self) -> None: try: self.status_hint = QLabel("Ready") self.status_hint.setObjectName("status_hint") - _declare_i18n(self.status_hint, i18n_text_key="status_ready") + self.status_hint.setText(translate(self, "status_ready", "Ready")) self.statusbar.addPermanentWidget(self.status_hint, 1) except Exception: pass diff --git a/pycompiler_ark/Ui/Gui/UiConnection.py b/pycompiler_ark/Ui/Gui/UiConnection.py index 7fd5859..cc28451 100644 --- a/pycompiler_ark/Ui/Gui/UiConnection.py +++ b/pycompiler_ark/Ui/Gui/UiConnection.py @@ -33,7 +33,7 @@ except Exception: QSvgRenderer = None # type: ignore[assignment] -from pycompiler_ark.Ui.i18n import _declare_i18n, show_language_dialog, translate +from pycompiler_ark.Ui.i18n import show_language_dialog, translate def _detect_system_color_scheme() -> str: @@ -471,35 +471,6 @@ def _find(cls, name: str): self.btn_acasl_loader.hide() self.btn_acasl_loader.setEnabled(False) - for widget, props in ( - (self.btn_select_folder, {"i18n_text_key": "select_folder", "i18n_tooltip_key": "tt_select_folder"}), - (self.btn_select_files, {"i18n_text_key": "select_files", "i18n_tooltip_key": "tt_select_files"}), - (self.compile_btn, {"i18n_text_key": "build_all", "i18n_tooltip_key": "tt_build_all"}), - (self.cancel_btn, {"i18n_text_key": "cancel_all", "i18n_tooltip_key": "tt_cancel_all"}), - (self.btn_suggest_deps, {"i18n_text_key": "suggest_deps", "i18n_tooltip_key": "tt_suggest_deps"}), - (self.btn_help, {"i18n_text_key": "help", "i18n_tooltip_key": "tt_help"}), - (self.btn_show_stats, {"i18n_text_key": "show_stats", "i18n_tooltip_key": "tt_show_stats"}), - (self.advanced_cfg_btn, {"i18n_text_key": "advanced_config"}), - (self.btn_remove_file, {"i18n_text_key": "btn_remove_file", "i18n_tooltip_key": "tt_remove_file"}), - (self.btn_clear_workspace, {"i18n_text_key": "btn_clear_workspace", "i18n_tooltip_key": "tt_clear_workspace"}), - (self.btn_bc_loader, {"i18n_text_key": "bc_loader", "i18n_tooltip_key": "tt_bc_loader"}), - (self.venv_button, {"i18n_text_key": "venv_button", "i18n_tooltip_key": "tt_venv_button"}), - (self.label_workspace_section, {"i18n_text_key": "label_workspace_section"}), - (self.venv_label, {"i18n_text_key": "venv_label", "i18n_text_system_key": "venv_label_system", "i18n_system_attr": "use_system_python"}), - (self.label_folder, {"i18n_text_key": "label_folder"}), - (self.label_files_section, {"i18n_text_key": "label_files_section"}), - (self.label_tools, {"i18n_text_key": "label_tools"}), - (self.label_options_section, {"i18n_text_key": "label_options_section"}), - (self.label_logs_section, {"i18n_text_key": "label_logs_section"}), - (self.label_progress, {"i18n_text_key": "label_progress"}), - (self.select_lang, {"i18n_text_key": "choose_language_button", "i18n_text_system_key": "choose_language_system_button", "i18n_system_attr": "language_pref", "i18n_tooltip_key": "tt_select_lang"}), - (self.select_theme, {"i18n_text_key": "choose_theme_button", "i18n_text_system_key": "choose_theme_system_button", "i18n_system_attr": "theme", "i18n_tooltip_key": "tt_select_theme"}), - (self.label_workspace_status, {"i18n_text_key": "label_workspace_status", "i18n_none_key": "label_workspace_status_none", "i18n_format_attr": "workspace_dir"}), - (self.file_filter_input, {"i18n_placeholder_key": "file_filter_placeholder"}), - ): - _declare_i18n(widget, **props) - - def _setup_compiler_tabs(self) -> None: """Initialize compiler tabs and bind available engines.""" from PySide6.QtWidgets import QTabWidget, QWidget diff --git a/pycompiler_ark/Ui/Gui/UiFeatures.py b/pycompiler_ark/Ui/Gui/UiFeatures.py index 818c85a..b3ec944 100644 --- a/pycompiler_ark/Ui/Gui/UiFeatures.py +++ b/pycompiler_ark/Ui/Gui/UiFeatures.py @@ -622,6 +622,17 @@ def register_language_refresh(self, callback: Callable) -> None: except Exception: pass + def unregister_language_refresh(self, callback: Callable) -> None: + """Unregister a previously registered language refresh callback.""" + try: + callbacks = getattr(self, "_language_refresh_callbacks", None) + if not callbacks: + return + if callback in callbacks: + callbacks.remove(callback) + except Exception: + pass + def log_i18n(self, fr: str, en: str) -> None: """Append a localized message to the log.""" try: diff --git a/pycompiler_ark/Ui/i18n.py b/pycompiler_ark/Ui/i18n.py index d0a2740..734423b 100644 --- a/pycompiler_ark/Ui/i18n.py +++ b/pycompiler_ark/Ui/i18n.py @@ -223,19 +223,14 @@ def translate(domain: object | None, key: str, default: str | None = None) -> st return default if default is not None else str(key) -def _declare_i18n(widget, **props) -> None: - """Attach i18n properties to a Qt object. - - This helper is centralised here so UI code, engines, and host widgets all use - the same property-setting behavior. - """ - if widget is None: - return - for key, value in props.items(): - try: - widget.setProperty(key, value) - except Exception: - pass +def _object_name(obj: Any) -> str: + try: + name = getattr(obj, "objectName", lambda: "")() + if isinstance(name, str): + return name.strip() + except Exception: + pass + return "" # Public async Plugins with real-time caching and error handling @@ -782,9 +777,6 @@ def i18n_synchro(self, lang_pref: str, tr: dict[str, Any]) -> str: _apply_main_app_translations(self, tr) - for cb in getattr(self, "_language_refresh_callbacks", []) or []: - cb() - from pycompiler_ark.Ui.Gui.IdeLikeGui.connections import ( _retranslate_ide_like_actions, ) @@ -801,6 +793,9 @@ def i18n_synchro(self, lang_pref: str, tr: dict[str, Any]) -> str: sdk_apply_tr(self, tr) + for cb in getattr(self, "_language_refresh_callbacks", []) or []: + cb() + if hasattr(self, "save_preferences"): self.save_preferences() @@ -849,6 +844,86 @@ def _prop(obj: Any, name: str) -> Any: def _is_system_value(value: Any) -> bool: return str(value).strip().lower() == "system" + def _binding_for_name(name: str) -> str: + if not name: + return "" + return { + "select_lang": "choose_language_button", + "select_theme": "choose_theme_button", + "act_language": "choose_language_button", + "act_theme": "choose_theme_button", + "btn_show_stats": "show_stats", + "btn_suggest_deps": "suggest_deps", + "btn_help": "help", + "btn_bc_loader": "bc_loader", + "btn_acasl_loader": "bc_loader", + "status_hint": "status_ready", + "act_workspace": "action_select_workspace", + "act_init": "action_init_project", + "act_venv": "action_select_venv", + "act_add_files": "action_add_files", + "act_clear_workspace": "btn_clear_workspace", + "act_stats": "show_stats", + "act_language": "choose_language_button", + "act_theme": "choose_theme_button", + "act_advanced": "advanced_config", + "act_lock": "lock_manager", + "act_save_engines": "save_engine_configs", + "act_help": "help", + }.get(name, name) + + def _tooltip_for_name(name: str) -> str: + if not name: + return "" + if name in { + "btn_select_folder", + "btn_select_files", + "compile_btn", + "cancel_btn", + "btn_remove_file", + "btn_select_icon", + "btn_nuitka_icon", + "btn_help", + "btn_bc_loader", + "btn_suggest_deps", + "btn_show_stats", + "btn_clear_workspace", + "venv_button", + }: + return f"tt_{name[4:]}" if name.startswith("btn_") else f"tt_{name}" + if name == "activity_btn_deps": + return "tt_suggest_deps" + if name == "btn_acasl_loader": + return "tt_bc_loader" + if name == "select_lang": + return "tt_select_lang" + if name == "select_theme": + return "tt_select_theme" + if name == "act_language": + return "tt_select_lang" + if name == "act_theme": + return "tt_select_theme" + if name in {"toolButton_more", "more_btn", "btn_more_actions"}: + return "tt_more_actions" + if name == "output_dir_input": + return "tt_output_dir" + if name == "windowed_checkbox": + return "tt_windowed" + if name == "disable_console_checkbox": + return "tt_disable_console" + if name == "debug_checkbox": + return "tt_debug" + if name == "verbose_checkbox": + return "tt_verbose" + return f"tt_{name}" if name.startswith("opt_") else "" + + def _placeholder_for_name(name: str) -> str: + if name == "file_filter_input": + return "file_filter_placeholder" + if name == "nuitka_output_dir": + return "nuitka_output_dir" + return "" + def _iter_objects(root: Any): stack = [root] seen: set[int] = set() @@ -896,7 +971,8 @@ def _apply_tab_text(obj: Any, key: str, default: str | None = None) -> None: parent = getattr(parent, "parent", lambda: None)() for obj in _iter_objects(self): - text_key = _prop(obj, "i18n_text_key") + name = _object_name(obj) + text_key = _prop(obj, "i18n_text_key") or _binding_for_name(name) if text_key: system_key = _prop(obj, "i18n_text_system_key") system_attr = _prop(obj, "i18n_system_attr") @@ -905,6 +981,20 @@ def _apply_tab_text(obj: Any, key: str, default: str | None = None) -> None: current = obj.text() if hasattr(obj, "text") else None chosen_key = str(text_key) + if not _prop(obj, "i18n_text_key"): + if name in {"select_lang", "act_language"}: + system_key = "choose_language_system_button" + system_attr = "language_pref" + elif name in {"select_theme", "act_theme"}: + system_key = "choose_theme_system_button" + system_attr = "theme" + elif name == "venv_label": + system_key = "venv_label_system" + system_attr = "use_system_python" + elif name == "label_workspace_status": + format_attr = "workspace_dir" + none_key = "label_workspace_status_none" + if system_key and system_attr and _is_system_value(getattr(self, str(system_attr), None)): chosen_key = str(system_key) @@ -922,19 +1012,19 @@ def _apply_tab_text(obj: Any, key: str, default: str | None = None) -> None: _apply_text(obj, chosen_key, current if isinstance(current, str) else None) - tooltip_key = _prop(obj, "i18n_tooltip_key") + tooltip_key = _prop(obj, "i18n_tooltip_key") or _tooltip_for_name(name) if tooltip_key: current = obj.toolTip() if hasattr(obj, "toolTip") else None _apply_tooltip(obj, str(tooltip_key), current if isinstance(current, str) else None) - placeholder_key = _prop(obj, "i18n_placeholder_key") + placeholder_key = _prop(obj, "i18n_placeholder_key") or _placeholder_for_name(name) if placeholder_key: current = obj.placeholderText() if hasattr(obj, "placeholderText") else None _apply_placeholder( obj, str(placeholder_key), current if isinstance(current, str) else None ) - tab_key = _prop(obj, "i18n_tab_key") + tab_key = _prop(obj, "i18n_tab_key") or (name if name.startswith("tab_") else "") if tab_key: current = obj.text() if hasattr(obj, "text") else None _apply_tab_text(obj, str(tab_key), current if isinstance(current, str) else None) From 4460c87282115784c4753a6e6554cb536d3fcea0 Mon Sep 17 00:00:00 2001 From: Samuel Amen Ague Date: Tue, 23 Jun 2026 10:28:12 +0000 Subject: [PATCH 3/6] i18n: restore live extension refresh Signed-off-by: Samuel Amen Ague --- pycompiler_ark/Core/engine/registry.py | 65 ++++++++++++++++- pycompiler_ark/Plugins/Cleaner/__init__.py | 9 ++- .../Plugins/Cleaner/languages/en.yml | 1 + .../Plugins/Cleaner/languages/fr.yml | 1 + .../Plugins_SDK/GeneralContext/__init__.py | 2 + .../Plugins_SDK/GeneralContext/i18n.py | 45 ++++++++++++ pycompiler_ark/Ui/Gui/Dialogs/BcaslDialog.py | 73 ++++++++++++++++++- 7 files changed, 191 insertions(+), 5 deletions(-) diff --git a/pycompiler_ark/Core/engine/registry.py b/pycompiler_ark/Core/engine/registry.py index c78c916..d6368f0 100644 --- a/pycompiler_ark/Core/engine/registry.py +++ b/pycompiler_ark/Core/engine/registry.py @@ -74,6 +74,48 @@ def _iter_i18n_roots(engine: CompilerEngine): continue +def _object_name(obj: Any) -> str: + try: + name = getattr(obj, "objectName", lambda: "")() + if isinstance(name, str): + return name.strip() + except Exception: + pass + return "" + + +def _binding_for_name(name: str) -> str: + if not name: + return "" + return { + "build_group": "build_group", + "onefile_checkbox": "onefile_checkbox", + "windowed_checkbox": "windowed_checkbox", + "standalone_checkbox": "standalone_checkbox", + "disable_console_checkbox": "disable_console_checkbox", + "debug_checkbox": "debug_checkbox", + "verbose_checkbox": "verbose_checkbox", + "mode_label": "mode_label", + "type_label": "type_label", + "console_label": "console_label", + "diagnostics_group": "diagnostics_group", + "hint_text": "hint_text", + }.get(name, "") + + +def _tooltip_for_name(name: str) -> str: + return { + "windowed_checkbox": "tt_windowed", + "disable_console_checkbox": "tt_disable_console", + "debug_checkbox": "tt_debug", + "verbose_checkbox": "tt_verbose", + }.get(name, "") + + +def _placeholder_for_name(name: str) -> str: + return "" + + def _apply_engine_i18n(root: Any, engine_id: str) -> None: def _prop(obj: Any, name: str) -> Any: if hasattr(obj, "property"): @@ -135,7 +177,8 @@ def _apply_tab_text(obj: Any, key: str, default: str | None = None) -> None: parent = getattr(parent, "parent", lambda: None)() for obj in _iter_objects(root): - text_key = _prop(obj, "i18n_text_key") + name = _object_name(obj) + text_key = _prop(obj, "i18n_text_key") or _binding_for_name(name) if text_key: system_key = _prop(obj, "i18n_text_system_key") system_attr = _prop(obj, "i18n_system_attr") @@ -144,6 +187,22 @@ def _apply_tab_text(obj: Any, key: str, default: str | None = None) -> None: current = obj.text() if hasattr(obj, "text") else None chosen_key = str(text_key) + if not _prop(obj, "i18n_text_key"): + if name == "hint_text": + chosen_key = "hint_text" + elif name == "build_group": + chosen_key = "build_group" + elif name == "diagnostics_group": + chosen_key = "diagnostics_group" + elif name == "mode_label": + chosen_key = "mode_label" + elif name == "type_label": + chosen_key = "type_label" + elif name == "console_label": + chosen_key = "console_label" + elif name in {"windowed_checkbox", "disable_console_checkbox", "debug_checkbox", "verbose_checkbox", "onefile_checkbox", "standalone_checkbox"}: + chosen_key = name + if system_key and system_attr and _is_system_value(getattr(root, str(system_attr), None)): chosen_key = str(system_key) @@ -159,12 +218,12 @@ def _apply_tab_text(obj: Any, key: str, default: str | None = None) -> None: _apply_text(obj, chosen_key, current if isinstance(current, str) else None) - tooltip_key = _prop(obj, "i18n_tooltip_key") + tooltip_key = _prop(obj, "i18n_tooltip_key") or _tooltip_for_name(name) if tooltip_key: current = obj.toolTip() if hasattr(obj, "toolTip") else None _apply_tooltip(obj, str(tooltip_key), current if isinstance(current, str) else None) - placeholder_key = _prop(obj, "i18n_placeholder_key") + placeholder_key = _prop(obj, "i18n_placeholder_key") or _placeholder_for_name(name) if placeholder_key: current = obj.placeholderText() if hasattr(obj, "placeholderText") else None _apply_placeholder( diff --git a/pycompiler_ark/Plugins/Cleaner/__init__.py b/pycompiler_ark/Plugins/Cleaner/__init__.py index a94a11d..2680385 100644 --- a/pycompiler_ark/Plugins/Cleaner/__init__.py +++ b/pycompiler_ark/Plugins/Cleaner/__init__.py @@ -99,32 +99,38 @@ def create_tab(self, parent, ctx: PreCompileContext, config: dict): return None w = QWidget(parent) + w.setObjectName("plugin_cleaner") lay = QVBoxLayout(w) lay.setSpacing(8) lay.setContentsMargins(8, 8, 8, 8) # Safety safety_group = QGroupBox(translate("cleaner", "ui_safety", "Safety"), w) + safety_group.setObjectName("ui_safety") safety_layout = QVBoxLayout() safety_layout.setSpacing(4) chk_confirm = QCheckBox( translate("cleaner", "ui_confirm", "Ask confirmation before cleaning"), safety_group, ) + chk_confirm.setObjectName("ui_confirm") safety_layout.addWidget(chk_confirm) safety_group.setLayout(safety_layout) # Targets targets_group = QGroupBox(translate("cleaner", "ui_targets", "Targets"), w) + targets_group.setObjectName("ui_targets") targets_layout = QFormLayout() targets_layout.setSpacing(6) chk_pyc = QCheckBox( translate("cleaner", "ui_pyc", "Remove .pyc files"), targets_group ) + chk_pyc.setObjectName("ui_pyc") chk_pycache = QCheckBox( translate("cleaner", "ui_pycache", "Remove __pycache__ folders"), targets_group, ) + chk_pycache.setObjectName("ui_pycache") targets_layout.addRow(chk_pyc) targets_layout.addRow(chk_pycache) targets_group.setLayout(targets_layout) @@ -142,6 +148,7 @@ def create_tab(self, parent, ctx: PreCompileContext, config: dict): ), w, ) + hint.setObjectName("ui_tip") hint.setStyleSheet("color: #888; font-size: 11px;") hint.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) lay.addWidget(hint) @@ -153,7 +160,7 @@ def on_save(cfg: dict): cfg["clean_pycache"] = bool(chk_pycache.isChecked()) return cfg - return ("Cleaner", w, on_save) + return (translate("cleaner", "tab_title", "Cleaner"), w, on_save) def on_pre_compile(self, ctx: PreCompileContext) -> None: """Nettoie le workspace avant la compilation.""" diff --git a/pycompiler_ark/Plugins/Cleaner/languages/en.yml b/pycompiler_ark/Plugins/Cleaner/languages/en.yml index 227d8a8..6e1724f 100644 --- a/pycompiler_ark/Plugins/Cleaner/languages/en.yml +++ b/pycompiler_ark/Plugins/Cleaner/languages/en.yml @@ -4,6 +4,7 @@ ui_confirm: Ask confirmation before cleaning ui_pyc: Remove .pyc files ui_pycache: Remove __pycache__ folders ui_tip: 'Tip: disable items you don''t want to delete.' +tab_title: Cleaner dlg_confirm: Do you want to clean the workspace (.pyc and __pycache__)? log_cancel: Cleaner cancelled by user log_noop: 'Cleaner: nothing to do (both options disabled)' diff --git a/pycompiler_ark/Plugins/Cleaner/languages/fr.yml b/pycompiler_ark/Plugins/Cleaner/languages/fr.yml index d81a2f2..3e95601 100644 --- a/pycompiler_ark/Plugins/Cleaner/languages/fr.yml +++ b/pycompiler_ark/Plugins/Cleaner/languages/fr.yml @@ -4,6 +4,7 @@ ui_confirm: Demander confirmation avant le nettoyage ui_pyc: Supprimer les fichiers .pyc ui_pycache: Supprimer les dossiers __pycache__ ui_tip: 'Astuce : désactivez ce que vous ne voulez pas supprimer.' +tab_title: Nettoyeur dlg_confirm: Voulez-vous nettoyer le workspace (.pyc et __pycache__) ? log_cancel: Nettoyage annulé par l'utilisateur log_noop: 'Cleaner : rien à faire (options désactivées)' diff --git a/pycompiler_ark/Plugins_SDK/GeneralContext/__init__.py b/pycompiler_ark/Plugins_SDK/GeneralContext/__init__.py index f7f8914..55dc15d 100644 --- a/pycompiler_ark/Plugins_SDK/GeneralContext/__init__.py +++ b/pycompiler_ark/Plugins_SDK/GeneralContext/__init__.py @@ -23,6 +23,7 @@ register_i18n_handler, register_plugin_translations, resolve_language_code, + refresh_widget_translations, set_translations, translate, unregister_i18n_handler, @@ -39,6 +40,7 @@ "normalize_language_code", "register_i18n_handler", "register_plugin_translations", + "refresh_widget_translations", "resolve_language_code", "set_translations", "translate", diff --git a/pycompiler_ark/Plugins_SDK/GeneralContext/i18n.py b/pycompiler_ark/Plugins_SDK/GeneralContext/i18n.py index 8e742ee..080d8d8 100644 --- a/pycompiler_ark/Plugins_SDK/GeneralContext/i18n.py +++ b/pycompiler_ark/Plugins_SDK/GeneralContext/i18n.py @@ -128,6 +128,51 @@ def translate(plugin_id: str, key: str, default: Optional[str] = None) -> str: return default if default is not None else str(key) +def _object_name(obj: Any) -> str: + try: + name = getattr(obj, "objectName", lambda: "")() + if isinstance(name, str): + return name.strip() + except Exception: + pass + return "" + + +def refresh_widget_translations(root: Any, plugin_id: str) -> None: + """Apply the active plugin catalog to an existing widget tree.""" + def _iter_objects(root_obj: Any): + stack = [root_obj] + seen: set[int] = set() + while stack: + obj = stack.pop() + if obj is None: + continue + oid = id(obj) + if oid in seen: + continue + seen.add(oid) + yield obj + try: + children = list(obj.children()) + except Exception: + children = [] + for child in reversed(children): + stack.append(child) + + def _apply_text(obj: Any, key: str, default: str | None = None) -> None: + if hasattr(obj, "setText"): + value = translate(plugin_id, key, default) + if isinstance(value, str) and value: + obj.setText(value) + + for obj in _iter_objects(root): + name = _object_name(obj) + text_key = name + if text_key: + current = obj.text() if hasattr(obj, "text") else None + _apply_text(obj, text_key, current if isinstance(current, str) else None) + + def register_i18n_handler(fn: Callable[[Any, dict], None]) -> None: if callable(fn): _HANDLERS.add(fn) diff --git a/pycompiler_ark/Ui/Gui/Dialogs/BcaslDialog.py b/pycompiler_ark/Ui/Gui/Dialogs/BcaslDialog.py index aae18f2..1c87a85 100644 --- a/pycompiler_ark/Ui/Gui/Dialogs/BcaslDialog.py +++ b/pycompiler_ark/Ui/Gui/Dialogs/BcaslDialog.py @@ -53,6 +53,10 @@ ) from pycompiler_ark.Core.Configs import load_ark_config, save_ark_config +from pycompiler_ark.Plugins_SDK.GeneralContext import ( + refresh_widget_translations, + translate, +) # --------------------------------------------------------------------------- # Helpers Thème @@ -453,14 +457,17 @@ def __init__( self._sections: list[_SectionWidget] = [] self._plugin_ui_state: dict[str, dict[str, Any]] = {} + self._language_refresh_cb = self._refresh_i18n self.setWindowTitle(gui.tr("BCASL Pipeline", "BCASL Pipeline")) self.resize(860, 680) self.setModal(False) self._build_ui() + self._register_language_refresh() self._install_shortcuts() self._push_undo() # état initial + self._refresh_i18n() # ------------------------------------------------------------------ # Construction UI @@ -559,6 +566,10 @@ def _on_bcasl_enabled_toggled(self, checked: bool) -> None: else: self._tabs.setToolTip("") + def closeEvent(self, event) -> None: + self._unregister_language_refresh() + super().closeEvent(event) + def _populate_sections(self) -> None: """Grouper les plugins par section et les insérer.""" plugins_raw = ( @@ -677,13 +688,73 @@ def _build_plugin_config_tabs(self) -> None: widget = tab_res if widget is None: continue + try: + if hasattr(widget, "setObjectName"): + widget.setObjectName(f"plugin_{pid}") + if hasattr(widget, "setProperty"): + widget.setProperty("i18n_domain", pid) + except Exception: + pass if not title: title = getattr(getattr(plugin, "meta", None), "name", None) or pid self._tabs.addTab(widget, str(title)) - self._plugin_ui_state[pid] = {"config": base_cfg, "on_save": on_save} + self._plugin_ui_state[pid] = { + "config": base_cfg, + "on_save": on_save, + "widget": widget, + "title_default": str(title), + } except Exception: continue + def _register_language_refresh(self) -> None: + try: + if hasattr(self._gui, "register_language_refresh"): + self._gui.register_language_refresh(self._language_refresh_cb) + except Exception: + pass + + def _unregister_language_refresh(self) -> None: + try: + if hasattr(self._gui, "unregister_language_refresh"): + self._gui.unregister_language_refresh(self._language_refresh_cb) + except Exception: + pass + + def _refresh_i18n(self) -> None: + try: + self.setWindowTitle(self._gui.tr("BCASL Pipeline", "BCASL Pipeline")) + except Exception: + pass + try: + if hasattr(self, "_chk_bcasl_enabled"): + self._chk_bcasl_enabled.setText( + self._gui.tr("Activer BCASL", "Enable BCASL") + ) + except Exception: + pass + try: + if hasattr(self, "_tabs"): + self._tabs.setTabText(0, self._gui.tr("Pipeline", "Pipeline")) + except Exception: + pass + for pid, state in self._plugin_ui_state.items(): + widget = state.get("widget") + if widget is not None: + try: + refresh_widget_translations(widget, pid) + except Exception: + pass + idx = self._tabs.indexOf(widget) if widget is not None else -1 + if idx >= 0: + title_default = str(state.get("title_default", pid)) + try: + self._tabs.setTabText( + idx, translate(pid, "tab_title", title_default) + ) + except Exception: + pass + # ------------------------------------------------------------------ # Raccourcis clavier # ------------------------------------------------------------------ From 495dac13921aaf485a700c6113e495948723f5e2 Mon Sep 17 00:00:00 2001 From: Samuel Amen Ague Date: Tue, 23 Jun 2026 10:28:48 +0000 Subject: [PATCH 4/6] refactor(engine): make engine tabs dynamic Signed-off-by: Samuel Amen Ague --- pycompiler_ark/engines/cx_freeze/__init__.py | 22 +++++++--------- pycompiler_ark/engines/nuitka/__init__.py | 26 ++++++++----------- .../engines/pyinstaller/__init__.py | 19 ++++++-------- 3 files changed, 28 insertions(+), 39 deletions(-) diff --git a/pycompiler_ark/engines/cx_freeze/__init__.py b/pycompiler_ark/engines/cx_freeze/__init__.py index 961cf90..dff17f2 100644 --- a/pycompiler_ark/engines/cx_freeze/__init__.py +++ b/pycompiler_ark/engines/cx_freeze/__init__.py @@ -33,7 +33,6 @@ engine_register, translate, ) -from pycompiler_ark.Ui.i18n import _declare_i18n from pycompiler_ark.engine_sdk.utils import log_with_level @@ -179,8 +178,8 @@ def create_tab(self, gui): # Create the tab widget tab = QWidget() - tab.setObjectName("tab_cx_freeze_dynamic") - _declare_i18n(tab, i18n_tab_key="tab_label") + tab.setObjectName(getattr(self, "name", self.id)) + tab.setProperty("i18n_tab_key", "tab_label") # Create main layout layout = QVBoxLayout(tab) @@ -188,7 +187,7 @@ def create_tab(self, gui): layout.setContentsMargins(8, 8, 8, 8) build_group = QGroupBox(translate(self.id, "build_group", "Build"), tab) - _declare_i18n(build_group, i18n_text_key="build_group") + build_group.setObjectName("build_group") build_layout = QFormLayout() build_layout.setSpacing(6) @@ -196,8 +195,7 @@ def create_tab(self, gui): self._cx_windowed = QCheckBox( translate(self.id, "windowed_checkbox", "No console") ) - self._cx_windowed.setObjectName("cx_windowed_dynamic") - _declare_i18n(self._cx_windowed, i18n_text_key="windowed_checkbox", i18n_tooltip_key="tt_windowed") + self._cx_windowed.setObjectName("windowed_checkbox") self._cx_windowed.setToolTip( translate(self.id, "tt_windowed", "Disable the console window.") ) @@ -208,15 +206,14 @@ def create_tab(self, gui): diagnostics_group = QGroupBox( translate(self.id, "diagnostics_group", "Diagnostics"), tab ) - _declare_i18n(diagnostics_group, i18n_text_key="diagnostics_group") + diagnostics_group.setObjectName("diagnostics_group") diagnostics_layout = QVBoxLayout() diagnostics_layout.setSpacing(4) self._cx_debug = QCheckBox( translate(self.id, "debug_checkbox", "Debug") ) - self._cx_debug.setObjectName("cx_debug_dynamic") - _declare_i18n(self._cx_debug, i18n_text_key="debug_checkbox", i18n_tooltip_key="tt_debug") + self._cx_debug.setObjectName("debug_checkbox") self._cx_debug.setToolTip( translate(self.id, "tt_debug", "Enable debug output.") ) @@ -225,8 +222,7 @@ def create_tab(self, gui): self._cx_verbose = QCheckBox( translate(self.id, "verbose_checkbox", "Verbose") ) - self._cx_verbose.setObjectName("cx_verbose_dynamic") - _declare_i18n(self._cx_verbose, i18n_text_key="verbose_checkbox", i18n_tooltip_key="tt_verbose") + self._cx_verbose.setObjectName("verbose_checkbox") self._cx_verbose.setToolTip( translate(self.id, "tt_verbose", "Enable verbose output.") ) @@ -241,7 +237,7 @@ def create_tab(self, gui): ), tab, ) - _declare_i18n(hint, i18n_text_key="hint_text") + hint.setObjectName("hint_text") hint.setStyleSheet("color: #888; font-size: 11px;") hint.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) @@ -254,7 +250,7 @@ def create_tab(self, gui): self._gui = gui self._tab_widget = tab - return tab, translate(self.id, "tab_label", "CX_Freeze") + return tab, translate(self.id, "tab_label", getattr(self, "name", self.id)) except Exception as e: try: diff --git a/pycompiler_ark/engines/nuitka/__init__.py b/pycompiler_ark/engines/nuitka/__init__.py index ad32b7d..e125901 100644 --- a/pycompiler_ark/engines/nuitka/__init__.py +++ b/pycompiler_ark/engines/nuitka/__init__.py @@ -32,7 +32,6 @@ engine_register, translate, ) -from pycompiler_ark.Ui.i18n import _declare_i18n from pycompiler_ark.engine_sdk.utils import log_with_level @@ -199,8 +198,8 @@ def create_tab(self, gui): # Create the tab widget tab = QWidget() - tab.setObjectName("tab_nuitka_dynamic") - _declare_i18n(tab, i18n_tab_key="tab_label") + tab.setObjectName(getattr(self, "name", self.id)) + tab.setProperty("i18n_tab_key", "tab_label") # Create main layout layout = QVBoxLayout(tab) @@ -208,7 +207,7 @@ def create_tab(self, gui): layout.setContentsMargins(8, 8, 8, 8) build_group = QGroupBox(translate(self.id, "build_group", "Build"), tab) - _declare_i18n(build_group, i18n_text_key="build_group") + build_group.setObjectName("build_group") build_layout = QFormLayout() build_layout.setSpacing(6) @@ -216,28 +215,25 @@ def create_tab(self, gui): self._nuitka_onefile = QCheckBox( translate(self.id, "onefile_checkbox", "Onefile (--onefile)") ) - self._nuitka_onefile.setObjectName("nuitka_onefile_dynamic") - _declare_i18n(self._nuitka_onefile, i18n_text_key="onefile_checkbox") + self._nuitka_onefile.setObjectName("onefile_checkbox") mode_label = QLabel(translate(self.id, "mode_label", "Mode:"), tab) - _declare_i18n(mode_label, i18n_text_key="mode_label") + mode_label.setObjectName("mode_label") build_layout.addRow(mode_label, self._nuitka_onefile) # Standalone option self._nuitka_standalone = QCheckBox( translate(self.id, "standalone_checkbox", "Standalone (--standalone)") ) - self._nuitka_standalone.setObjectName("nuitka_standalone_dynamic") - _declare_i18n(self._nuitka_standalone, i18n_text_key="standalone_checkbox") + self._nuitka_standalone.setObjectName("standalone_checkbox") type_label = QLabel(translate(self.id, "type_label", "Type:"), tab) - _declare_i18n(type_label, i18n_text_key="type_label") + type_label.setObjectName("type_label") build_layout.addRow(type_label, self._nuitka_standalone) # Disable console option self._nuitka_disable_console = QCheckBox( translate(self.id, "disable_console_checkbox", "Disable console") ) - self._nuitka_disable_console.setObjectName("nuitka_disable_console_dynamic") - _declare_i18n(self._nuitka_disable_console, i18n_text_key="disable_console_checkbox", i18n_tooltip_key="tt_disable_console") + self._nuitka_disable_console.setObjectName("disable_console_checkbox") self._nuitka_disable_console.setToolTip( translate( self.id, @@ -246,7 +242,7 @@ def create_tab(self, gui): ) ) console_label = QLabel(translate(self.id, "console_label", "Console:"), tab) - _declare_i18n(console_label, i18n_text_key="console_label") + console_label.setObjectName("console_label") build_layout.addRow(console_label, self._nuitka_disable_console) build_group.setLayout(build_layout) @@ -258,7 +254,7 @@ def create_tab(self, gui): ), tab, ) - _declare_i18n(hint, i18n_text_key="hint_text") + hint.setObjectName("hint_text") hint.setStyleSheet("color: #888; font-size: 11px;") hint.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) @@ -270,7 +266,7 @@ def create_tab(self, gui): self._gui = gui self._tab_widget = tab - return tab, translate(self.id, "tab_label", "Nuitka") + return tab, translate(self.id, "tab_label", getattr(self, "name", self.id)) except Exception as e: try: diff --git a/pycompiler_ark/engines/pyinstaller/__init__.py b/pycompiler_ark/engines/pyinstaller/__init__.py index 4038251..4812ef2 100644 --- a/pycompiler_ark/engines/pyinstaller/__init__.py +++ b/pycompiler_ark/engines/pyinstaller/__init__.py @@ -33,7 +33,6 @@ engine_register, translate, ) -from pycompiler_ark.Ui.i18n import _declare_i18n from pycompiler_ark.engine_sdk.utils import log_with_level @@ -180,8 +179,8 @@ def create_tab(self, gui): # Create the tab widget tab = QWidget() - tab.setObjectName("tab_pyinstaller_dynamic") - _declare_i18n(tab, i18n_tab_key="tab_label") + tab.setObjectName(getattr(self, "name", self.id)) + tab.setProperty("i18n_tab_key", "tab_label") # Create main layout layout = QVBoxLayout(tab) @@ -189,7 +188,7 @@ def create_tab(self, gui): layout.setContentsMargins(8, 8, 8, 8) build_group = QGroupBox(translate(self.id, "build_group", "Build"), tab) - _declare_i18n(build_group, i18n_text_key="build_group") + build_group.setObjectName("build_group") build_layout = QFormLayout() build_layout.setSpacing(6) @@ -197,18 +196,16 @@ def create_tab(self, gui): self._opt_onefile = QCheckBox( translate(self.id, "onefile_checkbox", "Onefile") ) - self._opt_onefile.setObjectName("opt_onefile_dynamic") - _declare_i18n(self._opt_onefile, i18n_text_key="onefile_checkbox") + self._opt_onefile.setObjectName("onefile_checkbox") mode_label = QLabel(translate(self.id, "mode_label", "Mode:"), tab) - _declare_i18n(mode_label, i18n_text_key="mode_label") + mode_label.setObjectName("mode_label") build_layout.addRow(mode_label, self._opt_onefile) # Windowed option self._opt_windowed = QCheckBox( translate(self.id, "windowed_checkbox", "Windowed") ) - self._opt_windowed.setObjectName("opt_windowed_dynamic") - _declare_i18n(self._opt_windowed, i18n_text_key="windowed_checkbox") + self._opt_windowed.setObjectName("windowed_checkbox") build_layout.addRow(self._opt_windowed) build_group.setLayout(build_layout) @@ -221,7 +218,7 @@ def create_tab(self, gui): ), tab, ) - _declare_i18n(hint, i18n_text_key="hint_text") + hint.setObjectName("hint_text") hint.setStyleSheet("color: #888; font-size: 11px;") hint.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) @@ -233,7 +230,7 @@ def create_tab(self, gui): self._gui = gui self._tab_widget = tab - return tab, translate(self.id, "tab_label", "PyInstaller") + return tab, translate(self.id, "tab_label", getattr(self, "name", self.id)) except Exception as e: try: From fbca95c5382a0cb076c4108a31987303d64e8210 Mon Sep 17 00:00:00 2001 From: Samuel Amen Ague Date: Tue, 23 Jun 2026 11:11:44 +0000 Subject: [PATCH 5/6] i18n: add output cleaner plugin tab Signed-off-by: Samuel Amen Ague --- .../Plugins/OutputCleaner/__init__.py | 61 +++++++++++++++ .../Plugins/OutputCleaner/languages/en.yml | 4 + .../Plugins/OutputCleaner/languages/fr.yml | 4 + .../Plugins_SDK/GeneralContext/i18n.py | 75 +++++++++++++++++-- 4 files changed, 138 insertions(+), 6 deletions(-) diff --git a/pycompiler_ark/Plugins/OutputCleaner/__init__.py b/pycompiler_ark/Plugins/OutputCleaner/__init__.py index 46fb940..b083d0e 100644 --- a/pycompiler_ark/Plugins/OutputCleaner/__init__.py +++ b/pycompiler_ark/Plugins/OutputCleaner/__init__.py @@ -77,6 +77,67 @@ def _get_config(self, ctx: PreCompileContext) -> dict: except Exception: return {} + def create_tab(self, parent, ctx: PreCompileContext, config: dict): + try: + from PySide6.QtWidgets import ( + QCheckBox, + QGroupBox, + QLabel, + QSizePolicy, + QVBoxLayout, + QWidget, + ) + except Exception: + return None + + widget = QWidget(parent) + widget.setObjectName("plugin_outputcleaner") + layout = QVBoxLayout(widget) + layout.setSpacing(8) + layout.setContentsMargins(8, 8, 8, 8) + + safety_group = QGroupBox( + _tr("ui_safety", "Safety"), widget + ) + safety_group.setObjectName("ui_safety") + safety_layout = QVBoxLayout(safety_group) + safety_layout.setSpacing(4) + + chk_confirm = QCheckBox( + _tr( + "ui_confirm", + "Ask confirmation before cleaning the output directory", + ), + safety_group, + ) + chk_confirm.setObjectName("ui_confirm") + safety_layout.addWidget(chk_confirm) + + hint = QLabel( + _tr( + "ui_hint", + "This plugin removes the build output before compilation.", + ), + widget, + ) + hint.setObjectName("ui_hint") + hint.setWordWrap(True) + hint.setStyleSheet("color: #888; font-size: 11px;") + hint.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + + safety_group.setLayout(safety_layout) + layout.addWidget(safety_group) + layout.addWidget(hint) + layout.addStretch(1) + + chk_confirm.setChecked(bool(config.get("confirm", True))) + + def on_save(cfg: dict): + cfg["confirm"] = bool(chk_confirm.isChecked()) + return cfg + + return (_tr("tab_title", "Output Cleaner"), widget, on_save) + def on_pre_compile(self, ctx: PreCompileContext) -> None: """Nettoie le dossier output avant la compilation.""" try: diff --git a/pycompiler_ark/Plugins/OutputCleaner/languages/en.yml b/pycompiler_ark/Plugins/OutputCleaner/languages/en.yml index 361b597..2ace306 100644 --- a/pycompiler_ark/Plugins/OutputCleaner/languages/en.yml +++ b/pycompiler_ark/Plugins/OutputCleaner/languages/en.yml @@ -1,6 +1,10 @@ _meta: code: en name: English +tab_title: "Output Cleaner" +ui_safety: "Safety" +ui_confirm: "Ask confirmation before cleaning the output directory" +ui_hint: "This plugin removes the build output before compilation." dialog_title: "Output Cleaner" warn_no_build_context: "OutputCleaner: No BuildContext available. Cannot identify output directory." warn_no_output_dir: "OutputCleaner: No output_dir defined in BuildContext." diff --git a/pycompiler_ark/Plugins/OutputCleaner/languages/fr.yml b/pycompiler_ark/Plugins/OutputCleaner/languages/fr.yml index dc08e4c..9e4f420 100644 --- a/pycompiler_ark/Plugins/OutputCleaner/languages/fr.yml +++ b/pycompiler_ark/Plugins/OutputCleaner/languages/fr.yml @@ -1,6 +1,10 @@ _meta: code: fr name: Français +tab_title: "Nettoyeur de sortie" +ui_safety: "Sécurité" +ui_confirm: "Demander confirmation avant de nettoyer le dossier de sortie" +ui_hint: "Ce plugin supprime la sortie de compilation avant la compilation." dialog_title: "Nettoyeur de sortie" warn_no_build_context: "OutputCleaner: Aucun BuildContext disponible. Impossible d'identifier le dossier de sortie." warn_no_output_dir: "OutputCleaner: Aucun output_dir défini dans BuildContext." diff --git a/pycompiler_ark/Plugins_SDK/GeneralContext/i18n.py b/pycompiler_ark/Plugins_SDK/GeneralContext/i18n.py index 080d8d8..6affd5f 100644 --- a/pycompiler_ark/Plugins_SDK/GeneralContext/i18n.py +++ b/pycompiler_ark/Plugins_SDK/GeneralContext/i18n.py @@ -159,19 +159,82 @@ def _iter_objects(root_obj: Any): for child in reversed(children): stack.append(child) + def _prop(obj: Any, name: str) -> str: + try: + value = getattr(obj, name, None) + if isinstance(value, str) and value.strip(): + return value.strip() + except Exception: + pass + try: + value = obj.property(name) if hasattr(obj, "property") else None + if isinstance(value, str) and value.strip(): + return value.strip() + except Exception: + pass + return "" + def _apply_text(obj: Any, key: str, default: str | None = None) -> None: - if hasattr(obj, "setText"): - value = translate(plugin_id, key, default) - if isinstance(value, str) and value: - obj.setText(value) + if not hasattr(obj, "setText"): + return + value = translate(plugin_id, key, default) + if isinstance(value, str) and value: + obj.setText(value) + + def _apply_title(obj: Any, key: str, default: str | None = None) -> None: + if not hasattr(obj, "setTitle"): + return + value = translate(plugin_id, key, default) + if isinstance(value, str) and value: + obj.setTitle(value) + + def _apply_placeholder(obj: Any, key: str, default: str | None = None) -> None: + if not hasattr(obj, "setPlaceholderText"): + return + value = translate(plugin_id, key, default) + if isinstance(value, str) and value: + obj.setPlaceholderText(value) + + def _apply_tooltip(obj: Any, key: str, default: str | None = None) -> None: + if not hasattr(obj, "setToolTip"): + return + value = translate(plugin_id, key, default) + if isinstance(value, str) and value: + obj.setToolTip(value) for obj in _iter_objects(root): name = _object_name(obj) - text_key = name - if text_key: + if not name: + continue + + title_key = _prop(obj, "i18n_title_key") or name + text_key = _prop(obj, "i18n_text_key") or name + tooltip_key = _prop(obj, "i18n_tooltip_key") + placeholder_key = _prop(obj, "i18n_placeholder_key") + + if hasattr(obj, "setTitle"): + current = obj.title() if hasattr(obj, "title") else None + _apply_title(obj, title_key, current if isinstance(current, str) else None) + continue + + if hasattr(obj, "setText"): current = obj.text() if hasattr(obj, "text") else None _apply_text(obj, text_key, current if isinstance(current, str) else None) + if tooltip_key: + current = obj.toolTip() if hasattr(obj, "toolTip") else None + _apply_tooltip( + obj, tooltip_key, current if isinstance(current, str) else None + ) + + if placeholder_key: + current = ( + obj.placeholderText() if hasattr(obj, "placeholderText") else None + ) + _apply_placeholder( + obj, placeholder_key, current if isinstance(current, str) else None + ) + def register_i18n_handler(fn: Callable[[Any, dict], None]) -> None: if callable(fn): From ff74cc4c93455041f2f57e2a884f0c4b686aee0c Mon Sep 17 00:00:00 2001 From: Samuel Amen Ague Date: Tue, 23 Jun 2026 11:11:44 +0000 Subject: [PATCH 6/6] docs: update i18n guides Signed-off-by: Samuel Amen Ague --- docs/app_i18n.md | 23 ++++++++++------------- docs/contributing.md | 2 ++ docs/how_to_create_a_bc_plugin.md | 3 +++ docs/how_to_create_an_engine.md | 6 +++++- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/docs/app_i18n.md b/docs/app_i18n.md index 0703fed..c0f8564 100644 --- a/docs/app_i18n.md +++ b/docs/app_i18n.md @@ -15,7 +15,7 @@ That module owns: - refreshing translated widgets when the language changes The public lookup API is `translate(self.id, key, default)`. -The app UI should use that API everywhere a label, tooltip, placeholder, or tab title needs a translated value. +The app UI should use that API everywhere a label, tooltip, placeholder, action text, or tab title needs a translated value. ## **Language Files** @@ -52,17 +52,7 @@ The recommended pattern is: 1. create the widget 2. attach i18n properties to it 3. let `pycompiler_ark/Ui/i18n.py` resolve the active language -4. call `translate(self.id, ...)` when reading text dynamically - -For standard widgets, use the helper already used in the codebase: - -```python -_declare_i18n( - self.compile_btn, - i18n_text_key="build_all", - i18n_tooltip_key="tt_build_all", -) -``` +4. let the generic walker read the widget properties and call `translate(self.id, ...)` If you need to attach properties manually, use: @@ -84,6 +74,13 @@ widget.setProperty("i18n_tooltip_key", "tt_build_all") - `i18n_format_attr` - `i18n_none_key` +The walker is generic: + +- `QGroupBox` uses `setTitle(...)` +- `QAction`, buttons, labels, and checkboxes use `setText(...)` +- line edits and similar widgets use `setPlaceholderText(...)` +- tooltips are applied when `i18n_tooltip_key` is present + Typical use cases: - `i18n_text_key`: button text, label text, action text @@ -102,7 +99,7 @@ When the user changes the language: 2. `get_translations()` loads the selected YAML file. 3. `i18n_synchro()` stores the active catalog. 4. `_apply_main_app_translations()` walks the UI tree and reapplies texts. -5. The IDE-like actions, engine registry, and plugin SDK are refreshed through their generic host hooks. +5. The engine registry and plugin SDK are refreshed through generic host hooks. This is why the application should not hardcode translated strings inside the refresh path. The refresh path must stay generic and data-driven. diff --git a/docs/contributing.md b/docs/contributing.md index 291bf8c..ed700b8 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -19,6 +19,8 @@ If you are extending PyCompiler ARK, start with: - **Creating a Compilation Engine**: [how_to_create_an_engine.md](https://github.com/raidos23/PyCompiler_ARK/blob/main/docs/how_to_create_an_engine.md) - **Creating a Pre-Compile Plugin**: [how_to_create_a_bc_plugin.md](https://github.com/raidos23/PyCompiler_ARK/blob/main/docs/how_to_create_a_bc_plugin.md) +For translation work, read the application guide first, then the engine or plugin guide that matches the area you are changing. + ## **Core References** - **BuildContext Spec**: [BuildContext.md](https://github.com/raidos23/PyCompiler_ARK/blob/main/docs/BuildContext.md) diff --git a/docs/how_to_create_a_bc_plugin.md b/docs/how_to_create_a_bc_plugin.md index 895c9c3..bc1f692 100644 --- a/docs/how_to_create_a_bc_plugin.md +++ b/docs/how_to_create_a_bc_plugin.md @@ -221,6 +221,7 @@ Notes. - `on_save(config_dict)` can return an updated dict. - Each plugin entry stores its config in the `config` field inside the `plugins` collection in `bcasl.yml`. - `create_tab(...)` may return a widget, a `(title, widget)` tuple, a `(title, widget, on_save)` tuple, or a dict with `title`, `widget`, and `on_save`. +- Keep the widget tree stable and assign meaningful `objectName()` values so the host can refresh translations in place. **Plugin i18n (GeneralContext)** @@ -251,8 +252,10 @@ Notes. - Keep plugin-specific keys inside the plugin package only. - Use `translate(...)` directly in the plugin UI code. - Do not add a custom i18n refresh hook inside the plugin package; the host handles language synchronization. +- When building a plugin tab, attach i18n properties such as `i18n_text_key`, `i18n_tooltip_key`, `i18n_placeholder_key`, or `i18n_title_key` to the widgets that need refresh. - The SDK also accepts the plugin folder name as ID (case-insensitive). - If a key is missing, `translate()` falls back to the default you pass in. +- Avoid importing application UI modules from a plugin. Keep the dependency boundary inside `Plugins_SDK` and the standard library. **Sandbox and Resource Limits** diff --git a/docs/how_to_create_an_engine.md b/docs/how_to_create_an_engine.md index 00fde04..7d7512e 100644 --- a/docs/how_to_create_an_engine.md +++ b/docs/how_to_create_an_engine.md @@ -85,7 +85,9 @@ UI and i18n. - `create_tab(self, gui) -> (QWidget, label) | None`: adds a tab. - `translate(self.id, key, default=None)`: engine-local translation lookup. -- The host refreshes the engine UI when the language changes, so the engine only needs to declare translation keys on its widgets and use `translate(...)` when building the UI. +- The host refreshes the engine UI when the language changes. +- Set stable widget `objectName()` values and, when needed, i18n properties such as `i18n_text_key`, `i18n_tooltip_key`, `i18n_placeholder_key`, and `i18n_tab_key`. +- Use `translate(...)` when building the UI and let the host reapply the active catalog at runtime. Tools and dependencies. @@ -107,6 +109,7 @@ Tools and dependencies. - **IMPORTANT**: Do not include UI components for **Icon** selection or **Output directory** in your engine tab. These are globally managed in `ark.yml` and carried by `BuildContext`. Focus only on engine-specific flags and options. - Prefer grouping options with `QGroupBox` sections and compact hints, following the built-in engines layout style. - Keep widget attribute names stable once they are used by config persistence or compilation logic. +- Do not import application UI modules from an engine. Use the engine SDK only. ### **Engine Config (get_config / set_config)** @@ -118,6 +121,7 @@ Flow: - `get_config(gui)` returns a JSON‑serializable dict of current UI state. - `set_config(gui, cfg)` applies a config dict back to the widgets. - The Core saves configs on compile and reloads them when a workspace is applied. +- The host can refresh translated text live without recreating the engine, so keep the widget tree stable. #### Minimal example