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:
@@ -11,6 +11,7 @@ import customtkinter as ctk
|
||||
from gui._constants import (
|
||||
COLOR_OK, COLOR_PENDING, COLOR_ERROR, COLOR_WARN,
|
||||
)
|
||||
from gui.locales import t
|
||||
from gui.views.base import BaseView
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -38,10 +39,12 @@ class DashboardView(BaseView):
|
||||
# ── Header ────────────────────────────────────────────────────────────
|
||||
hdr = ctk.CTkFrame(self, fg_color="transparent")
|
||||
hdr.grid(row=0, column=0, sticky="ew", padx=24, pady=(20, 10))
|
||||
ctk.CTkLabel(hdr, text="Dashboard",
|
||||
font=ctk.CTkFont(size=22, weight="bold")).pack(side="left")
|
||||
ctk.CTkButton(hdr, text="⟳ Refresh", width=100,
|
||||
command=self.refresh).pack(side="right")
|
||||
self._title_lbl = ctk.CTkLabel(hdr, text=t("dashboard.title"),
|
||||
font=ctk.CTkFont(size=22, weight="bold"))
|
||||
self._title_lbl.pack(side="left")
|
||||
self._refresh_btn = ctk.CTkButton(hdr, text=t("dashboard.refresh_btn"),
|
||||
width=100, command=self.refresh)
|
||||
self._refresh_btn.pack(side="right")
|
||||
|
||||
# ── Cards ─────────────────────────────────────────────────────────────
|
||||
cards = ctk.CTkFrame(self, fg_color="transparent")
|
||||
@@ -58,7 +61,7 @@ class DashboardView(BaseView):
|
||||
|
||||
self._run_btn = ctk.CTkButton(
|
||||
run_area,
|
||||
text="▶ Run Full Pipeline",
|
||||
text=t("dashboard.run_btn"),
|
||||
font=ctk.CTkFont(size=15, weight="bold"),
|
||||
height=46,
|
||||
command=self._on_run,
|
||||
@@ -72,11 +75,11 @@ class DashboardView(BaseView):
|
||||
pc = ctk.CTkFrame(parent)
|
||||
pc.grid(row=0, column=0, sticky="nsew", padx=(0, 8), pady=4)
|
||||
|
||||
ctk.CTkLabel(pc, text="Preset Files",
|
||||
ctk.CTkLabel(pc, text=t("dashboard.preset_card_title"),
|
||||
font=ctk.CTkFont(size=14, weight="bold")).pack(
|
||||
anchor="w", padx=14, pady=(14, 2))
|
||||
ctk.CTkLabel(pc,
|
||||
text="HTML exports from Arma 3 Launcher → Mods → Export to HTML",
|
||||
text=t("dashboard.preset_card_desc"),
|
||||
text_color="gray", font=ctk.CTkFont(size=11)).pack(
|
||||
anchor="w", padx=14)
|
||||
|
||||
@@ -89,16 +92,16 @@ class DashboardView(BaseView):
|
||||
self._sel_count_lbl = ctk.CTkLabel(sel_row, text="", text_color="gray",
|
||||
font=ctk.CTkFont(size=11))
|
||||
self._sel_count_lbl.pack(side="left")
|
||||
ctk.CTkButton(sel_row, text="None", width=54,
|
||||
ctk.CTkButton(sel_row, text=t("dashboard.btn_none"), width=54,
|
||||
fg_color="transparent", border_width=1,
|
||||
text_color=("gray10", "gray90"), font=ctk.CTkFont(size=11),
|
||||
command=self._select_none).pack(side="right")
|
||||
ctk.CTkButton(sel_row, text="All", width=54,
|
||||
ctk.CTkButton(sel_row, text=t("dashboard.btn_all"), width=54,
|
||||
fg_color="transparent", border_width=1,
|
||||
text_color=("gray10", "gray90"), font=ctk.CTkFont(size=11),
|
||||
command=self._select_all).pack(side="right", padx=(0, 6))
|
||||
|
||||
ctk.CTkButton(pc, text="+ Add Preset Files",
|
||||
ctk.CTkButton(pc, text=t("dashboard.btn_add"),
|
||||
fg_color="transparent", border_width=1,
|
||||
text_color=("gray10", "gray90"),
|
||||
command=self._add_presets).pack(pady=(0, 14))
|
||||
@@ -109,16 +112,17 @@ class DashboardView(BaseView):
|
||||
pipe = ctk.CTkFrame(parent)
|
||||
pipe.grid(row=0, column=1, sticky="nsew", padx=(8, 0), pady=4)
|
||||
|
||||
ctk.CTkLabel(pipe, text="Pipeline Status",
|
||||
ctk.CTkLabel(pipe, text=t("dashboard.pipeline_title"),
|
||||
font=ctk.CTkFont(size=14, weight="bold")).pack(
|
||||
anchor="w", padx=14, pady=(14, 8))
|
||||
|
||||
self._step_icons: dict[str, ctk.CTkLabel] = {}
|
||||
for key, label in [
|
||||
("parse", "Parse presets"),
|
||||
("compare", "Compare presets"),
|
||||
("download", "Download mods"),
|
||||
("link", "Link to Arma"),
|
||||
self._step_labels: dict[str, ctk.CTkLabel] = {}
|
||||
for key, lbl_key in [
|
||||
("parse", "dashboard.step_parse"),
|
||||
("compare", "dashboard.step_compare"),
|
||||
("download", "dashboard.step_download"),
|
||||
("link", "dashboard.step_link"),
|
||||
]:
|
||||
row = ctk.CTkFrame(pipe, fg_color="transparent")
|
||||
row.pack(fill="x", padx=14, pady=3)
|
||||
@@ -126,8 +130,10 @@ class DashboardView(BaseView):
|
||||
text_color=COLOR_PENDING,
|
||||
font=ctk.CTkFont(size=15))
|
||||
icon.pack(side="left")
|
||||
ctk.CTkLabel(row, text=label, anchor="w").pack(side="left", padx=6)
|
||||
self._step_icons[key] = icon
|
||||
lbl = ctk.CTkLabel(row, text=t(lbl_key), anchor="w")
|
||||
lbl.pack(side="left", padx=6)
|
||||
self._step_icons[key] = icon
|
||||
self._step_labels[key] = lbl
|
||||
|
||||
self._stats_lbl = ctk.CTkLabel(pipe, text="", text_color="gray",
|
||||
font=ctk.CTkFont(size=11),
|
||||
@@ -137,6 +143,22 @@ class DashboardView(BaseView):
|
||||
# ── refresh ───────────────────────────────────────────────────────────────
|
||||
|
||||
def refresh(self) -> None:
|
||||
# Retranslate static widgets that were built once
|
||||
self._title_lbl.configure(text=t("dashboard.title"))
|
||||
self._refresh_btn.configure(text=t("dashboard.refresh_btn"))
|
||||
# Only update run_btn text when not currently running
|
||||
if self._run_btn.cget("state") != "disabled":
|
||||
self._run_btn.configure(text=t("dashboard.run_btn"))
|
||||
# Retranslate step labels
|
||||
for key, lbl_key in [
|
||||
("parse", "dashboard.step_parse"),
|
||||
("compare", "dashboard.step_compare"),
|
||||
("download", "dashboard.step_download"),
|
||||
("link", "dashboard.step_link"),
|
||||
]:
|
||||
if key in self._step_labels:
|
||||
self._step_labels[key].configure(text=t(lbl_key))
|
||||
|
||||
self._rebuild_preset_list()
|
||||
self._update_pipeline_status()
|
||||
|
||||
@@ -148,21 +170,21 @@ class DashboardView(BaseView):
|
||||
cfg = self.app.cfg
|
||||
if not cfg:
|
||||
ctk.CTkLabel(self._preset_scroll,
|
||||
text="No config found. Complete Setup first.",
|
||||
text=t("dashboard.no_config"),
|
||||
text_color=COLOR_WARN).pack(anchor="w")
|
||||
return
|
||||
|
||||
html_dir = cfg.modlist_html
|
||||
if not html_dir.is_dir():
|
||||
ctk.CTkLabel(self._preset_scroll,
|
||||
text=f"Folder missing:\n{html_dir}",
|
||||
text=t("dashboard.folder_missing", path=html_dir),
|
||||
text_color=COLOR_WARN, justify="left").pack(padx=4, pady=8)
|
||||
return
|
||||
|
||||
files = sorted(html_dir.glob("*.html"))
|
||||
if not files:
|
||||
ctk.CTkLabel(self._preset_scroll,
|
||||
text="No preset files yet.\nUse the button below to add them.",
|
||||
text=t("dashboard.no_presets"),
|
||||
text_color="gray", justify="left").pack(padx=4, pady=8)
|
||||
return
|
||||
|
||||
@@ -207,9 +229,9 @@ class DashboardView(BaseView):
|
||||
if cfg.missing_report.exists():
|
||||
rep = json.loads(cfg.missing_report.read_text(encoding="utf-8"))
|
||||
missing = rep.get("missing", 0)
|
||||
stat = f"{total} mods · {shared} shared"
|
||||
stat = t("dashboard.stats", total=total, shared=shared)
|
||||
if missing:
|
||||
stat += f"\n{missing} missing from server"
|
||||
stat += t("dashboard.stats_missing", missing=missing)
|
||||
self._stats_lbl.configure(text=stat)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -236,7 +258,9 @@ class DashboardView(BaseView):
|
||||
COLOR_WARN if n_sel == 1 else
|
||||
COLOR_ERROR)
|
||||
self._sel_count_lbl.configure(
|
||||
text=f"{n_sel} of {n_total} selected", text_color=color)
|
||||
text=t("dashboard.sel_count", n_sel=n_sel, n_total=n_total),
|
||||
text_color=color,
|
||||
)
|
||||
|
||||
def _select_all(self) -> None:
|
||||
for var in self._preset_checks.values():
|
||||
@@ -253,11 +277,11 @@ class DashboardView(BaseView):
|
||||
def _add_presets(self) -> None:
|
||||
cfg = self.app.cfg
|
||||
if not cfg:
|
||||
messagebox.showwarning("Setup required",
|
||||
"Please complete Setup before adding presets.")
|
||||
messagebox.showwarning(t("dashboard.dlg_setup_title"),
|
||||
t("dashboard.dlg_setup_body"))
|
||||
return
|
||||
files = filedialog.askopenfilenames(
|
||||
title="Select Arma 3 Launcher preset files",
|
||||
title=t("dashboard.file_dialog_title"),
|
||||
filetypes=[("HTML Preset", "*.html"), ("All files", "*.*")],
|
||||
)
|
||||
if not files:
|
||||
@@ -280,10 +304,10 @@ class DashboardView(BaseView):
|
||||
def set_pipeline_ui(self, running: bool) -> None:
|
||||
"""Called by the app to reflect pipeline start/end in the UI."""
|
||||
if running:
|
||||
self._run_btn.configure(state="disabled", text="Running…")
|
||||
self._run_btn.configure(state="disabled", text=t("dashboard.running"))
|
||||
self._prog.pack(fill="x", pady=(6, 0))
|
||||
self._prog.start()
|
||||
else:
|
||||
self._run_btn.configure(state="normal", text="▶ Run Full Pipeline")
|
||||
self._run_btn.configure(state="normal", text=t("dashboard.run_btn"))
|
||||
self._prog.stop()
|
||||
self._prog.pack_forget()
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING
|
||||
import customtkinter as ctk
|
||||
|
||||
from gui._constants import COLOR_ERROR
|
||||
from gui.locales import t
|
||||
from gui.views.base import BaseView
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -27,16 +28,19 @@ class LogsView(BaseView):
|
||||
# ── Header ────────────────────────────────────────────────────────────
|
||||
hdr = ctk.CTkFrame(self, fg_color="transparent")
|
||||
hdr.grid(row=0, column=0, sticky="ew", padx=24, pady=(20, 8))
|
||||
ctk.CTkLabel(hdr, text="Logs",
|
||||
font=ctk.CTkFont(size=22, weight="bold")).pack(side="left")
|
||||
self._title_lbl = ctk.CTkLabel(hdr, text=t("logs.title"),
|
||||
font=ctk.CTkFont(size=22, weight="bold"))
|
||||
self._title_lbl.pack(side="left")
|
||||
|
||||
btn_row = ctk.CTkFrame(hdr, fg_color="transparent")
|
||||
btn_row.pack(side="right")
|
||||
ctk.CTkButton(btn_row, text="Copy", width=72,
|
||||
command=self._copy).pack(side="left", padx=4)
|
||||
ctk.CTkButton(btn_row, text="Clear", width=72,
|
||||
fg_color=COLOR_ERROR, hover_color="#c62828",
|
||||
command=self._clear).pack(side="left")
|
||||
self._copy_btn = ctk.CTkButton(btn_row, text=t("logs.copy_btn"), width=72,
|
||||
command=self._copy)
|
||||
self._copy_btn.pack(side="left", padx=4)
|
||||
self._clear_btn = ctk.CTkButton(btn_row, text=t("logs.clear_btn"), width=72,
|
||||
fg_color=COLOR_ERROR, hover_color="#c62828",
|
||||
command=self._clear)
|
||||
self._clear_btn.pack(side="left")
|
||||
|
||||
# ── Log textbox (persistent) ──────────────────────────────────────────
|
||||
self._log_box = ctk.CTkTextbox(
|
||||
@@ -44,6 +48,12 @@ class LogsView(BaseView):
|
||||
font=ctk.CTkFont(family="Consolas", size=12))
|
||||
self._log_box.grid(row=1, column=0, sticky="nsew", padx=24, pady=(0, 12))
|
||||
|
||||
def refresh(self) -> None:
|
||||
# Retranslate header widgets (log content intentionally preserved)
|
||||
self._title_lbl.configure(text=t("logs.title"))
|
||||
self._copy_btn.configure(text=t("logs.copy_btn"))
|
||||
self._clear_btn.configure(text=t("logs.clear_btn"))
|
||||
|
||||
def append(self, text: str) -> None:
|
||||
"""Thread-safe-ish: called from the app's after() poll loop (main thread)."""
|
||||
try:
|
||||
|
||||
@@ -9,6 +9,7 @@ import customtkinter as ctk
|
||||
|
||||
from arma_modlist_tools.fetcher import _normalize_name
|
||||
from gui._constants import COLOR_OK, COLOR_ERROR, COLOR_WARN, COLOR_RUNNING
|
||||
from gui.locales import t
|
||||
from gui.views.base import BaseView
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -61,18 +62,19 @@ class ModsView(BaseView):
|
||||
hdr.grid(row=0, column=0, sticky="ew", padx=24, pady=(20, 6))
|
||||
hdr.columnconfigure(1, weight=1)
|
||||
|
||||
ctk.CTkLabel(hdr, text="Mods",
|
||||
font=ctk.CTkFont(size=22, weight="bold")).grid(
|
||||
row=0, column=0, sticky="w")
|
||||
self._title_lbl = ctk.CTkLabel(hdr, text=t("mods.title"),
|
||||
font=ctk.CTkFont(size=22, weight="bold"))
|
||||
self._title_lbl.grid(row=0, column=0, sticky="w")
|
||||
|
||||
btn_frame = ctk.CTkFrame(hdr, fg_color="transparent")
|
||||
btn_frame.grid(row=0, column=2, sticky="e")
|
||||
|
||||
ctk.CTkButton(btn_frame, text="⟳ Refresh", width=100,
|
||||
command=self.refresh).pack(side="left", padx=(0, 6))
|
||||
self._refresh_btn = ctk.CTkButton(btn_frame, text=t("mods.refresh_btn"),
|
||||
width=100, command=self.refresh)
|
||||
self._refresh_btn.pack(side="left", padx=(0, 6))
|
||||
|
||||
self._check_btn = ctk.CTkButton(
|
||||
btn_frame, text="☁ Check Updates", width=130,
|
||||
btn_frame, text=t("mods.check_btn"), width=140,
|
||||
command=self._check_updates,
|
||||
)
|
||||
self._check_btn.pack(side="left")
|
||||
@@ -80,9 +82,10 @@ class ModsView(BaseView):
|
||||
# ── Search ────────────────────────────────────────────────────────────
|
||||
bar = ctk.CTkFrame(self, fg_color="transparent")
|
||||
bar.grid(row=1, column=0, sticky="ew", padx=24, pady=(0, 8))
|
||||
ctk.CTkLabel(bar, text="Search:").pack(side="left", padx=(0, 6))
|
||||
self._search_lbl = ctk.CTkLabel(bar, text=t("mods.search_label"))
|
||||
self._search_lbl.pack(side="left", padx=(0, 6))
|
||||
ctk.CTkEntry(bar, textvariable=self._search_var,
|
||||
placeholder_text="Filter mods in active tab…",
|
||||
placeholder_text=t("mods.search_placeholder"),
|
||||
width=220).pack(side="left")
|
||||
self._search_var.trace_add("write", lambda *_: self._apply_search())
|
||||
|
||||
@@ -99,6 +102,13 @@ class ModsView(BaseView):
|
||||
# =========================================================================
|
||||
|
||||
def refresh(self) -> None:
|
||||
# Retranslate static header widgets
|
||||
self._title_lbl.configure(text=t("mods.title"))
|
||||
self._refresh_btn.configure(text=t("mods.refresh_btn"))
|
||||
if not self._checking:
|
||||
self._check_btn.configure(text=t("mods.check_btn"))
|
||||
self._search_lbl.configure(text=t("mods.search_label"))
|
||||
|
||||
self._mod_rows.clear()
|
||||
|
||||
# Destroy previous tab_view / message
|
||||
@@ -111,19 +121,16 @@ class ModsView(BaseView):
|
||||
|
||||
cfg = self.app.cfg
|
||||
if not cfg:
|
||||
self._show_msg("No config found. Complete Setup first.")
|
||||
self._show_msg(t("mods.no_config"))
|
||||
return
|
||||
if not cfg.comparison.exists():
|
||||
self._show_msg(
|
||||
"No mod data yet.\n"
|
||||
"Go to Dashboard, select your presets, then click Run Full Pipeline."
|
||||
)
|
||||
self._show_msg(t("mods.no_data"))
|
||||
return
|
||||
|
||||
try:
|
||||
comp = json.loads(cfg.comparison.read_text(encoding="utf-8"))
|
||||
except Exception as e:
|
||||
self._show_msg(f"Error reading comparison.json: {e}", error=True)
|
||||
self._show_msg(t("mods.read_error", e=e), error=True)
|
||||
return
|
||||
|
||||
# Build ordered group list: shared first, then unique groups
|
||||
@@ -164,14 +171,14 @@ class ModsView(BaseView):
|
||||
fg_color=("gray82", "gray22"), corner_radius=6)
|
||||
col_hdr.grid(row=0, column=0, sticky="ew", padx=4, pady=(6, 2))
|
||||
col_hdr.columnconfigure(0, weight=1)
|
||||
for col, (w, lbl) in enumerate([
|
||||
(0, "Mod Name"),
|
||||
(80, "Downloaded"),
|
||||
(80, "Linked"),
|
||||
(160, "Server Status"),
|
||||
for col, (w, lbl_key) in enumerate([
|
||||
(0, "mods.col_name"),
|
||||
(80, "mods.col_downloaded"),
|
||||
(80, "mods.col_linked"),
|
||||
(160, "mods.col_server"),
|
||||
(80, ""),
|
||||
]):
|
||||
ctk.CTkLabel(col_hdr, text=lbl,
|
||||
ctk.CTkLabel(col_hdr, text=t(lbl_key) if lbl_key else "",
|
||||
font=ctk.CTkFont(weight="bold"),
|
||||
anchor="w", width=w or 1).grid(
|
||||
row=0, column=col,
|
||||
@@ -247,7 +254,7 @@ class ModsView(BaseView):
|
||||
# Update button (hidden until stale detected)
|
||||
folder_name = folder_path.name if folder_path else None
|
||||
update_btn = ctk.CTkButton(
|
||||
row, text="Update", width=70,
|
||||
row, text=t("mods.update_btn"), width=70,
|
||||
command=(lambda g=group, fn=folder_name:
|
||||
self._update_mod(g, fn)) if folder_name else None,
|
||||
state="normal" if folder_name else "disabled",
|
||||
@@ -282,13 +289,13 @@ class ModsView(BaseView):
|
||||
return
|
||||
|
||||
self._checking = True
|
||||
self._check_btn.configure(text="Checking…", state="disabled")
|
||||
self._check_btn.configure(text=t("mods.check_btn_checking"), state="disabled")
|
||||
|
||||
# Reset downloaded rows to "Checking…"
|
||||
for row in self._mod_rows.values():
|
||||
if row["folder_path"]:
|
||||
row["status_label"].configure(
|
||||
text="Checking…", text_color=COLOR_RUNNING)
|
||||
text=t("mods.status_checking"), text_color=COLOR_RUNNING)
|
||||
else:
|
||||
row["status_label"].configure(text="—", text_color="gray")
|
||||
|
||||
@@ -325,27 +332,29 @@ class ModsView(BaseView):
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def _apply_check_results(self, results: dict[str, tuple[str, int]]) -> None:
|
||||
# Build status map from current translations
|
||||
_STATUS: dict[str, tuple[str, str]] = {
|
||||
"ok": ("✓ Up to date", COLOR_OK),
|
||||
"stale": ("⚠ {n} outdated", COLOR_WARN),
|
||||
"not_downloaded": ("—", "gray"),
|
||||
"not_on_server": ("Not on server", "gray"),
|
||||
"error": ("✗ Error", COLOR_ERROR),
|
||||
"ok": (t("mods.status_ok"), COLOR_OK),
|
||||
"stale": (t("mods.status_stale"), COLOR_WARN),
|
||||
"not_downloaded": (t("mods.status_not_downloaded"), "gray"),
|
||||
"not_on_server": (t("mods.status_not_on_server"), "gray"),
|
||||
"error": (t("mods.status_error"), COLOR_ERROR),
|
||||
}
|
||||
for key, (status, n) in results.items():
|
||||
row = self._mod_rows.get(key)
|
||||
if not row:
|
||||
continue
|
||||
tmpl, color = _STATUS.get(status, ("—", "gray"))
|
||||
row["status_label"].configure(
|
||||
text=tmpl.replace("{n}", str(n)), text_color=color)
|
||||
# For "stale", the template contains {n} placeholder
|
||||
text = tmpl.format_map({"n": n}) if "{n}" in tmpl else tmpl
|
||||
row["status_label"].configure(text=text, text_color=color)
|
||||
if status == "stale" and row["folder_path"]:
|
||||
row["update_btn"].grid()
|
||||
else:
|
||||
row["update_btn"].grid_remove()
|
||||
|
||||
self._checking = False
|
||||
self._check_btn.configure(text="☁ Check Updates", state="normal")
|
||||
self._check_btn.configure(text=t("mods.check_btn"), state="normal")
|
||||
|
||||
def _update_mod(self, group: str, folder_name: str) -> None:
|
||||
self.app.navigate_to("Logs")
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
import customtkinter as ctk
|
||||
|
||||
from gui._constants import COLOR_WARN, PROJECT_ROOT
|
||||
from gui.locales import t
|
||||
from gui.views.base import BaseView
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -22,9 +23,9 @@ class ToolsView(BaseView):
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
self.grid_rowconfigure(1, weight=1)
|
||||
|
||||
ctk.CTkLabel(self, text="Tools",
|
||||
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("tools.title"),
|
||||
font=ctk.CTkFont(size=22, weight="bold"))
|
||||
self._title_lbl.grid(row=0, column=0, sticky="w", padx=24, pady=(20, 8))
|
||||
|
||||
self._tab_view = ctk.CTkTabview(self)
|
||||
self._tab_view.grid(row=1, column=0, sticky="nsew", padx=16, pady=(0, 12))
|
||||
@@ -32,6 +33,9 @@ class ToolsView(BaseView):
|
||||
# Per-tab group menu references so refresh() can repopulate them all
|
||||
self._group_menus: list[tuple[ctk.CTkOptionMenu, ctk.StringVar]] = []
|
||||
|
||||
# Tab-internal translatable labels (for hot-swap on refresh)
|
||||
self._translatable: list[tuple[ctk.CTkLabel | ctk.CTkButton | ctk.CTkCheckBox, str]] = []
|
||||
|
||||
self._build_check_names_tab()
|
||||
self._build_update_mods_tab()
|
||||
self._build_link_mods_tab()
|
||||
@@ -43,23 +47,32 @@ class ToolsView(BaseView):
|
||||
# =========================================================================
|
||||
|
||||
def refresh(self) -> None:
|
||||
self._title_lbl.configure(text=t("tools.title"))
|
||||
|
||||
# Retranslate registered widgets
|
||||
for widget, key in self._translatable:
|
||||
widget.configure(text=t(key))
|
||||
|
||||
# Refresh link-mods button label (depends on current command selection)
|
||||
self._lm_on_change()
|
||||
|
||||
groups = self._get_groups()
|
||||
all_groups = ["All groups"] + groups
|
||||
all_groups = [t("tools.all_groups")] + groups
|
||||
|
||||
# Repopulate generic group menus
|
||||
for menu, var in self._group_menus:
|
||||
prev = var.get()
|
||||
menu.configure(values=all_groups)
|
||||
if prev not in all_groups:
|
||||
var.set("All groups")
|
||||
# Keep selection if still valid, else reset to "All groups"
|
||||
if prev not in all_groups and prev not in groups:
|
||||
var.set(t("tools.all_groups"))
|
||||
|
||||
# Link Mods group menu (no "All groups")
|
||||
lm_prev = self._lm_group_var.get()
|
||||
lm_vals = groups if groups else ["(no groups found)"]
|
||||
lm_vals = groups if groups else [t("tools.no_groups")]
|
||||
self._lm_group_menu.configure(values=lm_vals)
|
||||
if lm_prev not in lm_vals:
|
||||
self._lm_group_var.set(lm_vals[0])
|
||||
self._lm_on_change() # re-evaluate button state
|
||||
|
||||
# Info labels
|
||||
self._update_sm_label()
|
||||
@@ -70,47 +83,49 @@ class ToolsView(BaseView):
|
||||
# =========================================================================
|
||||
|
||||
def _build_check_names_tab(self) -> None:
|
||||
# Tab name kept in English — used as CTkTabview lookup key
|
||||
self._tab_view.add("Check Names")
|
||||
tab = self._tab_view.tab("Check Names")
|
||||
tab.grid_columnconfigure(0, weight=1)
|
||||
|
||||
_desc(tab, row=0,
|
||||
text="Scan mod folders and compare against the server. "
|
||||
"Reports naming mismatches (MISMATCH), unrecognised folders "
|
||||
"(NOT_ON_SERVER), and wrong Steam IDs in meta.cpp (ID_COLLISION).")
|
||||
desc_lbl = _desc(tab, row=0, text=t("tools.cn_desc"))
|
||||
self._translatable.append((desc_lbl, "tools.cn_desc"))
|
||||
|
||||
# Group
|
||||
gf = _row(tab, row=1, label="Group:")
|
||||
self._cn_group_var = ctk.StringVar(value="All groups")
|
||||
gf, group_lbl = _row(tab, row=1, label=t("tools.label_group"))
|
||||
self._translatable.append((group_lbl, "tools.label_group"))
|
||||
self._cn_group_var = ctk.StringVar(value=t("tools.all_groups"))
|
||||
menu = ctk.CTkOptionMenu(gf, variable=self._cn_group_var,
|
||||
values=["All groups"], width=200)
|
||||
values=[t("tools.all_groups")], width=200)
|
||||
menu.pack(side="left")
|
||||
self._group_menus.append((menu, self._cn_group_var))
|
||||
|
||||
# Checkboxes
|
||||
cf = _row(tab, row=2, label="Options:")
|
||||
cf, opts_lbl = _row(tab, row=2, label=t("tools.label_options"))
|
||||
self._translatable.append((opts_lbl, "tools.label_options"))
|
||||
self._cn_fix_var = ctk.BooleanVar(value=False)
|
||||
self._cn_fix_ids_var = ctk.BooleanVar(value=False)
|
||||
ctk.CTkCheckBox(cf, text="Auto-fix folder name mismatches (--fix)",
|
||||
variable=self._cn_fix_var,
|
||||
command=self._cn_on_toggle).pack(side="left", padx=(0, 16))
|
||||
ctk.CTkCheckBox(cf, text="Auto-fix wrong Steam IDs in meta.cpp (--fix-ids)",
|
||||
variable=self._cn_fix_ids_var,
|
||||
command=self._cn_on_toggle).pack(side="left")
|
||||
cn_fix_chk = ctk.CTkCheckBox(cf, text=t("tools.cn_fix_chk"),
|
||||
variable=self._cn_fix_var,
|
||||
command=self._cn_on_toggle)
|
||||
cn_fix_chk.pack(side="left", padx=(0, 16))
|
||||
self._translatable.append((cn_fix_chk, "tools.cn_fix_chk"))
|
||||
cn_ids_chk = ctk.CTkCheckBox(cf, text=t("tools.cn_fix_ids_chk"),
|
||||
variable=self._cn_fix_ids_var,
|
||||
command=self._cn_on_toggle)
|
||||
cn_ids_chk.pack(side="left")
|
||||
self._translatable.append((cn_ids_chk, "tools.cn_fix_ids_chk"))
|
||||
|
||||
# Warning (hidden until checkbox ticked)
|
||||
self._cn_warn = ctk.CTkLabel(
|
||||
tab,
|
||||
text="⚠ --fix renames folders and updates junctions. "
|
||||
"--fix-ids rewrites meta.cpp files.",
|
||||
text_color=_WARN_COLOR, anchor="w",
|
||||
)
|
||||
# not gridded yet — shown on demand
|
||||
self._cn_warn = ctk.CTkLabel(tab, text=t("tools.cn_warn"),
|
||||
text_color=_WARN_COLOR, anchor="w")
|
||||
self._translatable.append((self._cn_warn, "tools.cn_warn"))
|
||||
|
||||
# Run button
|
||||
ctk.CTkButton(tab, text="Run Check Names", width=180,
|
||||
command=self._cn_run).grid(
|
||||
row=10, column=0, padx=24, pady=(16, 24), sticky="e")
|
||||
cn_btn = ctk.CTkButton(tab, text=t("tools.cn_btn"), width=180,
|
||||
command=self._cn_run)
|
||||
cn_btn.grid(row=10, column=0, padx=24, pady=(16, 24), sticky="e")
|
||||
self._translatable.append((cn_btn, "tools.cn_btn"))
|
||||
|
||||
def _cn_on_toggle(self) -> None:
|
||||
if self._cn_fix_var.get() or self._cn_fix_ids_var.get():
|
||||
@@ -121,7 +136,7 @@ class ToolsView(BaseView):
|
||||
def _cn_run(self) -> None:
|
||||
args = ["check_names.py"]
|
||||
g = self._cn_group_var.get()
|
||||
if g != "All groups":
|
||||
if g != t("tools.all_groups"):
|
||||
args += ["--group", g]
|
||||
if self._cn_fix_var.get():
|
||||
args.append("--fix")
|
||||
@@ -136,54 +151,56 @@ class ToolsView(BaseView):
|
||||
tab = self._tab_view.tab("Update Mods")
|
||||
tab.grid_columnconfigure(0, weight=1)
|
||||
|
||||
_desc(tab, row=0,
|
||||
text="Re-download mod files whose size on the server differs from "
|
||||
"your local copy. Use --force to re-download everything "
|
||||
"regardless of size.")
|
||||
desc_lbl = _desc(tab, row=0, text=t("tools.um_desc"))
|
||||
self._translatable.append((desc_lbl, "tools.um_desc"))
|
||||
|
||||
# Group
|
||||
gf = _row(tab, row=1, label="Group:")
|
||||
self._um_group_var = ctk.StringVar(value="All groups")
|
||||
gf, group_lbl = _row(tab, row=1, label=t("tools.label_group"))
|
||||
self._translatable.append((group_lbl, "tools.label_group"))
|
||||
self._um_group_var = ctk.StringVar(value=t("tools.all_groups"))
|
||||
um_menu = ctk.CTkOptionMenu(
|
||||
gf, variable=self._um_group_var, values=["All groups"], width=200,
|
||||
gf, variable=self._um_group_var, values=[t("tools.all_groups")], width=200,
|
||||
command=self._um_on_group_change,
|
||||
)
|
||||
um_menu.pack(side="left")
|
||||
self._group_menus.append((um_menu, self._um_group_var))
|
||||
|
||||
# Mod name (enabled only when a specific group is selected)
|
||||
mf = _row(tab, row=2, label="Mod folder:")
|
||||
mf, mod_lbl = _row(tab, row=2, label=t("tools.um_mod_label"))
|
||||
self._translatable.append((mod_lbl, "tools.um_mod_label"))
|
||||
self._um_mod_entry = ctk.CTkEntry(
|
||||
mf, placeholder_text="Optional — e.g. @ace", width=220,
|
||||
mf, placeholder_text=t("tools.um_mod_placeholder"), width=220,
|
||||
state="disabled",
|
||||
)
|
||||
self._um_mod_entry.pack(side="left")
|
||||
ctk.CTkLabel(mf, text="(only when a specific group is selected)",
|
||||
text_color="gray").pack(side="left", padx=8)
|
||||
um_hint = ctk.CTkLabel(mf, text=t("tools.um_mod_hint"), text_color="gray")
|
||||
um_hint.pack(side="left", padx=8)
|
||||
self._translatable.append((um_hint, "tools.um_mod_hint"))
|
||||
|
||||
# Force checkbox
|
||||
ff = _row(tab, row=3, label="Options:")
|
||||
ff, opts_lbl = _row(tab, row=3, label=t("tools.label_options"))
|
||||
self._translatable.append((opts_lbl, "tools.label_options"))
|
||||
self._um_force_var = ctk.BooleanVar(value=False)
|
||||
ctk.CTkCheckBox(
|
||||
ff, text="Force re-download all files (--force)",
|
||||
um_force_chk = ctk.CTkCheckBox(
|
||||
ff, text=t("tools.um_force_chk"),
|
||||
variable=self._um_force_var,
|
||||
command=self._um_on_toggle,
|
||||
).pack(side="left")
|
||||
)
|
||||
um_force_chk.pack(side="left")
|
||||
self._translatable.append((um_force_chk, "tools.um_force_chk"))
|
||||
|
||||
# Warning
|
||||
self._um_warn = ctk.CTkLabel(
|
||||
tab,
|
||||
text="⚠ --force re-downloads every file regardless of size. "
|
||||
"This may transfer a large amount of data.",
|
||||
text_color=_WARN_COLOR, anchor="w",
|
||||
)
|
||||
self._um_warn = ctk.CTkLabel(tab, text=t("tools.um_warn"),
|
||||
text_color=_WARN_COLOR, anchor="w")
|
||||
self._translatable.append((self._um_warn, "tools.um_warn"))
|
||||
|
||||
ctk.CTkButton(tab, text="Run Update", width=180,
|
||||
command=self._um_run).grid(
|
||||
row=10, column=0, padx=24, pady=(16, 24), sticky="e")
|
||||
um_btn = ctk.CTkButton(tab, text=t("tools.um_btn"), width=180,
|
||||
command=self._um_run)
|
||||
um_btn.grid(row=10, column=0, padx=24, pady=(16, 24), sticky="e")
|
||||
self._translatable.append((um_btn, "tools.um_btn"))
|
||||
|
||||
def _um_on_group_change(self, _: str) -> None:
|
||||
is_specific = self._um_group_var.get() != "All groups"
|
||||
is_specific = self._um_group_var.get() != t("tools.all_groups")
|
||||
self._um_mod_entry.configure(state="normal" if is_specific else "disabled")
|
||||
if not is_specific:
|
||||
self._um_mod_entry.delete(0, "end")
|
||||
@@ -197,7 +214,7 @@ class ToolsView(BaseView):
|
||||
def _um_run(self) -> None:
|
||||
args = ["update_mods.py"]
|
||||
g = self._um_group_var.get()
|
||||
if g != "All groups":
|
||||
if g != t("tools.all_groups"):
|
||||
args += ["--group", g]
|
||||
mod = self._um_mod_entry.get().strip()
|
||||
if mod:
|
||||
@@ -213,15 +230,12 @@ class ToolsView(BaseView):
|
||||
tab = self._tab_view.tab("Link Mods")
|
||||
tab.grid_columnconfigure(0, weight=1)
|
||||
|
||||
_desc(tab, row=0,
|
||||
text="Manage junction/symlink links between your downloads folder "
|
||||
"and the Arma 3 directory.\n"
|
||||
"Status — show what's linked. "
|
||||
"Link — create missing junctions. "
|
||||
"Unlink — remove junctions (mod files are NOT deleted).")
|
||||
desc_lbl = _desc(tab, row=0, text=t("tools.lm_desc"))
|
||||
self._translatable.append((desc_lbl, "tools.lm_desc"))
|
||||
|
||||
# Command selector
|
||||
cf = _row(tab, row=1, label="Command:")
|
||||
# Command selector — values kept in English (drive internal logic)
|
||||
cf, cmd_lbl = _row(tab, row=1, label=t("tools.label_command"))
|
||||
self._translatable.append((cmd_lbl, "tools.label_command"))
|
||||
self._lm_cmd_var = ctk.StringVar(value="Status")
|
||||
ctk.CTkSegmentedButton(
|
||||
cf,
|
||||
@@ -231,33 +245,36 @@ class ToolsView(BaseView):
|
||||
).pack(side="left")
|
||||
|
||||
# Group (required — no "All groups")
|
||||
gf = _row(tab, row=2, label="Group:")
|
||||
gf, group_lbl = _row(tab, row=2, label=t("tools.label_group"))
|
||||
self._translatable.append((group_lbl, "tools.label_group"))
|
||||
self._lm_group_var = ctk.StringVar(value="")
|
||||
self._lm_group_menu = ctk.CTkOptionMenu(
|
||||
gf, variable=self._lm_group_var,
|
||||
values=["(no groups found)"], width=200,
|
||||
values=[t("tools.no_groups")], width=200,
|
||||
command=lambda _: self._lm_on_change(),
|
||||
)
|
||||
self._lm_group_menu.pack(side="left")
|
||||
|
||||
# Warning (shown for Unlink)
|
||||
self._lm_warn = ctk.CTkLabel(
|
||||
tab,
|
||||
text="⚠ Unlink removes junction links from the Arma 3 directory. "
|
||||
"Mod files in downloads/ are NOT deleted.",
|
||||
text_color=_WARN_COLOR, anchor="w",
|
||||
)
|
||||
self._lm_warn = ctk.CTkLabel(tab, text=t("tools.lm_warn"),
|
||||
text_color=_WARN_COLOR, anchor="w")
|
||||
self._translatable.append((self._lm_warn, "tools.lm_warn"))
|
||||
|
||||
# Run button (label changes with command)
|
||||
self._lm_run_btn = ctk.CTkButton(
|
||||
tab, text="Show Status", width=180,
|
||||
tab, text=t("tools.lm_show_status"), width=180,
|
||||
command=self._lm_run,
|
||||
)
|
||||
self._lm_run_btn.grid(row=10, column=0, padx=24, pady=(16, 24), sticky="e")
|
||||
|
||||
def _lm_on_change(self, _: str = "") -> None:
|
||||
cmd = self._lm_cmd_var.get()
|
||||
labels = {"Status": "Show Status", "Link": "Create Links", "Unlink": "Remove Links"}
|
||||
# Keys are English segmented-button values; values are translated labels
|
||||
labels = {
|
||||
"Status": t("tools.lm_show_status"),
|
||||
"Link": t("tools.lm_create_links"),
|
||||
"Unlink": t("tools.lm_remove_links"),
|
||||
}
|
||||
self._lm_run_btn.configure(text=labels.get(cmd, cmd))
|
||||
|
||||
if cmd == "Unlink":
|
||||
@@ -269,19 +286,17 @@ class ToolsView(BaseView):
|
||||
cmd = self._lm_cmd_var.get().lower()
|
||||
group = self._lm_group_var.get()
|
||||
|
||||
if not group or group == "(no groups found)":
|
||||
messagebox.showwarning("No group selected",
|
||||
"Please select a group from the dropdown.")
|
||||
if not group or group == t("tools.no_groups"):
|
||||
messagebox.showwarning(t("tools.lm_no_group_title"),
|
||||
t("tools.lm_no_group_body"))
|
||||
return
|
||||
|
||||
args = ["link_mods.py", cmd, "--group", group]
|
||||
|
||||
if cmd == "unlink":
|
||||
confirmed = messagebox.askyesno(
|
||||
"Confirm Unlink",
|
||||
f"Remove junction links for group '{group}'?\n\n"
|
||||
"This removes links from the Arma 3 directory but does NOT delete "
|
||||
"mod files in downloads/.",
|
||||
t("tools.lm_confirm_title"),
|
||||
t("tools.lm_confirm_body", group=group),
|
||||
)
|
||||
if not confirmed:
|
||||
return
|
||||
@@ -296,17 +311,16 @@ class ToolsView(BaseView):
|
||||
tab = self._tab_view.tab("Sync Missing")
|
||||
tab.grid_columnconfigure(0, weight=1)
|
||||
|
||||
_desc(tab, row=0,
|
||||
text="Retry downloading mods that were missing from the server "
|
||||
"when you last ran the pipeline. "
|
||||
"Checks the server again and downloads any that have since appeared.")
|
||||
desc_lbl = _desc(tab, row=0, text=t("tools.sm_desc"))
|
||||
self._translatable.append((desc_lbl, "tools.sm_desc"))
|
||||
|
||||
self._sm_info = ctk.CTkLabel(tab, text="", text_color="gray", anchor="w")
|
||||
self._sm_info.grid(row=1, column=0, padx=24, pady=(4, 0), sticky="w")
|
||||
|
||||
ctk.CTkButton(tab, text="Run Sync Missing", width=180,
|
||||
command=lambda: self._launch(["sync_missing.py"])).grid(
|
||||
row=10, column=0, padx=24, pady=(16, 24), sticky="e")
|
||||
sm_btn = ctk.CTkButton(tab, text=t("tools.sm_btn"), width=180,
|
||||
command=lambda: self._launch(["sync_missing.py"]))
|
||||
sm_btn.grid(row=10, column=0, padx=24, pady=(16, 24), sticky="e")
|
||||
self._translatable.append((sm_btn, "tools.sm_btn"))
|
||||
|
||||
def _update_sm_label(self) -> None:
|
||||
cfg = self.app.cfg
|
||||
@@ -321,13 +335,11 @@ class ToolsView(BaseView):
|
||||
try:
|
||||
data = json.loads(report_path.read_text(encoding="utf-8"))
|
||||
count = data.get("missing", len(data.get("missing_mods", [])))
|
||||
self._sm_info.configure(
|
||||
text=f"{count} mod(s) currently listed as missing.")
|
||||
self._sm_info.configure(text=t("tools.sm_count", count=count))
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
self._sm_info.configure(
|
||||
text="No missing_report.json found — run the pipeline first.")
|
||||
self._sm_info.configure(text=t("tools.sm_no_report"))
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@@ -336,17 +348,16 @@ class ToolsView(BaseView):
|
||||
tab = self._tab_view.tab("Report Missing")
|
||||
tab.grid_columnconfigure(0, weight=1)
|
||||
|
||||
_desc(tab, row=0,
|
||||
text="Check which mods from comparison.json are absent from the "
|
||||
"file server. Saves missing_report.json so you can track what "
|
||||
"still needs to be added to the server.")
|
||||
desc_lbl = _desc(tab, row=0, text=t("tools.rm_desc"))
|
||||
self._translatable.append((desc_lbl, "tools.rm_desc"))
|
||||
|
||||
self._rm_info = ctk.CTkLabel(tab, text="", text_color="gray", anchor="w")
|
||||
self._rm_info.grid(row=1, column=0, padx=24, pady=(4, 0), sticky="w")
|
||||
|
||||
ctk.CTkButton(tab, text="Generate Report", width=180,
|
||||
command=lambda: self._launch(["report_missing.py"])).grid(
|
||||
row=10, column=0, padx=24, pady=(16, 24), sticky="e")
|
||||
rm_btn = ctk.CTkButton(tab, text=t("tools.rm_btn"), width=180,
|
||||
command=lambda: self._launch(["report_missing.py"]))
|
||||
rm_btn.grid(row=10, column=0, padx=24, pady=(16, 24), sticky="e")
|
||||
self._translatable.append((rm_btn, "tools.rm_btn"))
|
||||
|
||||
def _update_rm_label(self) -> None:
|
||||
cfg = self.app.cfg
|
||||
@@ -361,11 +372,11 @@ class ToolsView(BaseView):
|
||||
try:
|
||||
data = json.loads(report_path.read_text(encoding="utf-8"))
|
||||
ts = data.get("generated_at", "unknown")
|
||||
self._rm_info.configure(text=f"Last generated: {ts}")
|
||||
self._rm_info.configure(text=t("tools.rm_last", ts=ts))
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
self._rm_info.configure(text="No report yet.")
|
||||
self._rm_info.configure(text=t("tools.rm_none"))
|
||||
|
||||
# =========================================================================
|
||||
# Private — helpers
|
||||
@@ -400,10 +411,14 @@ def _desc(parent, row: int, text: str) -> ctk.CTkLabel:
|
||||
return lbl
|
||||
|
||||
|
||||
def _row(parent, row: int, label: str) -> ctk.CTkFrame:
|
||||
"""A label + horizontal frame for a settings row."""
|
||||
ctk.CTkLabel(parent, text=label, anchor="w", width=110).grid(
|
||||
row=row, column=0, padx=(24, 0), pady=6, sticky="w")
|
||||
def _row(parent, row: int, label: str) -> tuple[ctk.CTkFrame, ctk.CTkLabel]:
|
||||
"""A label + horizontal frame for a settings row.
|
||||
|
||||
Returns (content_frame, label_widget) so callers can register the label
|
||||
for later retranslation.
|
||||
"""
|
||||
lbl = ctk.CTkLabel(parent, text=label, anchor="w", width=110)
|
||||
lbl.grid(row=row, column=0, padx=(24, 0), pady=6, sticky="w")
|
||||
f = ctk.CTkFrame(parent, fg_color="transparent")
|
||||
f.grid(row=row, column=0, padx=(140, 24), pady=6, sticky="w")
|
||||
return f
|
||||
return f, lbl
|
||||
|
||||
Reference in New Issue
Block a user