Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 10 additions & 13 deletions docs/app_i18n.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand Down Expand Up @@ -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:

Expand All @@ -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
Expand All @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions docs/how_to_create_a_bc_plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)**

Expand Down Expand Up @@ -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**

Expand Down
6 changes: 5 additions & 1 deletion docs/how_to_create_an_engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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)**

Expand All @@ -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

Expand Down
89 changes: 89 additions & 0 deletions pycompiler_ark/Core/engine/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,83 @@
from __future__ import annotations

import time
from dataclasses import dataclass
from typing import TYPE_CHECKING, Callable, Optional

if TYPE_CHECKING:
from pycompiler_ark.Core.engine.build_context import BuildContext


@dataclass(frozen=True)
class EngineMeta:
"""Métadonnées d'un moteur de compilation."""

id: str
name: str
version: str
required_core_version: str = "1.0.0"
required_sdk_version: str = "1.0.0"
description: str = ""
author: str = ""

def __post_init__(self) -> None:
nid = str(self.id or "").strip()
nname = str(self.name or "").strip()
nversion = str(self.version or "").strip()
if not nid:
raise ValueError("EngineMeta invalide: 'id' requis")
if not nname:
raise ValueError("EngineMeta invalide: 'name' requis")
if not nversion:
raise ValueError("EngineMeta invalide: 'version' requis")
object.__setattr__(self, "id", nid)
object.__setattr__(self, "name", nname)
object.__setattr__(self, "version", nversion)
object.__setattr__(self, "required_core_version", str(self.required_core_version or "1.0.0").strip() or "1.0.0")
object.__setattr__(self, "required_sdk_version", str(self.required_sdk_version or "1.0.0").strip() or "1.0.0")
object.__setattr__(self, "description", str(self.description or "").strip())
object.__setattr__(self, "author", str(self.author or "").strip())


def resolve_engine_meta(engine_or_cls: object) -> EngineMeta:
"""Resolve engine metadata from a meta object or legacy class attributes."""

meta = getattr(engine_or_cls, "meta", None)
if isinstance(meta, EngineMeta):
return meta

if isinstance(meta, dict):
return EngineMeta(
id=str(meta.get("id") or getattr(engine_or_cls, "id", "") or "base"),
name=str(meta.get("name") or getattr(engine_or_cls, "name", "") or "BaseEngine"),
version=str(meta.get("version") or getattr(engine_or_cls, "version", "") or "1.0.0"),
required_core_version=str(
meta.get("required_core_version")
or getattr(engine_or_cls, "required_core_version", "1.0.0")
),
required_sdk_version=str(
meta.get("required_sdk_version")
or getattr(engine_or_cls, "required_sdk_version", "1.0.0")
),
description=str(meta.get("description") or ""),
author=str(meta.get("author") or ""),
)

return EngineMeta(
id=str(getattr(engine_or_cls, "id", "") or "base"),
name=str(getattr(engine_or_cls, "name", "") or "BaseEngine"),
version=str(getattr(engine_or_cls, "version", "") or "1.0.0"),
required_core_version=str(
getattr(engine_or_cls, "required_core_version", "1.0.0")
),
required_sdk_version=str(
getattr(engine_or_cls, "required_sdk_version", "1.0.0")
),
description=str(getattr(engine_or_cls, "description", "") or ""),
author=str(getattr(engine_or_cls, "author", "") or ""),
)


def log_i18n_level(gui, level: str, fr: str, en: str) -> None:
"""Minimal i18n log helper to avoid engine loader <-> engine_sdk circular imports."""
try:
Expand Down Expand Up @@ -81,12 +152,30 @@ class CompilerEngine:
provided via the `gui` object.
"""

meta: EngineMeta = EngineMeta(id="base", name="BaseEngine", version="1.0.0")
id: str = "base"
name: str = "BaseEngine"
version: str = "1.0.0"
required_core_version: str = "1.0.0"
required_sdk_version: str = "1.0.0"

def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
try:
resolved = resolve_engine_meta(cls)
cls.meta = resolved
cls.id = resolved.id
cls.name = resolved.name
cls.version = resolved.version
cls.required_core_version = resolved.required_core_version
cls.required_sdk_version = resolved.required_sdk_version
if resolved.description and not getattr(cls, "description", None):
cls.description = resolved.description
if resolved.author and not getattr(cls, "author", None):
cls.author = resolved.author
except Exception:
pass

def preflight(self, gui, file: str) -> bool:
"""Perform preflight checks and setup. Return True if OK, False to abort."""
return True
Expand Down
75 changes: 62 additions & 13 deletions pycompiler_ark/Core/engine/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -84,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"):
Expand Down Expand Up @@ -145,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")
Expand All @@ -154,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)

Expand All @@ -169,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(
Expand Down
Loading