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
187 lines
7.3 KiB
Python
187 lines
7.3 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Callable
|
|
|
|
import customtkinter as ctk
|
|
from tkinter import filedialog
|
|
|
|
from gui._constants import COLOR_OK, COLOR_ERROR, PROJECT_ROOT
|
|
from gui.locales import t
|
|
|
|
|
|
class SetupWizard(ctk.CTkToplevel):
|
|
"""Modal first-run wizard that writes config.json."""
|
|
|
|
def __init__(
|
|
self,
|
|
parent: ctk.CTk,
|
|
on_complete: Callable[[], None],
|
|
) -> None:
|
|
super().__init__(parent)
|
|
self.title(t("wizard.title"))
|
|
self.geometry("500x420")
|
|
self.resizable(False, False)
|
|
self.grab_set()
|
|
self.lift()
|
|
self.focus_force()
|
|
|
|
self._on_complete = on_complete
|
|
self._url = ctk.StringVar(value="https://")
|
|
self._user = ctk.StringVar()
|
|
self._pw = ctk.StringVar()
|
|
self._arma = ctk.StringVar()
|
|
|
|
self._body = ctk.CTkFrame(self, fg_color="transparent")
|
|
self._body.pack(fill="both", expand=True, padx=28, pady=24)
|
|
|
|
self._show(0)
|
|
|
|
def _clear(self) -> None:
|
|
for w in self._body.winfo_children():
|
|
w.destroy()
|
|
|
|
def _show(self, step: int) -> None:
|
|
self._clear()
|
|
[self._page_server, self._page_paths, self._page_review][step]()
|
|
|
|
# ── Page 1: server ───────────────────────────────────────────────────────
|
|
|
|
def _page_server(self) -> None:
|
|
ctk.CTkLabel(
|
|
self._body, text=t("wizard.step1_title"),
|
|
font=ctk.CTkFont(size=16, weight="bold"),
|
|
).pack(anchor="w")
|
|
ctk.CTkLabel(
|
|
self._body, text=t("wizard.step1_desc"),
|
|
text_color="gray",
|
|
).pack(anchor="w", pady=(4, 18))
|
|
|
|
for lbl_key, var, show in [
|
|
("wizard.label_url", self._url, ""),
|
|
("wizard.label_user", self._user, ""),
|
|
("wizard.label_pw", self._pw, "•"),
|
|
]:
|
|
ctk.CTkLabel(self._body, text=t(lbl_key)).pack(anchor="w")
|
|
ctk.CTkEntry(self._body, textvariable=var, width=440, show=show).pack(
|
|
anchor="w", pady=(2, 10))
|
|
|
|
foot = ctk.CTkFrame(self._body, fg_color="transparent")
|
|
foot.pack(fill="x", pady=(8, 0))
|
|
self._conn_lbl = ctk.CTkLabel(foot, text="", text_color="gray")
|
|
self._conn_lbl.pack(side="left")
|
|
ctk.CTkButton(foot, text=t("wizard.btn_next"), width=90,
|
|
command=lambda: self._show(1)).pack(side="right")
|
|
ctk.CTkButton(foot, text=t("wizard.btn_test"), width=140,
|
|
fg_color="transparent", border_width=1,
|
|
text_color=("gray10", "gray90"),
|
|
command=self._test).pack(side="right", padx=(0, 8))
|
|
|
|
def _test(self) -> None:
|
|
self._conn_lbl.configure(text=t("wizard.testing"), text_color="gray")
|
|
self.update()
|
|
try:
|
|
import requests
|
|
r = requests.get(self._url.get(),
|
|
auth=(self._user.get(), self._pw.get()),
|
|
timeout=8)
|
|
if r.ok:
|
|
self._conn_lbl.configure(text=t("wizard.connected"),
|
|
text_color=COLOR_OK)
|
|
else:
|
|
self._conn_lbl.configure(text=t("wizard.http_error", code=r.status_code),
|
|
text_color=COLOR_ERROR)
|
|
except Exception as e:
|
|
self._conn_lbl.configure(text=t("wizard.conn_error", e=e),
|
|
text_color=COLOR_ERROR)
|
|
|
|
# ── Page 2: paths ────────────────────────────────────────────────────────
|
|
|
|
def _page_paths(self) -> None:
|
|
ctk.CTkLabel(
|
|
self._body, text=t("wizard.step2_title"),
|
|
font=ctk.CTkFont(size=16, weight="bold"),
|
|
).pack(anchor="w")
|
|
ctk.CTkLabel(
|
|
self._body, text=t("wizard.step2_desc"),
|
|
text_color="gray", wraplength=440, justify="left",
|
|
).pack(anchor="w", pady=(4, 18))
|
|
|
|
ctk.CTkLabel(self._body, text=t("wizard.label_arma")).pack(anchor="w")
|
|
row = ctk.CTkFrame(self._body, fg_color="transparent")
|
|
row.pack(fill="x", pady=(2, 8))
|
|
ctk.CTkEntry(row, textvariable=self._arma, width=350).pack(side="left")
|
|
ctk.CTkButton(row, text=t("wizard.btn_browse"), width=80,
|
|
command=self._browse_arma).pack(side="left", padx=8)
|
|
|
|
ctk.CTkLabel(
|
|
self._body, text=t("wizard.step2_hint"),
|
|
text_color="gray", font=ctk.CTkFont(size=11),
|
|
wraplength=440, justify="left",
|
|
).pack(anchor="w", pady=(8, 0))
|
|
|
|
foot = ctk.CTkFrame(self._body, fg_color="transparent")
|
|
foot.pack(fill="x", pady=(20, 0))
|
|
ctk.CTkButton(foot, text=t("wizard.btn_back"), width=80,
|
|
fg_color="transparent", border_width=1,
|
|
text_color=("gray10", "gray90"),
|
|
command=lambda: self._show(0)).pack(side="left")
|
|
ctk.CTkButton(foot, text=t("wizard.btn_next"), width=80,
|
|
command=lambda: self._show(2)).pack(side="right")
|
|
|
|
def _browse_arma(self) -> None:
|
|
d = filedialog.askdirectory(title=t("wizard.browse_title"))
|
|
if d:
|
|
self._arma.set(d)
|
|
|
|
# ── Page 3: review + save ────────────────────────────────────────────────
|
|
|
|
def _page_review(self) -> None:
|
|
ctk.CTkLabel(
|
|
self._body, text=t("wizard.step3_title"),
|
|
font=ctk.CTkFont(size=16, weight="bold"),
|
|
).pack(anchor="w")
|
|
ctk.CTkLabel(
|
|
self._body, text=t("wizard.step3_desc"),
|
|
text_color="gray",
|
|
).pack(anchor="w", pady=(4, 14))
|
|
|
|
summary = (
|
|
f"Server URL: {self._url.get()}\n"
|
|
f"Username: {self._user.get()}\n"
|
|
f"Arma folder: {self._arma.get() or t('wizard.not_set')}\n"
|
|
)
|
|
box = ctk.CTkTextbox(self._body, height=90,
|
|
font=ctk.CTkFont(family="Consolas", size=12))
|
|
box.insert("1.0", summary)
|
|
box.configure(state="disabled")
|
|
box.pack(fill="x", pady=(0, 16))
|
|
|
|
foot = ctk.CTkFrame(self._body, fg_color="transparent")
|
|
foot.pack(fill="x")
|
|
ctk.CTkButton(foot, text=t("wizard.btn_back"), width=80,
|
|
fg_color="transparent", border_width=1,
|
|
text_color=("gray10", "gray90"),
|
|
command=lambda: self._show(1)).pack(side="left")
|
|
ctk.CTkButton(foot, text=t("wizard.btn_save"), width=120,
|
|
command=self._save).pack(side="right")
|
|
|
|
def _save(self) -> None:
|
|
cfg = {
|
|
"server": {
|
|
"base_url": self._url.get(),
|
|
"username": self._user.get(),
|
|
"password": self._pw.get(),
|
|
},
|
|
"paths": {
|
|
"arma_dir": self._arma.get(),
|
|
"downloads": "downloads",
|
|
"modlist_html": "modlist_html",
|
|
"modlist_json": "modlist_json",
|
|
},
|
|
}
|
|
(PROJECT_ROOT / "config.json").write_text(
|
|
json.dumps(cfg, indent=2), encoding="utf-8")
|
|
self.destroy()
|
|
self._on_complete()
|