feat: add Vietnamese localization to GUI

Introduces a two-language (EN/VI) i18n system with hot-swap support.
All ~160 user-facing strings are centralised in gui/locales.py; views
retranslate in-place on language switch without restarting the app.

- gui/locales.py: new file — _EN/_VI dicts, t() lookup, set_language(),
  get_language(); assert guard ensures EN/VI key parity
- gui/app.py: switch_language(), _apply_startup_language(),
  _save_language_pref(), _rebuild_nav_labels(); language stored in
  config.json under ui.language; pipeline step headers and run_tool
  status lines translated
- gui/views/settings.py: Language dropdown card (English / Tiếng Việt)
- gui/views/dashboard.py: all strings via t(); static header widgets
  stored and retranslated in refresh()
- gui/views/mods.py: all strings via t(); _STATUS dict built at call
  time so server status labels update on language switch
- gui/views/tools.py: all strings via _translatable registry; tab names
  and segmented-button values kept in English (CTkTabview constraint)
- gui/views/logs.py: title + Copy/Clear buttons stored, retranslated
- gui/wizard.py: all 3 pages fully translated
- docs/huong-dan-su-dung.md: full Vietnamese user guide
- CLAUDE.md: documents localization architecture and constraints
This commit is contained in:
Tran G. (Revernomad) Khoa
2026-04-08 16:58:41 +07:00
parent 4478ec3cab
commit 903cd366e2
10 changed files with 1226 additions and 230 deletions

View File

@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING
import customtkinter as ctk
from gui.locales import t
from gui.views.base import BaseView
if TYPE_CHECKING:
@@ -11,15 +12,15 @@ if TYPE_CHECKING:
class SettingsView(BaseView):
"""Appearance switcher, wizard re-opener, and current config display."""
"""Appearance switcher, language selector, wizard re-opener, and current config display."""
def build(self) -> None:
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=1)
ctk.CTkLabel(self, text="Settings",
font=ctk.CTkFont(size=22, weight="bold")).grid(
row=0, column=0, sticky="w", padx=24, pady=(20, 8))
self._title_lbl = ctk.CTkLabel(self, text=t("settings.title"),
font=ctk.CTkFont(size=22, weight="bold"))
self._title_lbl.grid(row=0, column=0, sticky="w", padx=24, pady=(20, 8))
self._scroll = ctk.CTkScrollableFrame(self, fg_color="transparent")
self._scroll.grid(row=1, column=0, sticky="nsew", padx=24, pady=(0, 12))
@@ -27,7 +28,8 @@ class SettingsView(BaseView):
self._build_cards()
def refresh(self) -> None:
# Config info may have changed (e.g. after wizard); rebuild cards.
# Config info and language may have changed; rebuild everything.
self._title_lbl.configure(text=t("settings.title"))
for w in self._scroll.winfo_children():
w.destroy()
self._build_cards()
@@ -36,22 +38,21 @@ class SettingsView(BaseView):
# ── Server & Paths ────────────────────────────────────────────────────
c1 = ctk.CTkFrame(self._scroll)
c1.pack(fill="x", pady=6)
ctk.CTkLabel(c1, text="Server & Path Configuration",
ctk.CTkLabel(c1, text=t("settings.server_card_title"),
font=ctk.CTkFont(size=14, weight="bold")).pack(
anchor="w", padx=16, pady=(14, 3))
ctk.CTkLabel(c1,
text="Re-run the setup wizard to change your server URL, "
"credentials, or Arma folder.",
text=t("settings.server_card_desc"),
text_color="gray", wraplength=600, justify="left").pack(
anchor="w", padx=16, pady=(0, 8))
ctk.CTkButton(c1, text="Open Setup Wizard", width=160,
ctk.CTkButton(c1, text=t("settings.wizard_btn"), width=160,
command=self.app.open_wizard).pack(
anchor="e", padx=16, pady=(0, 14))
# ── Appearance ────────────────────────────────────────────────────────
c2 = ctk.CTkFrame(self._scroll)
c2.pack(fill="x", pady=6)
ctk.CTkLabel(c2, text="Appearance",
ctk.CTkLabel(c2, text=t("settings.appearance_title"),
font=ctk.CTkFont(size=14, weight="bold")).pack(
anchor="w", padx=16, pady=(14, 3))
mode_var = ctk.StringVar(value=ctk.get_appearance_mode())
@@ -60,12 +61,28 @@ class SettingsView(BaseView):
command=ctk.set_appearance_mode,
width=140).pack(anchor="w", padx=16, pady=(0, 14))
# ── Language ──────────────────────────────────────────────────────────
c_lang = ctk.CTkFrame(self._scroll)
c_lang.pack(fill="x", pady=6)
ctk.CTkLabel(c_lang, text=t("settings.language_title"),
font=ctk.CTkFont(size=14, weight="bold")).pack(
anchor="w", padx=16, pady=(14, 3))
from gui.locales import get_language
current_display = "Tiếng Việt" if get_language() == "vi" else "English"
ctk.CTkOptionMenu(
c_lang,
values=["English", "Tiếng Việt"],
variable=ctk.StringVar(value=current_display),
command=lambda v: self.app.switch_language("vi" if v == "Tiếng Việt" else "en"),
width=160,
).pack(anchor="w", padx=16, pady=(0, 14))
# ── Current config info ───────────────────────────────────────────────
cfg = self.app.cfg
if cfg:
c3 = ctk.CTkFrame(self._scroll)
c3.pack(fill="x", pady=6)
ctk.CTkLabel(c3, text="Current Configuration",
ctk.CTkLabel(c3, text=t("settings.config_title"),
font=ctk.CTkFont(size=14, weight="bold")).pack(
anchor="w", padx=16, pady=(14, 3))
info = (