fix(i18n): dịch nốt chữ do Qt tự vẽ, và thêm dòng phiên bản ở góc phải
CI / test (pull_request) Canceled after 0s
CI / test (pull_request) Canceled after 0s
Người dùng báo: chọn tiếng Nhật mà nhóm Sandbox Security, nút Save/Cancel và
nhiều chỗ khác vẫn tiếng Anh. Bộ test i18n cũ vẫn xanh vì nó chỉ bắt lỗi "có
dịch nhưng không ai áp lại" — hai lỗ thật nằm chỗ khác:
* Chuỗi HARDCODE không đi qua ``tr()`` bao giờ (``QPushButton("Unlock")``), nên
phép đo "đổi ngôn ngữ rồi tìm chỗ không mang mốc" thấy nó đứng yên ở cả hai
lần chụp và coi là bình thường.
* Nhãn nút do CHÍNH Qt vẽ. ``QDialogButtonBox``, ``QMessageBox.question`` và
``QInputDialog.get*`` lấy chữ từ bảng dịch riêng của Qt; ứng dụng không cài
``QTranslator`` nào và bản PySide6 đang dùng cũng không đóng gói file
``qtbase_*.qm`` nào để cài — nên chúng luôn rơi về tiếng Anh.
``ui/dialog_buttons.py`` gán nhãn của dự án đè lên nhãn Qt: ``dialog_buttons``
(10 hộp thoại), ``confirm`` (13 hộp Có/Không), ``ask_text``/``ask_multiline``/
``ask_item`` (16 hộp nhập liệu). Cùng với 17 chuỗi hardcode và 5 câu lỗi mà
``core/tasks.py`` trả thẳng ra hộp thoại — nay trả KHOÁ i18n, nơi hiển thị mới
gọi ``tr()`` — là 61 chỗ.
Ba chỗ nữa cùng lớp lỗi, phát hiện khi rà lại:
* ``_add_section`` nhận chuỗi ĐÃ dịch nên bốn tiêu đề mục của Step config đứng
nguyên ở ngôn ngữ lúc dựng panel. Nay nhận khoá + ``bind_dynamic`` để không
mất trạng thái gập/mở khi đổi ngôn ngữ.
* Thẻ tool ở Giám sát ▸ Công cụ hiện thẳng ``spec.description`` — chuỗi gửi cho
MÔ HÌNH trong schema function-calling, phải giữ tiếng Anh. Thêm bộ mô tả hiển
thị riêng cho 9 tool.
* Tên nhóm catalog ở tab Connector ("Other (any generic MCP server)").
Kèm theo, phần giao diện người dùng yêu cầu:
* ``__version__`` 2.26.0 -> 0.0.1, một nguồn cho tiêu đề cửa sổ, tab Giới thiệu
và dòng mới ở góc phải thanh trạng thái (thay dòng ghi công tác giả).
* Tắt size grip: nó vẽ một vệt ngay bên phải dòng phiên bản. Cửa sổ vẫn kéo
giãn được từ các cạnh.
* ``_NAV_SETTINGS_GAP`` 10 -> 4: hàng Cài đặt bớt xa nhóm Dashboard/Giám sát.
Hai test SẼ TREO nếu không sửa kèm: chúng patch ``QInputDialog.getText/getItem``
để tự trả lời, mà code nay gọi ``ask_text``/``ask_item`` — patch không còn chặn
được và hộp thoại thật sẽ mở ra chờ người bấm.
Ba cổng mới trong ``tests/ui/test_i18n_khong_hardcode_chu.py`` canh ở mức cấu
trúc (không ai được dựng lại kiểu cũ); đã kiểm chúng CẮN trên bản trước khi sửa:
10 + 13 + 17 vi phạm.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
thanhnv
co-authored by
Claude Opus 5
parent
aeecdf5da0
commit
d2101e1f87
@@ -34,6 +34,7 @@ from PySide6.QtGui import QBrush, QColor, QPainterPath, QPen, QPolygonF
|
||||
from PySide6.QtWidgets import QGraphicsItem, QGraphicsObject, QGraphicsPathItem, QMenu
|
||||
|
||||
from ...core.co4e import STEP_DONE, STEP_ERROR, STEP_PLANNED, STEP_RUNNING, Edge, Node
|
||||
from ...i18n import tr
|
||||
from ...theme import current_palette
|
||||
from .canvas_geometry import _elide, _rounded_path
|
||||
|
||||
@@ -233,9 +234,9 @@ class _NodeItem(QGraphicsObject):
|
||||
def contextMenuEvent(self, e):
|
||||
"""Menu chuột phải trên node: thêm bước kế, nối từ đây, xoá bước."""
|
||||
menu = QMenu()
|
||||
a_add = menu.addAction("+ Add next step")
|
||||
a_conn = menu.addAction("→ Connect from here")
|
||||
a_del = menu.addAction("🗑 Delete step")
|
||||
a_add = menu.addAction("+ " + tr("co4e.canvas_add_next"))
|
||||
a_conn = menu.addAction("→ " + tr("co4e.canvas_connect_from"))
|
||||
a_del = menu.addAction("🗑 " + tr("co4e.delete_step"))
|
||||
chosen = menu.exec(e.screenPos())
|
||||
if chosen is a_add:
|
||||
self.canvas.add_step_below(self.node.id)
|
||||
@@ -342,7 +343,7 @@ class _EdgeItem(QGraphicsPathItem):
|
||||
def contextMenuEvent(self, e):
|
||||
"""Menu chuột phải trên đường nối: xoá liên kết."""
|
||||
menu = QMenu()
|
||||
act_del = menu.addAction("🗑 Delete connection")
|
||||
act_del = menu.addAction("🗑 " + tr("co4e.canvas_delete_edge"))
|
||||
if menu.exec(e.screenPos()) is act_del:
|
||||
self.canvas.delete_edge(self.edge)
|
||||
e.accept()
|
||||
|
||||
@@ -13,7 +13,7 @@ import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
from PySide6.QtCore import QSize, Qt
|
||||
from PySide6.QtWidgets import QInputDialog, QMenu, QMessageBox, QTableWidget, QTableWidgetItem
|
||||
from PySide6.QtWidgets import QMenu, QMessageBox, QTableWidget, QTableWidgetItem
|
||||
from ...core import co4e
|
||||
from ...i18n import tr
|
||||
from ...theme import current_palette
|
||||
@@ -337,9 +337,9 @@ class Co4ERunsMixin:
|
||||
h = self.manager.get(run_id)
|
||||
if h is None:
|
||||
return
|
||||
from PySide6.QtWidgets import QInputDialog
|
||||
new, ok = QInputDialog.getText(self, tr("co4e.rename_run"),
|
||||
tr("co4e.rename_run_label"), text=h.name)
|
||||
from ...ui.dialog_buttons import ask_text
|
||||
new, ok = ask_text(self, tr("co4e.rename_run"),
|
||||
tr("co4e.rename_run_label"), text=h.name)
|
||||
new = (new or "").strip()
|
||||
if not ok or not new or new == h.name:
|
||||
return
|
||||
|
||||
@@ -8,10 +8,11 @@ from __future__ import annotations
|
||||
import re
|
||||
from typing import List, Optional
|
||||
from PySide6.QtCore import QSize, Qt
|
||||
from PySide6.QtWidgets import QInputDialog, QMenu
|
||||
from PySide6.QtWidgets import QMenu
|
||||
from ...core import co4e
|
||||
from ...core.co4e import STEP_DONE, STEP_RUNNING
|
||||
from ...i18n import tr
|
||||
from ...ui.dialog_buttons import ask_text
|
||||
from ...ui.icons import icon
|
||||
from ...presentation.co4e.co4e_chat_view import _skill_names
|
||||
|
||||
@@ -118,8 +119,8 @@ class Co4EWorkflowCrudMixin:
|
||||
wf = co4e.get_workflow(ident)
|
||||
if wf is None:
|
||||
return
|
||||
name, ok = QInputDialog.getText(self, tr("co4e.rename"), tr("co4e.rename_prompt"),
|
||||
text=wf.name)
|
||||
name, ok = ask_text(self, tr("co4e.rename"), tr("co4e.rename_prompt"),
|
||||
text=wf.name)
|
||||
name = (name or "").strip()
|
||||
if not ok or not name:
|
||||
return
|
||||
|
||||
@@ -24,20 +24,21 @@ thứ tự kế thừa không ảnh hưởng hành vi (khác trường hợp
|
||||
``co4e_canvas_widget.py``, nơi thứ tự mixin-trước-Qt-base là bắt buộc vì có
|
||||
override trùng tên).
|
||||
|
||||
Import trong từng method giữ nguyên y hệt bản gốc (kể cả các import cục bộ có
|
||||
vẻ thừa như ``from PySide6.QtWidgets import QInputDialog`` lặp lại bên trong
|
||||
``_add_subagent``/``_edit_subagent`` dù đã có ở top-level) — chỉ số cấp `..`
|
||||
được nâng lên `...` cho khớp việc file dời từ ``ui/`` (cách gốc 2 cấp) sang
|
||||
``presentation/co4e/`` (cách gốc 3 cấp).
|
||||
Import trong từng method giữ nguyên y hệt bản gốc — chỉ số cấp `..` được nâng
|
||||
lên `...` cho khớp việc file dời từ ``ui/`` (cách gốc 2 cấp) sang
|
||||
``presentation/co4e/`` (cách gốc 3 cấp). Riêng các lời gọi ``QInputDialog``
|
||||
đã chuyển sang ``ui.dialog_buttons``: hàm tĩnh của Qt tự dựng hộp thoại bên
|
||||
trong nên nút "Cancel" của nó luôn là tiếng Anh.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List
|
||||
|
||||
from PySide6.QtWidgets import QInputDialog, QListWidgetItem
|
||||
from PySide6.QtWidgets import QListWidgetItem
|
||||
|
||||
from ...core.co4e import SubAgent
|
||||
from ...i18n import tr
|
||||
from ...ui.dialog_buttons import ask_item, ask_multiline, ask_text
|
||||
|
||||
|
||||
class _StepConfigActionsMixin:
|
||||
@@ -67,14 +68,12 @@ class _StepConfigActionsMixin:
|
||||
"""Thêm một sub-agent vào bước đang chọn (chạy song song trong bước đó)."""
|
||||
if self._step is None:
|
||||
return
|
||||
from PySide6.QtWidgets import QInputDialog
|
||||
|
||||
names = self._available_agent_names()
|
||||
if names:
|
||||
name, ok = QInputDialog.getItem(self, tr("co4e.pick_agent"), tr("co4e.pick_agent"),
|
||||
names, 0, True) # editable: can type a new one
|
||||
name, ok = ask_item(self, tr("co4e.pick_agent"), tr("co4e.pick_agent"),
|
||||
names, 0, True) # editable: can type a new one
|
||||
else:
|
||||
name, ok = QInputDialog.getText(self, tr("co4e.pick_agent"), tr("co4e.pick_agent"))
|
||||
name, ok = ask_text(self, tr("co4e.pick_agent"), tr("co4e.pick_agent"))
|
||||
name = (name or "").strip()
|
||||
if not ok or not name:
|
||||
return
|
||||
@@ -89,13 +88,11 @@ class _StepConfigActionsMixin:
|
||||
row = self.sub_list.row(item)
|
||||
if not (0 <= row < len(self._step.sub_agents)):
|
||||
return
|
||||
from PySide6.QtWidgets import QInputDialog
|
||||
|
||||
names = self._available_agent_names()
|
||||
cur = self._step.sub_agents[row].agent
|
||||
start = names.index(cur) if cur in names else 0
|
||||
name, ok = QInputDialog.getItem(self, tr("co4e.pick_agent"), tr("co4e.pick_agent"),
|
||||
names or [cur], start, True)
|
||||
name, ok = ask_item(self, tr("co4e.pick_agent"), tr("co4e.pick_agent"),
|
||||
names or [cur], start, True)
|
||||
name = (name or "").strip()
|
||||
if ok and name:
|
||||
self._step.sub_agents[row].agent = name
|
||||
@@ -151,7 +148,7 @@ class _StepConfigActionsMixin:
|
||||
role = self.role_edit.text().strip()
|
||||
if not name and not role:
|
||||
return
|
||||
hint, ok = QInputDialog.getMultiLineText(
|
||||
hint, ok = ask_multiline(
|
||||
self, tr("co4e.ai_draft_hint_title"), tr("co4e.ai_draft_hint_label"))
|
||||
if not ok:
|
||||
return
|
||||
|
||||
@@ -79,7 +79,7 @@ class StepConfigPanel(_StepConfigActionsMixin, QScrollArea):
|
||||
# (skills/files/sub-agents). No tabs/accordion: every group's border
|
||||
# and heading are what separate it from its neighbours, and all three
|
||||
# are on screen (or one scroll away) at once.
|
||||
form, _basic_card = _add_section(outer, tr("co4e.tab_basic"))
|
||||
form, _basic_card = _add_section(outer, "co4e.tab_basic")
|
||||
|
||||
self.label_edit = QLineEdit()
|
||||
self.label_edit.textChanged.connect(self._on_edit)
|
||||
@@ -124,7 +124,7 @@ class StepConfigPanel(_StepConfigActionsMixin, QScrollArea):
|
||||
self.context_edit.textChanged.connect(self._on_edit)
|
||||
form.addRow(bind_text(QLabel(), "co4e.f_context"), self.context_edit)
|
||||
|
||||
form2, _model_card = _add_section(outer, tr("co4e.tab_model_perm"))
|
||||
form2, _model_card = _add_section(outer, "co4e.tab_model_perm")
|
||||
|
||||
model_row = QHBoxLayout()
|
||||
self.model_combo = QComboBox()
|
||||
@@ -163,7 +163,7 @@ class StepConfigPanel(_StepConfigActionsMixin, QScrollArea):
|
||||
vrow = QWidget(); vrow.setLayout(verify_row)
|
||||
form2.addRow("", vrow)
|
||||
|
||||
form3, _skills_card = _add_section(outer, tr("co4e.tab_skills_files"))
|
||||
form3, _skills_card = _add_section(outer, "co4e.tab_skills_files")
|
||||
|
||||
# Skills checklist (registry skills)
|
||||
self.skills_list = QListWidget()
|
||||
@@ -193,7 +193,7 @@ class StepConfigPanel(_StepConfigActionsMixin, QScrollArea):
|
||||
# Skills & Tệp, since it's really a distinct group, just one that
|
||||
# only applies to parallel-variant steps. load_step() hides the whole
|
||||
# card for a non-parallel step (see is_par below).
|
||||
form4, self._parallel_card = _add_section(outer, tr("co4e.f_subagents"))
|
||||
form4, self._parallel_card = _add_section(outer, "co4e.f_subagents")
|
||||
self.sub_list = QListWidget()
|
||||
self.sub_list.setMaximumHeight(90)
|
||||
self.sub_list.itemDoubleClicked.connect(self._edit_subagent) # re-pick agent
|
||||
|
||||
@@ -26,6 +26,7 @@ from __future__ import annotations
|
||||
from PySide6.QtCore import Qt, QEasingCurve, QPropertyAnimation, Signal
|
||||
from PySide6.QtWidgets import QFormLayout, QLabel, QVBoxLayout, QWidget
|
||||
|
||||
from ...i18n import bind_dynamic, tr
|
||||
from ...theme import current_palette
|
||||
|
||||
_SECTION_ANIM_MS = 180
|
||||
@@ -64,7 +65,7 @@ class _SectionHeader(QLabel):
|
||||
super().showEvent(event)
|
||||
|
||||
|
||||
def _add_section(outer: QVBoxLayout, title: str) -> tuple[QFormLayout, QWidget]:
|
||||
def _add_section(outer: QVBoxLayout, title_key: str) -> tuple[QFormLayout, QWidget]:
|
||||
"""One group of fields, collapsed to just its heading by default and
|
||||
independently expandable, so a long step config reads as a short list of
|
||||
group names until you open the one you need. Deliberately bare — no card
|
||||
@@ -74,7 +75,11 @@ def _add_section(outer: QVBoxLayout, title: str) -> tuple[QFormLayout, QWidget]:
|
||||
the group's rows to ``form``; ``card`` is the whole section (header +
|
||||
body) — hide it to remove the group entirely (e.g. for a section that
|
||||
only applies to some steps), rather than hiding individual rows inside
|
||||
an always-visible header."""
|
||||
an always-visible header.
|
||||
|
||||
Nhận KHOÁ dịch, không nhận chuỗi đã dịch: nhãn mục do hàm này tự dựng nên
|
||||
nơi gọi không giữ tham chiếu nào để áp lại: truyền ``tr(...)`` vào đây thì
|
||||
bốn tiêu đề đứng nguyên ở ngôn ngữ lúc dựng panel."""
|
||||
p = current_palette()
|
||||
card = QWidget()
|
||||
card_lay = QVBoxLayout(card)
|
||||
@@ -96,7 +101,6 @@ def _add_section(outer: QVBoxLayout, title: str) -> tuple[QFormLayout, QWidget]:
|
||||
# diacritics.
|
||||
header.ensurePolished()
|
||||
header.setFixedHeight(header.fontMetrics().height())
|
||||
header.setText(f"▶ {title}")
|
||||
card_lay.addWidget(header)
|
||||
|
||||
body = QWidget()
|
||||
@@ -112,6 +116,14 @@ def _add_section(outer: QVBoxLayout, title: str) -> tuple[QFormLayout, QWidget]:
|
||||
|
||||
is_open = False
|
||||
|
||||
def _sync_header() -> None:
|
||||
"""Nhãn mục: dấu gập/mở hiện tại + tiêu đề theo ngôn ngữ đang chọn."""
|
||||
header.setText(f"{'▼' if is_open else '▶'} {tr(title_key)}")
|
||||
|
||||
# Ràng buộc ĐỘNG chứ không bind cứng một chuỗi: nhãn này mang cả trạng thái
|
||||
# gập/mở, nên bind cứng sẽ trả nó về ▶ mỗi lần người dùng đổi ngôn ngữ.
|
||||
bind_dynamic(header, _sync_header)
|
||||
|
||||
def _on_finished() -> None:
|
||||
"""Hiệu ứng gập/mở chạy xong: bỏ trần chiều cao khi đang mở, để bước có nhiều
|
||||
trường không bị cắt cụt.
|
||||
@@ -130,7 +142,7 @@ def _add_section(outer: QVBoxLayout, title: str) -> tuple[QFormLayout, QWidget]:
|
||||
"""Lật trạng thái gập/mở của một mục và chạy hiệu ứng tương ứng."""
|
||||
nonlocal is_open
|
||||
is_open = not is_open
|
||||
header.setText(f"{'▼' if is_open else '▶'} {title}")
|
||||
_sync_header()
|
||||
anim.stop()
|
||||
if is_open:
|
||||
body.setVisible(True)
|
||||
|
||||
Reference in New Issue
Block a user