diff --git a/README.md b/README.md index a9252bc0..ee4add40 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ ## Table of Contents +- [What's new (2026-06-19) — i18n / l10n Testing](#whats-new-2026-06-19--i18n--l10n-testing) - [What's new (2026-06-19) — Data Quality](#whats-new-2026-06-19--data-quality) - [What's new (2026-06-19) — SBOM & Suite Sharding](#whats-new-2026-06-19--sbom--suite-sharding) - [What's new (2026-06-19) — Reactive Observer](#whats-new-2026-06-19--reactive-observer) @@ -72,6 +73,14 @@ --- +## What's new (2026-06-19) — i18n / l10n Testing + +Three pure-stdlib internationalization/localization testing helpers that compound, full stack. Full reference: [`docs/source/Eng/doc/new_features/v20_features_doc.rst`](docs/source/Eng/doc/new_features/v20_features_doc.rst). + +- **Pseudo-localization** — `pseudo_localize` / `pseudo_localize_catalog` (`AC_pseudo_localize`, `ac_pseudo_localize`): accent + pad UI strings (placeholders preserved, `⟦…⟧` wrapped) to flush out hardcoded text and pre-stress layout before real translation. +- **Text-overflow detection** — `check_overflow(elements)` (`AC_check_overflow`, `ac_check_overflow`): flag text whose estimated width exceeds its widget bounds (the #1 l10n bug), computed from the a11y bounds AutoControl already reads. +- **Catalog completeness** — `check_catalog(base, target)` (`AC_check_catalog`, `ac_check_catalog`): diff a translation catalog for missing / orphaned / empty keys and placeholder mismatches — a CI gate against blank UI. + ## What's new (2026-06-19) — Data Quality Three pure-stdlib data-quality helpers (the gate between `load_rows`/OCR and downstream entry), full stack. Full reference: [`docs/source/Eng/doc/new_features/v19_features_doc.rst`](docs/source/Eng/doc/new_features/v19_features_doc.rst). diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 42e6ddd5..3a58c6a9 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -12,6 +12,7 @@ ## 目录 +- [本次更新 (2026-06-19) — i18n / l10n 测试](#本次更新-2026-06-19--i18n--l10n-测试) - [本次更新 (2026-06-19) — 数据质量](#本次更新-2026-06-19--数据质量) - [本次更新 (2026-06-19) — SBOM 与测试分片](#本次更新-2026-06-19--sbom-与测试分片) - [本次更新 (2026-06-19) — 反应式观察器](#本次更新-2026-06-19--反应式观察器) @@ -71,6 +72,14 @@ --- +## 本次更新 (2026-06-19) — i18n / l10n 测试 + +三项可互相搭配的纯标准库国际化/本地化测试辅助工具,走完整五层。完整参考:[`docs/source/Zh/doc/new_features/v20_features_doc.rst`](../docs/source/Zh/doc/new_features/v20_features_doc.rst)。 + +- **伪本地化** — `pseudo_localize` / `pseudo_localize_catalog`(`AC_pseudo_localize`、`ac_pseudo_localize`):为 UI 字符串加重音与填充(保留占位符、以 `⟦…⟧` 包裹),在真正翻译前揪出硬编码文本并对版面施压。 +- **文本溢出检测** — `check_overflow(elements)`(`AC_check_overflow`、`ac_check_overflow`):标记估计宽度超过控件边界的文本(本地化头号 bug),由 AutoControl 既有读取的 a11y 边界计算。 +- **目录完整性** — `check_catalog(base, target)`(`AC_check_catalog`、`ac_check_catalog`):比对翻译目录的缺失/多余/空白键与占位符不一致——防止空白 UI 的 CI 闸。 + ## 本次更新 (2026-06-19) — 数据质量 三项纯标准库的数据质量辅助工具(介于 `load_rows`/OCR 与下游输入之间的闸),走完整五层。完整参考:[`docs/source/Zh/doc/new_features/v19_features_doc.rst`](../docs/source/Zh/doc/new_features/v19_features_doc.rst)。 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 21bc79f6..98666165 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -12,6 +12,7 @@ ## 目錄 +- [本次更新 (2026-06-19) — i18n / l10n 測試](#本次更新-2026-06-19--i18n--l10n-測試) - [本次更新 (2026-06-19) — 資料品質](#本次更新-2026-06-19--資料品質) - [本次更新 (2026-06-19) — SBOM 與測試分片](#本次更新-2026-06-19--sbom-與測試分片) - [本次更新 (2026-06-19) — 反應式觀察器](#本次更新-2026-06-19--反應式觀察器) @@ -71,6 +72,14 @@ --- +## 本次更新 (2026-06-19) — i18n / l10n 測試 + +三項可互相搭配的純標準庫國際化/在地化測試輔助工具,走完整五層。完整參考:[`docs/source/Zh/doc/new_features/v20_features_doc.rst`](../docs/source/Zh/doc/new_features/v20_features_doc.rst)。 + +- **偽在地化** — `pseudo_localize` / `pseudo_localize_catalog`(`AC_pseudo_localize`、`ac_pseudo_localize`):為 UI 字串加重音與填充(保留佔位符、以 `⟦…⟧` 包覆),在真正翻譯前揪出寫死文字並對版面施壓。 +- **文字溢位偵測** — `check_overflow(elements)`(`AC_check_overflow`、`ac_check_overflow`):標記估計寬度超過元件邊界的文字(在地化頭號 bug),由 AutoControl 既有讀取的 a11y 邊界計算。 +- **目錄完整性** — `check_catalog(base, target)`(`AC_check_catalog`、`ac_check_catalog`):比對翻譯目錄的缺漏/多餘/空白鍵與佔位符不一致——防止空白 UI 的 CI 閘。 + ## 本次更新 (2026-06-19) — 資料品質 三項純標準庫的資料品質輔助工具(介於 `load_rows`/OCR 與下游輸入之間的閘),走完整五層。完整參考:[`docs/source/Zh/doc/new_features/v19_features_doc.rst`](../docs/source/Zh/doc/new_features/v19_features_doc.rst)。 diff --git a/docs/source/Eng/doc/new_features/v20_features_doc.rst b/docs/source/Eng/doc/new_features/v20_features_doc.rst new file mode 100644 index 00000000..61c56290 --- /dev/null +++ b/docs/source/Eng/doc/new_features/v20_features_doc.rst @@ -0,0 +1,61 @@ +================================================== +New Features (2026-06-19) — i18n / l10n Testing +================================================== + +Three pure-standard-library internationalization / localization testing +helpers that compound, wired through the full stack (facade, ``AC_*`` +executor commands, MCP tools, Script Builder). + +.. contents:: + :local: + :depth: 2 + + +Pseudo-localization +================== + +Accent and pad UI strings (preserving placeholders) to flush out hardcoded +text and pre-stress layout *before* any real translation exists:: + + from je_auto_control import pseudo_localize, pseudo_localize_catalog + + pseudo_localize("Hello {name}") # "⟦Hèllo {name}········⟧" + pseudo_localize_catalog({"save": "Save", "cancel": "Cancel"}) + +Placeholders (``{name}`` / ``{{x}}`` / ``%s`` / ``%d``) are preserved +verbatim; ``expansion`` controls the padding fraction; the ``⟦…⟧`` brackets +make truncation visible. Exposed as ``AC_pseudo_localize`` / +``ac_pseudo_localize``. Untranslated (un-accented) strings in a screen are a +sign of unexternalized, hardcoded text. + + +Text-overflow detection +======================= + +Flag text whose estimated width exceeds its widget bounds — the #1 +localization bug (German/Finnish expand 30–50%). Computed from the +accessibility bounds AutoControl already reads:: + + from je_auto_control import check_overflow + + issues = check_overflow(elements, avg_char_px=7.0) + # [{"text": "...", "width": 40, "required_px": 400.0, "overflow_px": 360.0}] + +Width is estimated as ``len(text) * avg_char_px`` (deterministic +heuristic). Exposed as ``AC_check_overflow`` / ``ac_check_overflow`` (uses +the live a11y tree unless ``elements`` are supplied). + + +Catalog completeness +=================== + +Diff a translation catalog against a base locale for missing / orphaned / +empty keys and placeholder mismatches — a CI gate against blank UI:: + + from je_auto_control import check_catalog + + report = check_catalog(base_locale, target_locale) + report["missing"] # keys absent in target + report["placeholder_mismatch"] # e.g. "{count}" dropped in the target + +Exposed as ``AC_check_catalog`` / ``ac_check_catalog``. diff --git a/docs/source/Eng/eng_index.rst b/docs/source/Eng/eng_index.rst index 65203719..a6e6e985 100644 --- a/docs/source/Eng/eng_index.rst +++ b/docs/source/Eng/eng_index.rst @@ -42,6 +42,7 @@ Comprehensive guides for all AutoControl features. doc/new_features/v17_features_doc doc/new_features/v18_features_doc doc/new_features/v19_features_doc + doc/new_features/v20_features_doc doc/ocr_backends/ocr_backends_doc doc/observability/observability_doc doc/operations_layer/operations_layer_doc diff --git a/docs/source/Zh/doc/new_features/v20_features_doc.rst b/docs/source/Zh/doc/new_features/v20_features_doc.rst new file mode 100644 index 00000000..aa9a55bd --- /dev/null +++ b/docs/source/Zh/doc/new_features/v20_features_doc.rst @@ -0,0 +1,58 @@ +========================================== +新功能 (2026-06-19) — i18n / l10n 測試 +========================================== + +三項可互相搭配的純標準庫國際化 / 在地化測試輔助工具,走完整五層 +(facade、``AC_*`` 執行器指令、MCP 工具、Script Builder)。 + +.. contents:: + :local: + :depth: 2 + + +偽在地化(Pseudo-localization) +============================== + +在真正的翻譯出現*之前*,先為 UI 字串加上重音與填充(保留佔位符),以 +揪出寫死的字串並預先對版面施壓:: + + from je_auto_control import pseudo_localize, pseudo_localize_catalog + + pseudo_localize("Hello {name}") # "⟦Hèllo {name}········⟧" + pseudo_localize_catalog({"save": "Save", "cancel": "Cancel"}) + +佔位符(``{name}`` / ``{{x}}`` / ``%s`` / ``%d``)會原樣保留;``expansion`` +控制填充比例;``⟦…⟧`` 括號讓截斷一眼可見。對應 ``AC_pseudo_localize`` / +``ac_pseudo_localize``。畫面中未被加重音(未翻譯)的字串,代表它是未外部化 +的寫死文字。 + + +文字溢位偵測 +============ + +標記估計寬度超過其元件邊界的文字——在地化的頭號 bug(德文/芬蘭文會膨脹 +30–50%)。由 AutoControl 既有讀取的 accessibility 邊界計算:: + + from je_auto_control import check_overflow + + issues = check_overflow(elements, avg_char_px=7.0) + # [{"text": "...", "width": 40, "required_px": 400.0, "overflow_px": 360.0}] + +寬度以 ``len(text) * avg_char_px`` 估計(決定性啟發式)。對應 +``AC_check_overflow`` / ``ac_check_overflow``(未提供 ``elements`` 時使用 +即時 a11y 樹)。 + + +翻譯目錄完整性 +============== + +把翻譯目錄與基準語系比對:缺漏 / 多餘 / 空白鍵與佔位符不一致——可作為防止 +空白 UI 的 CI 閘:: + + from je_auto_control import check_catalog + + report = check_catalog(base_locale, target_locale) + report["missing"] # target 中缺漏的鍵 + report["placeholder_mismatch"] # 例如 target 漏掉 "{count}" + +對應 ``AC_check_catalog`` / ``ac_check_catalog``。 diff --git a/docs/source/Zh/zh_index.rst b/docs/source/Zh/zh_index.rst index bf01290f..bdc9d1ca 100644 --- a/docs/source/Zh/zh_index.rst +++ b/docs/source/Zh/zh_index.rst @@ -42,6 +42,7 @@ AutoControl 所有功能的完整使用指南。 doc/new_features/v17_features_doc doc/new_features/v18_features_doc doc/new_features/v19_features_doc + doc/new_features/v20_features_doc doc/ocr_backends/ocr_backends_doc doc/observability/observability_doc doc/operations_layer/operations_layer_doc diff --git a/je_auto_control/__init__.py b/je_auto_control/__init__.py index 639011af..36b2ce4c 100644 --- a/je_auto_control/__init__.py +++ b/je_auto_control/__init__.py @@ -157,6 +157,10 @@ from je_auto_control.utils.data_quality import ( extract_fields, mask_rows, validate_rows, ) +# i18n / l10n testing: pseudo-localize, overflow + catalog checks +from je_auto_control.utils.i18n_test import ( + check_catalog, check_overflow, pseudo_localize, pseudo_localize_catalog, +) # Background popup/interrupt watchdog (unattended automation) from je_auto_control.utils.watchdog import ( PopupWatchdog, WatchdogRule, default_popup_watchdog, @@ -577,6 +581,8 @@ def start_autocontrol_gui(*args, **kwargs): "build_sbom", "write_sbom", "merge_results", "shard_flows", "extract_fields", "mask_rows", "validate_rows", + "check_catalog", "check_overflow", "pseudo_localize", + "pseudo_localize_catalog", # MCP server "AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt", "MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool", diff --git a/je_auto_control/gui/script_builder/command_schema.py b/je_auto_control/gui/script_builder/command_schema.py index 3252f867..a1bcc0b7 100644 --- a/je_auto_control/gui/script_builder/command_schema.py +++ b/je_auto_control/gui/script_builder/command_schema.py @@ -659,6 +659,7 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None: _add_office_specs(specs) _add_memory_specs(specs) _add_data_quality_specs(specs) + _add_i18n_specs(specs) specs.append(CommandSpec( "AC_wcag_audit", "Accessibility", "WCAG 2.2 Conformance Audit", fields=( @@ -739,6 +740,33 @@ def _add_observer_specs(specs: List[CommandSpec]) -> None: description="Stop the background observer thread.")) +def _add_i18n_specs(specs: List[CommandSpec]) -> None: + specs.append(CommandSpec( + "AC_pseudo_localize", "Data", "Pseudo-Localize", + fields=( + FieldSpec("text", FieldType.STRING, optional=True), + FieldSpec("expansion", FieldType.FLOAT, optional=True, + default=0.4), + ), + description="Accent+pad a string (or 'mapping' via JSON view) for " + "i18n stress testing.", + )) + specs.append(CommandSpec( + "AC_check_overflow", "Data", "Check Text Overflow", + fields=( + FieldSpec("app_name", FieldType.STRING, optional=True), + FieldSpec("avg_char_px", FieldType.FLOAT, optional=True, + default=7.0), + ), + description="Flag text wider than its widget (translation overflow).", + )) + specs.append(CommandSpec( + "AC_check_catalog", "Data", "Check Translation Catalog", + description="Diff 'target' vs 'base' catalog (JSON view): missing / " + "empty / placeholder mismatch.", + )) + + def _add_data_quality_specs(specs: List[CommandSpec]) -> None: specs.append(CommandSpec( "AC_validate_rows", "Data", "Validate Rows (schema)", diff --git a/je_auto_control/utils/executor/action_executor.py b/je_auto_control/utils/executor/action_executor.py index cd325b2c..a3129ef0 100644 --- a/je_auto_control/utils/executor/action_executor.py +++ b/je_auto_control/utils/executor/action_executor.py @@ -2685,6 +2685,38 @@ def _mask_rows(rows: List[Dict[str, Any]], return {"rows": mask_rows(rows, rules)} +def _pseudo_localize(text: Optional[str] = None, + mapping: Optional[Dict[str, Any]] = None, + expansion: float = 0.4) -> Dict[str, Any]: + """Adapter: pseudo-localize a string or a whole catalog mapping.""" + from je_auto_control.utils.i18n_test import ( + pseudo_localize, pseudo_localize_catalog) + if mapping is not None: + return {"catalog": pseudo_localize_catalog( + mapping, expansion=float(expansion))} + return {"text": pseudo_localize(text or "", expansion=float(expansion))} + + +def _check_overflow(elements: Optional[List[Any]] = None, + avg_char_px: float = 7.0, + app_name: Optional[str] = None) -> Dict[str, Any]: + """Adapter: flag text wider than its widget (live a11y unless given).""" + from je_auto_control.utils.i18n_test import check_overflow + items = elements + if items is None: + from je_auto_control.utils.accessibility.accessibility_api import ( + list_accessibility_elements) + items = list_accessibility_elements(app_name=app_name) + return {"issues": check_overflow(items, avg_char_px=float(avg_char_px))} + + +def _check_catalog(base: Dict[str, Any], + target: Dict[str, Any]) -> Dict[str, Any]: + """Adapter: diff a translation catalog against the base locale.""" + from je_auto_control.utils.i18n_test import check_catalog + return check_catalog(base, target) + + class Executor: """ Executor @@ -2889,6 +2921,9 @@ def __init__(self): "AC_validate_rows": _validate_rows, "AC_extract_fields": _extract_fields, "AC_mask_rows": _mask_rows, + "AC_pseudo_localize": _pseudo_localize, + "AC_check_overflow": _check_overflow, + "AC_check_catalog": _check_catalog, "AC_a11y_record_start": _a11y_record_start, "AC_a11y_record_stop": _a11y_record_stop, "AC_a11y_record_events": _a11y_record_events, diff --git a/je_auto_control/utils/i18n_test/__init__.py b/je_auto_control/utils/i18n_test/__init__.py new file mode 100644 index 00000000..168ab425 --- /dev/null +++ b/je_auto_control/utils/i18n_test/__init__.py @@ -0,0 +1,9 @@ +"""Internationalization / localization testing helpers.""" +from je_auto_control.utils.i18n_test.i18n_test import ( + check_catalog, check_overflow, pseudo_localize, pseudo_localize_catalog, +) + +__all__ = [ + "check_catalog", "check_overflow", "pseudo_localize", + "pseudo_localize_catalog", +] diff --git a/je_auto_control/utils/i18n_test/i18n_test.py b/je_auto_control/utils/i18n_test/i18n_test.py new file mode 100644 index 00000000..cdbeafbf --- /dev/null +++ b/je_auto_control/utils/i18n_test/i18n_test.py @@ -0,0 +1,121 @@ +"""Internationalization / localization (i18n / l10n) testing helpers. + +Three pure-standard-library checks that compound: + +* :func:`pseudo_localize` accents and pads UI strings (preserving + placeholders) to flush out hardcoded text and pre-stress layout *before* + any real translation exists. +* :func:`check_overflow` flags text whose estimated width exceeds its + widget bounds — the #1 l10n bug, computable from the accessibility bounds + AutoControl already reads. +* :func:`check_catalog` diffs a translation catalog against a base locale + for missing / empty / orphaned keys and placeholder mismatches. + +Imports no ``PySide6``; no third-party dependency. +""" +import re +from typing import Any, Dict, List + +# Accent map for common Latin letters (pseudo-localization). +_ACCENTS = str.maketrans( + "aeiouAEIOUncysNCYS", "àèìòùÀÈÌÒÙñçÿśÑÇÝŚ") +# Placeholders to preserve verbatim: {name}, {{x}}, %s, %d, {0}. +_PLACEHOLDER = re.compile(r"\{\{[^}]*\}\}|\{[^}]*\}|%[sd]") +_SENTINEL = re.compile("\x00(\\d+)\x00") + + +def pseudo_localize(text: str, *, expansion: float = 0.4, + accent: bool = True, brackets: bool = True) -> str: + """Return a pseudo-localized copy of ``text`` (placeholders preserved). + + Accents Latin letters, pads by ``expansion`` (fraction of length) to + mimic translation growth, and wraps in ``⟦…⟧`` so truncation is visible. + """ + source = text or "" + holders: List[str] = [] + + def stash(match: "re.Match") -> str: + holders.append(match.group(0)) + return f"\x00{len(holders) - 1}\x00" + + protected = _PLACEHOLDER.sub(stash, source) + body = protected.translate(_ACCENTS) if accent else protected + body += "·" * max(0, round(len(protected) * float(expansion))) + restored = _SENTINEL.sub(lambda m: holders[int(m.group(1))], body) + return f"⟦{restored}⟧" if brackets else restored + + +def pseudo_localize_catalog(mapping: Dict[str, Any], + **kwargs: Any) -> Dict[str, str]: + """Apply :func:`pseudo_localize` to every value of a catalog mapping.""" + return {key: pseudo_localize(str(value), **kwargs) + for key, value in mapping.items()} + + +def _text_of(element: Any) -> str: + if isinstance(element, dict): + return str(element.get("text") or element.get("name") or "") + return str(getattr(element, "name", "") or "") + + +def _bounds_of(element: Any) -> List[int]: + if isinstance(element, dict): + raw = element.get("bbox") or element.get("bounds") or [] + else: + raw = getattr(element, "bounds", []) or [] + return list(raw) + + +def check_overflow(elements: List[Any], *, + avg_char_px: float = 7.0) -> List[Dict[str, Any]]: + """Flag elements whose estimated text width exceeds their widget width. + + Width is estimated as ``len(text) * avg_char_px`` (a deterministic + heuristic); each issue is ``{text, width, required_px, overflow_px}``. + """ + issues: List[Dict[str, Any]] = [] + for element in elements: + text = _text_of(element) + bounds = _bounds_of(element) + if not text or len(bounds) < 4 or bounds[2] <= 0: + continue + width = bounds[2] + required = len(text) * float(avg_char_px) + if required > width: + issues.append({"text": text, "width": width, + "required_px": round(required, 1), + "overflow_px": round(required - width, 1)}) + return issues + + +def _placeholders(value: Any) -> set: + return set(_PLACEHOLDER.findall(str(value))) + + +def _empty_keys(base: Dict[str, Any], target: Dict[str, Any]) -> List[str]: + return sorted(key for key in target + if key in base and not str(target[key]).strip()) + + +def _mismatch_keys(base: Dict[str, Any], target: Dict[str, Any]) -> List[str]: + return sorted( + key for key in base + if key in target + and _placeholders(base[key]) != _placeholders(target[key])) + + +def check_catalog(base: Dict[str, Any], + target: Dict[str, Any]) -> Dict[str, Any]: + """Diff a translation ``target`` catalog against the ``base`` locale. + + Returns ``{ok, missing, orphaned, empty, placeholder_mismatch}``: + keys absent in target, keys only in target, blank target values, and + keys whose placeholder set differs from the base. + """ + missing = sorted(key for key in base if key not in target) + orphaned = sorted(key for key in target if key not in base) + empty = _empty_keys(base, target) + mismatch = _mismatch_keys(base, target) + return {"ok": not (missing or empty or mismatch), + "missing": missing, "orphaned": orphaned, + "empty": empty, "placeholder_mismatch": mismatch} diff --git a/je_auto_control/utils/mcp_server/tools/_factories.py b/je_auto_control/utils/mcp_server/tools/_factories.py index c96de7b8..0b3905fb 100644 --- a/je_auto_control/utils/mcp_server/tools/_factories.py +++ b/je_auto_control/utils/mcp_server/tools/_factories.py @@ -2225,6 +2225,50 @@ def data_quality_tools() -> List[MCPTool]: ] +def i18n_tools() -> List[MCPTool]: + return [ + MCPTool( + name="ac_pseudo_localize", + description=("Pseudo-localize a 'text' string or a 'mapping' " + "catalog (accent + pad + bracket, placeholders " + "preserved) to flush out hardcoded strings and " + "pre-stress layout before real translation."), + input_schema=schema({ + "text": {"type": "string"}, + "mapping": {"type": "object"}, + "expansion": {"type": "number"}, + }), + handler=h.pseudo_localize, + annotations=READ_ONLY, + ), + MCPTool( + name="ac_check_overflow", + description=("Flag text elements whose estimated width exceeds " + "their widget bounds (translation overflow). Uses " + "the live a11y tree unless 'elements' are supplied."), + input_schema=schema({ + "elements": {"type": "array"}, + "avg_char_px": {"type": "number"}, + "app_name": {"type": "string"}, + }), + handler=h.check_overflow, + annotations=READ_ONLY, + ), + MCPTool( + name="ac_check_catalog", + description=("Diff a translation 'target' catalog against 'base': " + "missing / orphaned / empty keys and placeholder " + "mismatches. Returns {ok, ...}."), + input_schema=schema({ + "base": {"type": "object"}, + "target": {"type": "object"}, + }, required=["base", "target"]), + handler=h.check_catalog, + annotations=READ_ONLY, + ), + ] + + def unattended_tools() -> List[MCPTool]: return [ MCPTool( @@ -3278,7 +3322,7 @@ def media_assert_tools() -> List[MCPTool]: element_repository_tools, flow_debugger_tools, skill_library_tools, guardrail_tools, a2a_tools, office_tools, agent_memory_tools, determinism_tools, observer_tools, - sbom_tools, sharding_tools, data_quality_tools, + sbom_tools, sharding_tools, data_quality_tools, i18n_tools, screen_record_tools, process_and_shell_tools, remote_desktop_tools, gamepad_tools, usb_passthrough_tools, assertion_tools, data_source_tools, diff --git a/je_auto_control/utils/mcp_server/tools/_handlers.py b/je_auto_control/utils/mcp_server/tools/_handlers.py index 0796cea6..e4f298ad 100644 --- a/je_auto_control/utils/mcp_server/tools/_handlers.py +++ b/je_auto_control/utils/mcp_server/tools/_handlers.py @@ -1089,6 +1089,29 @@ def mask_rows(rows, rules): return {"rows": _mask(rows, rules)} +def pseudo_localize(text=None, mapping=None, expansion=0.4): + from je_auto_control.utils.i18n_test import ( + pseudo_localize as _pl, pseudo_localize_catalog as _plc) + if mapping is not None: + return {"catalog": _plc(mapping, expansion=float(expansion))} + return {"text": _pl(text or "", expansion=float(expansion))} + + +def check_overflow(elements=None, avg_char_px=7.0, app_name=None): + from je_auto_control.utils.i18n_test import check_overflow as _co + items = elements + if items is None: + from je_auto_control.utils.accessibility.accessibility_api import ( + list_accessibility_elements) + items = list_accessibility_elements(app_name=app_name) + return {"issues": _co(items, avg_char_px=float(avg_char_px))} + + +def check_catalog(base, target): + from je_auto_control.utils.i18n_test import check_catalog as _cc + return _cc(base, target) + + def vlm_locate(description: str, screen_region: Optional[List[int]] = None, model: Optional[str] = None) -> Optional[List[int]]: diff --git a/test/unit_test/headless/test_i18n_batch.py b/test/unit_test/headless/test_i18n_batch.py new file mode 100644 index 00000000..228fa73d --- /dev/null +++ b/test/unit_test/headless/test_i18n_batch.py @@ -0,0 +1,90 @@ +"""Headless tests for the i18n/l10n batch: pseudo-localization, text-overflow +detection, and translation-catalog diffing. Pure stdlib; no Qt imports.""" +import je_auto_control as ac +from je_auto_control.utils.i18n_test import ( + check_catalog, check_overflow, pseudo_localize, pseudo_localize_catalog) + + +# --- pseudo-localization -------------------------------------------------- + +def test_pseudo_localize_pads_and_preserves_placeholders(): + out = pseudo_localize("Hello {name}", expansion=0.5) + assert out.startswith("⟦") and out.endswith("⟧") + assert "{name}" in out # placeholder intact + assert len(out) > len("Hello {name}") + 2 # padded/expanded + assert "Hèllo" in out or "Hèllò" in out # accented + + +def test_pseudo_localize_no_brackets_no_accent(): + out = pseudo_localize("OK", accent=False, brackets=False, expansion=0) + assert out == "OK" + + +def test_pseudo_localize_catalog(): + cat = pseudo_localize_catalog({"a": "Save", "b": "Cancel {x}"}) + assert set(cat) == {"a", "b"} + assert "{x}" in cat["b"] + + +# --- overflow detection --------------------------------------------------- + +def test_check_overflow_flags_wide_text(): + elements = [ + {"text": "short", "bbox": [0, 0, 200, 20]}, # fits + {"text": "x" * 50, "bbox": [0, 0, 40, 20]}, # overflows + {"text": "", "bbox": [0, 0, 5, 20]}, # no text + ] + issues = check_overflow(elements, avg_char_px=8.0) + assert len(issues) == 1 + assert issues[0]["overflow_px"] > 0 + assert issues[0]["text"] == "x" * 50 + + +# --- catalog diff --------------------------------------------------------- + +def test_check_catalog_reports_problems(): + base = {"hi": "Hello {n}", "bye": "Bye", "only_base": "x"} + target = {"hi": "Hallo", "bye": " ", "extra": "y"} + report = check_catalog(base, target) + assert report["ok"] is False + assert report["missing"] == ["only_base"] + assert report["orphaned"] == ["extra"] + assert report["empty"] == ["bye"] + assert report["placeholder_mismatch"] == ["hi"] # {n} dropped + + +def test_check_catalog_clean(): + report = check_catalog({"a": "A {x}"}, {"a": "Ä {x}"}) + assert report["ok"] is True + + +# --- wiring --------------------------------------------------------------- + +def test_executor_wiring(): + rec = ac.execute_action([["AC_pseudo_localize", {"text": "Hi {x}"}]]) + assert any("{x}" in str(v) for v in rec.values()) + cat = ac.execute_action([["AC_check_catalog", { + "base": {"a": "A"}, "target": {}}]]) + assert any("missing" in str(v) for v in cat.values()) + known = ac.executor.known_commands() + assert {"AC_pseudo_localize", "AC_check_overflow", + "AC_check_catalog"} <= known + + +def test_mcp_and_builder_wiring(): + from je_auto_control.utils.mcp_server.tools import ( + build_default_tool_registry) + names = {t.name for t in build_default_tool_registry()} + assert {"ac_pseudo_localize", "ac_check_overflow", + "ac_check_catalog"} <= names + from je_auto_control.gui.script_builder.command_schema import _build_specs + cmds = {s.command for s in _build_specs()} + assert {"AC_pseudo_localize", "AC_check_overflow", + "AC_check_catalog"} <= cmds + + +def test_facade_exports(): + for attr in ("pseudo_localize", "pseudo_localize_catalog", + "check_overflow", "check_catalog"): + assert hasattr(ac, attr) + assert attr in ac.__all__