Files
cowork-local/i18n/__init__.py
T
vudt15 2a5ee29c2c fix(qa): resolve DF-002 through DF-011 from QA defect tracking sheet
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.
2026-09-07 21:22:00 +09:00

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()