Batch of fixes for defects tracked in "Task Tracking Template.xlsx" (sheet Defect Management), verified against the sheet's Root Cause/Cach xu ly columns before this commit: - DF-002: Co4E node status not reflected after tab switch + missing edit-lock on running/done nodes (node_property_panel.py, co4e_runs.py, co4e_workflow_crud.py, co4e_canvas_widget.py, co4e_flow_tabs.py, canvas_items.py) - DF-003: hide the run.bat console window unless the app exits with an error (run.bat, scripts/console_visibility.ps1 - new) - DF-004: floating Help Assistant icon covering the Send button after a window resize (presentation/shell/main_window.py) - DF-005: "block network" toggle didn't stop ICMP/raw-socket tools like ping (infrastructure/filesystem/command_tools.py, security/command_risk_classifier.py) - DF-006: Monitoring "gay nang khi log lon" - root cause was re-reading the ENTIRE audit log history every 3s tick, not missing pagination; bounded to a 30-day window (presentation/monitoring/monitoring_tab.py) AND added the "So dong/trang" page-size control the ticket also asked for (presentation/monitoring/shared/event_table.py, shared/filter_scaffold.py, tabs/action_logs_tab.py, tabs/mcp_tab.py, tabs/security_events_tab.py, i18n/agents_admin_tab.py) - DF-007: support choosing a OneDrive/SharePoint folder as a project's working directory via Microsoft Graph, downloaded as a local mirror with manual sync (core/projects.py, core/ms365_graph.py, core/cloud_workspace_sync.py - new, ui/ms365_signin_dialog.py - new, ui/cloud_folder_picker_dialog.py - new, i18n/cloud_workspace.py - new, ui/workspace_tab.py) - DF-008: AI-edit instruction box was a fixed-height single-line QLineEdit; replaced with an auto-expanding, Enter-to-send/Shift+Enter-newline input (presentation/folder/ai_file_editor_dialog.py) - DF-011: run_command failed with WinError 267 for a project whose per-turn output directory had never been created (application/conversations/core_runtime_adapter.py) DF-009 (AI-edit Apply/Discard buttons easy to miss) and DF-010 (AI reply language - dev-confirmed not a bug) are intentionally NOT part of this commit: DF-009 has no code fix yet (still "Assigned" in the sheet, only a UX recommendation was recorded), DF-010 was rejected as expected behavior. Tests: tests/test_cloud_workspace_sync.py, tests/test_ms365_cloud_dialogs.py, tests/test_ai_file_editor_input.py, tests/test_monitoring_page_size.py (all new, all passing). Full suite: 896 passed, 13 known-and-documented failures unrelated to this change (an existing core/audit_log.py bug, this checkout not being a git repo before now, and a repo/subprocess folder-naming mismatch affecting ~66 characterization tests) - see the sheet's DF-006 Evidence column for details.
104 lines
3.7 KiB
Python
104 lines
3.7 KiB
Python
"""Runtime UI translation: English / Japanese / Vietnamese.
|
|
|
|
``tr(key, **kwargs)`` returns the string for the current language (falling
|
|
back to English, then the key itself so a missing entry is still visible
|
|
instead of crashing). ``.format(**kwargs)`` is applied when placeholders are
|
|
passed, so callers can do e.g. ``tr("composer.attachments", n=3)``.
|
|
|
|
Persistent, long-lived widgets (the main window chrome, the tabs, the
|
|
sidebar, the composer, ...) must reflect a language change immediately, so
|
|
they register a zero-arg callback via :func:`on_language_changed` that
|
|
re-applies ``tr()`` to their own text; the callback runs once right away and
|
|
again every time the language changes. Transient dialogs (Settings, Skills,
|
|
Flow, Permission...) are rebuilt from scratch each time they are opened, so
|
|
they simply call ``tr()`` while constructing their widgets and need no
|
|
registration.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Callable, Dict, List
|
|
|
|
LANGUAGES: Dict[str, str] = {"en": "English", "ja": "日本語", "vi": "Tiếng Việt"}
|
|
# Short codes shown in the compact top-bar switcher (Settings keeps the full names above).
|
|
LANGUAGE_SHORT: Dict[str, str] = {"en": "EN", "ja": "JP", "vi": "VN"}
|
|
DEFAULT_LANGUAGE = "vi"
|
|
|
|
_current = DEFAULT_LANGUAGE
|
|
_listeners: List[Callable[[], None]] = []
|
|
|
|
# key -> {"en": ..., "ja": ..., "vi": ...}
|
|
from . import login_dialog as _login_dialog
|
|
from . import sidebar as _sidebar
|
|
from . import composer as _composer
|
|
from . import hint as _hint
|
|
from . import cowork_tab as _cowork_tab
|
|
from . import settings_dialog as _settings_dialog
|
|
from . import skills_dialog as _skills_dialog
|
|
from . import libreoffice_view as _libreoffice_view
|
|
from . import agents_admin_tab as _agents_admin_tab
|
|
from . import monitoring_overview as _monitoring_overview
|
|
from . import cloud_workspace as _cloud_workspace
|
|
|
|
# Gộp theo đúng thứ tự cũ: khoá trùng thì cụm sau thắng, y như khi tất cả
|
|
# còn nằm chung một dict literal.
|
|
STRINGS: Dict[str, Dict[str, str]] = {
|
|
**_login_dialog.STRINGS,
|
|
**_sidebar.STRINGS,
|
|
**_composer.STRINGS,
|
|
**_hint.STRINGS,
|
|
**_cowork_tab.STRINGS,
|
|
**_settings_dialog.STRINGS,
|
|
**_skills_dialog.STRINGS,
|
|
**_libreoffice_view.STRINGS,
|
|
**_agents_admin_tab.STRINGS,
|
|
**_monitoring_overview.STRINGS,
|
|
**_cloud_workspace.STRINGS,
|
|
}
|
|
|
|
|
|
def set_language(lang: str) -> None:
|
|
"""Switch the active language and notify every registered persistent widget."""
|
|
global _current
|
|
if lang not in LANGUAGES:
|
|
lang = DEFAULT_LANGUAGE
|
|
if lang == _current:
|
|
return
|
|
_current = lang
|
|
for fn in list(_listeners):
|
|
try:
|
|
fn()
|
|
except RuntimeError:
|
|
# The widget behind this callback was already destroyed — drop it.
|
|
try:
|
|
_listeners.remove(fn)
|
|
except ValueError:
|
|
pass
|
|
|
|
|
|
def get_language() -> str:
|
|
"""Mã ngôn ngữ đang dùng."""
|
|
return _current
|
|
|
|
|
|
def tr(key: str, **kwargs) -> str:
|
|
"""Chuỗi đã dịch cho một khoá.
|
|
|
|
Thiếu khoá thì trả về CHÍNH khoá đó — hiện ra một chuỗi lạ trên giao diện
|
|
vẫn tốt hơn là làm vỡ màn hình. Thiếu bản dịch của ngôn ngữ hiện tại thì rơi
|
|
về tiếng Anh.
|
|
"""
|
|
entry = STRINGS.get(key)
|
|
if not entry:
|
|
return key
|
|
text = entry.get(_current) or entry.get("en") or next(iter(entry.values()), key)
|
|
return text.format(**kwargs) if kwargs else text
|
|
|
|
|
|
def on_language_changed(fn: Callable[[], None]) -> None:
|
|
"""Register a callback that re-applies translations to a persistent widget.
|
|
|
|
Called once immediately (to apply the current language) and again on every
|
|
future call to :func:`set_language`."""
|
|
_listeners.append(fn)
|
|
fn()
|