# i18n — luật chuỗi hiển thị Nguồn: docstring `i18n/__init__.py`. --- ## 1. Ba ngôn ngữ, mặc định tiếng Việt ```python LANGUAGES = {"en": "English", "ja": "日本語", "vi": "Tiếng Việt"} LANGUAGE_SHORT = {"en": "EN", "ja": "JP", "vi": "VN"} # switcher gọn ở top bar DEFAULT_LANGUAGE = "vi" ``` `tr(key, **kwargs)` trả chuỗi theo ngôn ngữ hiện tại, fallback lần lượt: **ngôn ngữ hiện tại → `en` → chính cái key**. Nghĩa là thiếu entry thì UI hiện ra `workspace.tab_folder` chứ không crash — nếu người dùng chụp màn hình có chuỗi dạng `a.b_c` thì đó chính là triệu chứng thiếu key. `.format(**kwargs)` được áp dụng khi có placeholder: `tr("composer.attachments", n=3)`. ## 2. Widget nào phải đăng ký callback | Loại widget | Cách xử lý | |---|---| | **Sống lâu** — chrome cửa sổ chính, tab, sidebar, composer | Đăng ký `on_language_changed(cb)`; `cb` áp lại `tr()` cho chính widget đó. Callback chạy **ngay một lần** và mỗi lần đổi ngôn ngữ | | **Tạm thời** — Settings, Skills, Flow, Permission dialog | Dựng lại từ đầu mỗi lần mở, nên chỉ cần gọi `tr()` lúc construct, **không** đăng ký | Quy ước đặt tên hàm callback trong repo: `_retranslate()` / `_apply_i18n()` — xem `ui/workspace_tab.py:484` trở đi làm mẫu chuẩn. **Bug điển hình:** "Đổi ngôn ngữ nhưng nhãn X không đổi" → widget sống lâu mà quên đăng ký, hoặc có đăng ký nhưng callback bỏ sót đúng nhãn đó. Không sửa bằng cách gọi `tr()` lại ở chỗ khác — sửa trong callback. ## 3. File từ điển `i18n/` chia theo màn hình, không phải một file khổng lồ: ```text i18n/login_dialog.py i18n/sidebar.py i18n/composer.py i18n/cowork_tab.py i18n/settings_dialog.py i18n/skills_dialog.py i18n/libreoffice_view.py i18n/agents_admin_tab.py i18n/monitoring_overview.py i18n/hint.py ``` Mỗi file export dict `key -> {"en":..., "ja":..., "vi":...}`, được `i18n/__init__.py` import và gộp lại. Thêm key mới: 1. Chọn đúng file theo màn hình (không nhét đại vào `login_dialog.py` chỉ vì nó lớn nhất). 2. Điền **đủ 3 ngôn ngữ**. Thiếu `ja` là lỗi hay gặp nhất và chỉ lộ ra khi khách Nhật dùng. 3. Đặt key theo `.` — `workspace.tab_folder`, `app.nav.recents`. ## 4. Rủi ro riêng của tiếng Nhật và tiếng Việt | Rủi ro | Triệu chứng | Cách xử lý | |---|---|---| | Tiếng Nhật ngắn hơn, tiếng Việt dài hơn tiếng Anh | Nút vừa với `EN`, tràn với `VI`; label bị `...` với `JA` | Không `setFixedWidth` theo chuỗi tiếng Anh. Dùng `sizeHint` + `minimumWidth`, hoặc cho phép wrap | | Dấu tiếng Việt bị cắt phần trên/dưới | `Ắ`, `ộ` mất dấu ở nhãn cao cố định | Không đặt `setFixedHeight` cho label theo pixel; để layout tự tính | | Font mặc định thiếu glyph Nhật | Ô vuông tofu `□□□` trên máy chưa cài font | Kiểm tra `_FONT` trong `theme/palettes.py`, khai báo fallback | | Sắp xếp / so sánh chuỗi | Danh sách project sắp sai với tên có dấu | Dùng `locale`-aware sort, không `sorted()` thô | | Chiều dài chuỗi tính bằng ký tự ≠ chiều rộng hiển thị | Elide sai với chữ Nhật | Đo bằng `QFontMetrics.horizontalAdvance`, không `len()` | ## 5. Checklist sửa bug i18n - [ ] Key mới có đủ `en` / `ja` / `vi`? - [ ] Đã thử đổi qua cả 3 ngôn ngữ **trong lúc app đang chạy** (không phải restart)? - [ ] Widget sống lâu đã đăng ký `on_language_changed`? - [ ] Không còn chuỗi hardcode nào trong bản vá? - [ ] Layout còn đúng với chuỗi dài nhất trong 3 ngôn ngữ? - [ ] Không dùng `len()` để đo bề rộng chữ?