- Add arma_modlist_tools/cleaner.py: find_orphan_folders() detects @ModName folders no longer referenced in comparison.json; uses _normalize_name from fetcher for consistent three-level matching - Add clean_orphans.py: CLI with --dry-run and --yes/-y flags; junction-safe deletion via _is_junction() guard before shutil.rmtree - Add Clean Orphans tab to gui/views/tools.py: scrollable checkbox list, background scan/delete threads, pending-done-msg pattern for post-scan status, EN/VI localization strings in gui/locales.py - Add 23 unit tests (section 12), 6 E2E subprocess tests (section 13), 23 coverage-gap tests (section 14), 9 live-server fetcher tests (section 15) - Fix leaked builtins.open mock in _test_read_os_release_parses_file - Overall coverage: 84% → 93%; fetcher.py: 36% → 72%
531 lines
30 KiB
Python
531 lines
30 KiB
Python
from __future__ import annotations
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Localization module — English + Vietnamese
|
|
#
|
|
# Usage:
|
|
# from gui.locales import t
|
|
# label_text = t("dashboard.title")
|
|
# label_text = t("dashboard.sel_count", n_sel=2, n_total=5)
|
|
#
|
|
# Tab names in ToolsView are NOT translated — they double as CTkTabview
|
|
# lookup keys and cannot be renamed after creation.
|
|
# Segmented-button values in ToolsView ("Status", "Link", "Unlink") are also
|
|
# kept in English because they drive internal logic in _lm_on_change().
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_LANG: str = "en"
|
|
|
|
_EN: dict[str, str] = {
|
|
# ── App / sidebar ────────────────────────────────────────────────────────
|
|
"app.title": "Arma Mod Manager",
|
|
"nav.dashboard": "Dashboard",
|
|
"nav.mods": "Mods",
|
|
"nav.tools": "Tools",
|
|
"nav.logs": "Logs",
|
|
"nav.settings": "Settings",
|
|
|
|
# ── Pipeline step headers (printed to log) ───────────────────────────────
|
|
"pipeline.step1_name": "Parse presets",
|
|
"pipeline.step2_name": "Compare presets",
|
|
"pipeline.step3_name": "Download mods",
|
|
"pipeline.step4_name": "Link mods",
|
|
|
|
# ── app.py dialogs ────────────────────────────────────────────────────────
|
|
"app.dlg_presets_title": "Not enough presets selected",
|
|
"app.dlg_presets_body": (
|
|
"Please select at least 2 preset files to compare.\n\n"
|
|
"Use the checkboxes on the Dashboard to choose which presets to use."
|
|
),
|
|
"app.dlg_setup_title": "Setup required",
|
|
"app.dlg_setup_body": "Please complete Setup first.",
|
|
|
|
# ── run_tool status lines (go to log) ────────────────────────────────────
|
|
"app.tool_done": "✓ Done",
|
|
"app.tool_exit_code": "✗ Exited with code {code}",
|
|
"app.tool_failed": "✗ Failed to start {script}: {e}",
|
|
|
|
# ── Dashboard ────────────────────────────────────────────────────────────
|
|
"dashboard.title": "Dashboard",
|
|
"dashboard.refresh_btn": "⟳ Refresh",
|
|
"dashboard.preset_card_title": "Preset Files",
|
|
"dashboard.preset_card_desc": "HTML exports from Arma 3 Launcher → Mods → Export to HTML",
|
|
"dashboard.sel_count": "{n_sel} of {n_total} selected",
|
|
"dashboard.btn_none": "None",
|
|
"dashboard.btn_all": "All",
|
|
"dashboard.btn_add": "+ Add Preset Files",
|
|
"dashboard.no_config": "No config found. Complete Setup first.",
|
|
"dashboard.folder_missing": "Folder missing:\n{path}",
|
|
"dashboard.no_presets": "No preset files yet.\nUse the button below to add them.",
|
|
"dashboard.file_dialog_title": "Select Arma 3 Launcher preset files",
|
|
"dashboard.dlg_setup_title": "Setup required",
|
|
"dashboard.dlg_setup_body": "Please complete Setup before adding presets.",
|
|
"dashboard.pipeline_title": "Pipeline Status",
|
|
"dashboard.step_parse": "Parse presets",
|
|
"dashboard.step_compare": "Compare presets",
|
|
"dashboard.step_download": "Download mods",
|
|
"dashboard.step_link": "Link to Arma",
|
|
"dashboard.stats": "{total} mods · {shared} shared",
|
|
"dashboard.stats_missing": "\n{missing} missing from server",
|
|
"dashboard.run_btn": "▶ Run Full Pipeline",
|
|
"dashboard.running": "Running…",
|
|
|
|
# ── Mods ─────────────────────────────────────────────────────────────────
|
|
"mods.title": "Mods",
|
|
"mods.refresh_btn": "⟳ Refresh",
|
|
"mods.check_btn": "☁ Check Updates",
|
|
"mods.check_btn_checking": "Checking…",
|
|
"mods.search_label": "Search:",
|
|
"mods.search_placeholder": "Filter mods in active tab…",
|
|
"mods.no_config": "No config found. Complete Setup first.",
|
|
"mods.no_data": (
|
|
"No mod data yet.\n"
|
|
"Go to Dashboard, select your presets, then click Run Full Pipeline."
|
|
),
|
|
"mods.read_error": "Error reading comparison.json: {e}",
|
|
"mods.col_name": "Mod Name",
|
|
"mods.col_downloaded": "Downloaded",
|
|
"mods.col_linked": "Linked",
|
|
"mods.col_server": "Server Status",
|
|
"mods.status_ok": "✓ Up to date",
|
|
"mods.status_stale": "⚠ {n} outdated",
|
|
"mods.status_not_downloaded": "—",
|
|
"mods.status_not_on_server": "Not on server",
|
|
"mods.status_error": "✗ Error",
|
|
"mods.status_checking": "Checking…",
|
|
"mods.update_btn": "Update",
|
|
|
|
# ── Logs ─────────────────────────────────────────────────────────────────
|
|
"logs.title": "Logs",
|
|
"logs.copy_btn": "Copy",
|
|
"logs.clear_btn": "Clear",
|
|
|
|
# ── Settings ─────────────────────────────────────────────────────────────
|
|
"settings.title": "Settings",
|
|
"settings.server_card_title": "Server & Path Configuration",
|
|
"settings.server_card_desc": (
|
|
"Re-run the setup wizard to change your server URL, "
|
|
"credentials, or Arma folder."
|
|
),
|
|
"settings.wizard_btn": "Open Setup Wizard",
|
|
"settings.appearance_title": "Appearance",
|
|
"settings.language_title": "Language",
|
|
"settings.config_title": "Current Configuration",
|
|
|
|
# ── Wizard ───────────────────────────────────────────────────────────────
|
|
"wizard.title": "Setup — Arma Mod Manager",
|
|
"wizard.step1_title": "Step 1 of 3 — Server Connection",
|
|
"wizard.step1_desc": "Enter the details for your Caddy mod server.",
|
|
"wizard.label_url": "Server URL",
|
|
"wizard.label_user": "Username",
|
|
"wizard.label_pw": "Password",
|
|
"wizard.btn_next": "Next →",
|
|
"wizard.btn_test": "Test Connection",
|
|
"wizard.testing": "Testing…",
|
|
"wizard.connected": "✓ Connected",
|
|
"wizard.http_error": "✗ HTTP {code}",
|
|
"wizard.conn_error": "✗ {e}",
|
|
"wizard.step2_title": "Step 2 of 3 — Arma 3 Server Folder",
|
|
"wizard.step2_desc": (
|
|
"Point to your Arma 3 Server installation. "
|
|
"Links (junctions) will be created here."
|
|
),
|
|
"wizard.label_arma": "Arma 3 Server folder",
|
|
"wizard.btn_browse": "Browse",
|
|
"wizard.step2_hint": (
|
|
"All other folders (downloads, presets) will be created "
|
|
"automatically next to this tool."
|
|
),
|
|
"wizard.btn_back": "← Back",
|
|
"wizard.step3_title": "Step 3 of 3 — Review & Save",
|
|
"wizard.step3_desc": "Check your settings, then click Save.",
|
|
"wizard.not_set": "(not set)",
|
|
"wizard.btn_save": "Save & Open",
|
|
"wizard.browse_title": "Select Arma 3 Server folder",
|
|
|
|
# ── Tools — shared ────────────────────────────────────────────────────────
|
|
"tools.title": "Tools",
|
|
"tools.label_group": "Group:",
|
|
"tools.label_options": "Options:",
|
|
"tools.label_command": "Command:",
|
|
"tools.all_groups": "All groups",
|
|
"tools.no_groups": "(no groups found)",
|
|
|
|
# ── Tools — Check Names ──────────────────────────────────────────────────
|
|
"tools.cn_desc": (
|
|
"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)."
|
|
),
|
|
"tools.cn_fix_chk": "Auto-fix folder name mismatches (--fix)",
|
|
"tools.cn_fix_ids_chk": "Auto-fix wrong Steam IDs in meta.cpp (--fix-ids)",
|
|
"tools.cn_warn": (
|
|
"⚠ --fix renames folders and updates junctions. "
|
|
"--fix-ids rewrites meta.cpp files."
|
|
),
|
|
"tools.cn_btn": "Run Check Names",
|
|
|
|
# ── Tools — Update Mods ──────────────────────────────────────────────────
|
|
"tools.um_desc": (
|
|
"Re-download mod files whose size on the server differs from "
|
|
"your local copy. Use --force to re-download everything "
|
|
"regardless of size."
|
|
),
|
|
"tools.um_mod_label": "Mod folder:",
|
|
"tools.um_mod_placeholder": "Optional — e.g. @ace",
|
|
"tools.um_mod_hint": "(only when a specific group is selected)",
|
|
"tools.um_force_chk": "Force re-download all files (--force)",
|
|
"tools.um_warn": (
|
|
"⚠ --force re-downloads every file regardless of size. "
|
|
"This may transfer a large amount of data."
|
|
),
|
|
"tools.um_btn": "Run Update",
|
|
|
|
# ── Tools — Link Mods ────────────────────────────────────────────────────
|
|
"tools.lm_desc": (
|
|
"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)."
|
|
),
|
|
"tools.lm_warn": (
|
|
"⚠ Unlink removes junction links from the Arma 3 directory. "
|
|
"Mod files in downloads/ are NOT deleted."
|
|
),
|
|
"tools.lm_show_status": "Show Status",
|
|
"tools.lm_create_links": "Create Links",
|
|
"tools.lm_remove_links": "Remove Links",
|
|
"tools.lm_no_group_title": "No group selected",
|
|
"tools.lm_no_group_body": "Please select a group from the dropdown.",
|
|
"tools.lm_confirm_title": "Confirm Unlink",
|
|
"tools.lm_confirm_body": (
|
|
"Remove junction links for group '{group}'?\n\n"
|
|
"This removes links from the Arma 3 directory but does NOT delete "
|
|
"mod files in downloads/."
|
|
),
|
|
|
|
# ── Tools — Sync Missing ─────────────────────────────────────────────────
|
|
"tools.sm_desc": (
|
|
"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."
|
|
),
|
|
"tools.sm_btn": "Run Sync Missing",
|
|
"tools.sm_count": "{count} mod(s) currently listed as missing.",
|
|
"tools.sm_no_report": "No missing_report.json found — run the pipeline first.",
|
|
|
|
# ── Tools — Report Missing ───────────────────────────────────────────────
|
|
"tools.rm_desc": (
|
|
"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."
|
|
),
|
|
"tools.rm_btn": "Generate Report",
|
|
"tools.rm_last": "Last generated: {ts}",
|
|
"tools.rm_none": "No report yet.",
|
|
|
|
# ── Tools — Clean Orphans ────────────────────────────────────────────────
|
|
"tools.oc_desc": (
|
|
"Scan the downloads folder for mod folders that are no longer "
|
|
"referenced in comparison.json. These orphans accumulate when you "
|
|
"remove mods from your presets and re-run the pipeline. "
|
|
"Select the ones you want to remove to free up disk space."
|
|
),
|
|
"tools.oc_warn": (
|
|
"⚠ Deleting orphans permanently removes mod files from disk. "
|
|
"This cannot be undone."
|
|
),
|
|
"tools.oc_scan_btn": "Scan for Orphans",
|
|
"tools.oc_scanning": "Scanning…",
|
|
"tools.oc_no_config": "No config found. Complete Setup first.",
|
|
"tools.oc_no_comparison": "No comparison.json found — run the pipeline first.",
|
|
"tools.oc_none_found": "No orphans found. Your downloads folder is clean.",
|
|
"tools.oc_found": "{count} orphan(s) found — {size} total",
|
|
"tools.oc_sel_all": "Select All",
|
|
"tools.oc_sel_none": "Deselect All",
|
|
"tools.oc_delete_btn": "Delete Selected",
|
|
"tools.oc_confirm_title": "Confirm Delete",
|
|
"tools.oc_confirm_body": (
|
|
"Permanently delete {count} orphan folder(s) ({size})?\n\n"
|
|
"This cannot be undone."
|
|
),
|
|
"tools.oc_done": "Deleted {count} folder(s), freed {size}.",
|
|
"tools.oc_error": "Error deleting {path}: {e}",
|
|
"tools.oc_error_title": "Delete errors",
|
|
"tools.oc_scan_error": "Scan error: {e}",
|
|
}
|
|
|
|
_VI: dict[str, str] = {
|
|
# ── App / sidebar ────────────────────────────────────────────────────────
|
|
"app.title": "Arma Mod Manager",
|
|
"nav.dashboard": "Tổng quan",
|
|
"nav.mods": "Danh sách Mod",
|
|
"nav.tools": "Công cụ",
|
|
"nav.logs": "Nhật ký",
|
|
"nav.settings": "Cài đặt",
|
|
|
|
# ── Pipeline step headers ────────────────────────────────────────────────
|
|
"pipeline.step1_name": "Phân tích preset",
|
|
"pipeline.step2_name": "So sánh preset",
|
|
"pipeline.step3_name": "Tải mod",
|
|
"pipeline.step4_name": "Liên kết mod",
|
|
|
|
# ── app.py dialogs ────────────────────────────────────────────────────────
|
|
"app.dlg_presets_title": "Chưa chọn đủ preset",
|
|
"app.dlg_presets_body": (
|
|
"Vui lòng chọn ít nhất 2 tệp preset để so sánh.\n\n"
|
|
"Sử dụng các ô tick ở Tổng quan để chọn preset."
|
|
),
|
|
"app.dlg_setup_title": "Cần thiết lập",
|
|
"app.dlg_setup_body": "Vui lòng hoàn thành thiết lập trước.",
|
|
|
|
# ── run_tool status lines ────────────────────────────────────────────────
|
|
"app.tool_done": "✓ Hoàn thành",
|
|
"app.tool_exit_code": "✗ Thoát với mã lỗi {code}",
|
|
"app.tool_failed": "✗ Không thể khởi động {script}: {e}",
|
|
|
|
# ── Dashboard ────────────────────────────────────────────────────────────
|
|
"dashboard.title": "Tổng quan",
|
|
"dashboard.refresh_btn": "⟳ Làm mới",
|
|
"dashboard.preset_card_title": "Tệp Preset",
|
|
"dashboard.preset_card_desc": "Xuất từ Arma 3 Launcher → Mods → Export to HTML",
|
|
"dashboard.sel_count": "Đã chọn {n_sel} / {n_total}",
|
|
"dashboard.btn_none": "Bỏ chọn",
|
|
"dashboard.btn_all": "Tất cả",
|
|
"dashboard.btn_add": "+ Thêm tệp Preset",
|
|
"dashboard.no_config": "Chưa tìm thấy cấu hình. Vui lòng hoàn thành thiết lập.",
|
|
"dashboard.folder_missing": "Thư mục không tồn tại:\n{path}",
|
|
"dashboard.no_presets": "Chưa có tệp preset.\nDùng nút bên dưới để thêm.",
|
|
"dashboard.file_dialog_title": "Chọn tệp preset Arma 3 Launcher",
|
|
"dashboard.dlg_setup_title": "Cần thiết lập",
|
|
"dashboard.dlg_setup_body": "Vui lòng hoàn thành thiết lập trước khi thêm preset.",
|
|
"dashboard.pipeline_title": "Trạng thái Pipeline",
|
|
"dashboard.step_parse": "Phân tích preset",
|
|
"dashboard.step_compare": "So sánh preset",
|
|
"dashboard.step_download": "Tải mod",
|
|
"dashboard.step_link": "Liên kết với Arma",
|
|
"dashboard.stats": "{total} mod · {shared} dùng chung",
|
|
"dashboard.stats_missing": "\n{missing} mod thiếu trên máy chủ",
|
|
"dashboard.run_btn": "▶ Chạy toàn bộ quy trình",
|
|
"dashboard.running": "Đang chạy…",
|
|
|
|
# ── Mods ─────────────────────────────────────────────────────────────────
|
|
"mods.title": "Danh sách Mod",
|
|
"mods.refresh_btn": "⟳ Làm mới",
|
|
"mods.check_btn": "☁ Kiểm tra cập nhật",
|
|
"mods.check_btn_checking": "Đang kiểm tra…",
|
|
"mods.search_label": "Tìm kiếm:",
|
|
"mods.search_placeholder": "Lọc mod trong tab hiện tại…",
|
|
"mods.no_config": "Chưa tìm thấy cấu hình. Vui lòng hoàn thành thiết lập.",
|
|
"mods.no_data": (
|
|
"Chưa có dữ liệu mod.\n"
|
|
"Vào Tổng quan, chọn preset rồi nhấn Chạy toàn bộ quy trình."
|
|
),
|
|
"mods.read_error": "Lỗi đọc comparison.json: {e}",
|
|
"mods.col_name": "Tên Mod",
|
|
"mods.col_downloaded": "Đã tải",
|
|
"mods.col_linked": "Đã liên kết",
|
|
"mods.col_server": "Trạng thái máy chủ",
|
|
"mods.status_ok": "✓ Đã cập nhật",
|
|
"mods.status_stale": "⚠ {n} tệp cũ",
|
|
"mods.status_not_downloaded": "—",
|
|
"mods.status_not_on_server": "Không có trên máy chủ",
|
|
"mods.status_error": "✗ Lỗi",
|
|
"mods.status_checking": "Đang kiểm tra…",
|
|
"mods.update_btn": "Cập nhật",
|
|
|
|
# ── Logs ─────────────────────────────────────────────────────────────────
|
|
"logs.title": "Nhật ký",
|
|
"logs.copy_btn": "Sao chép",
|
|
"logs.clear_btn": "Xóa",
|
|
|
|
# ── Settings ─────────────────────────────────────────────────────────────
|
|
"settings.title": "Cài đặt",
|
|
"settings.server_card_title": "Cấu hình máy chủ & đường dẫn",
|
|
"settings.server_card_desc": (
|
|
"Mở lại trình hướng dẫn thiết lập để thay đổi URL máy chủ, "
|
|
"thông tin đăng nhập hoặc thư mục Arma."
|
|
),
|
|
"settings.wizard_btn": "Mở trình thiết lập",
|
|
"settings.appearance_title": "Giao diện",
|
|
"settings.language_title": "Ngôn ngữ",
|
|
"settings.config_title": "Cấu hình hiện tại",
|
|
|
|
# ── Wizard ───────────────────────────────────────────────────────────────
|
|
"wizard.title": "Thiết lập — Arma Mod Manager",
|
|
"wizard.step1_title": "Bước 1 / 3 — Kết nối máy chủ",
|
|
"wizard.step1_desc": "Nhập thông tin máy chủ Caddy của bạn.",
|
|
"wizard.label_url": "URL máy chủ",
|
|
"wizard.label_user": "Tên đăng nhập",
|
|
"wizard.label_pw": "Mật khẩu",
|
|
"wizard.btn_next": "Tiếp theo →",
|
|
"wizard.btn_test": "Kiểm tra kết nối",
|
|
"wizard.testing": "Đang kiểm tra…",
|
|
"wizard.connected": "✓ Đã kết nối",
|
|
"wizard.http_error": "✗ HTTP {code}",
|
|
"wizard.conn_error": "✗ {e}",
|
|
"wizard.step2_title": "Bước 2 / 3 — Thư mục Arma 3 Server",
|
|
"wizard.step2_desc": (
|
|
"Trỏ tới thư mục cài đặt Arma 3 Server của bạn. "
|
|
"Các liên kết (junction) sẽ được tạo tại đây."
|
|
),
|
|
"wizard.label_arma": "Thư mục Arma 3 Server",
|
|
"wizard.btn_browse": "Duyệt",
|
|
"wizard.step2_hint": (
|
|
"Các thư mục khác (downloads, presets) sẽ được tạo tự động "
|
|
"bên cạnh công cụ này."
|
|
),
|
|
"wizard.btn_back": "← Quay lại",
|
|
"wizard.step3_title": "Bước 3 / 3 — Xem lại & Lưu",
|
|
"wizard.step3_desc": "Kiểm tra cài đặt rồi nhấn Lưu.",
|
|
"wizard.not_set": "(chưa đặt)",
|
|
"wizard.btn_save": "Lưu & Mở",
|
|
"wizard.browse_title": "Chọn thư mục Arma 3 Server",
|
|
|
|
# ── Tools — shared ────────────────────────────────────────────────────────
|
|
"tools.title": "Công cụ",
|
|
"tools.label_group": "Nhóm:",
|
|
"tools.label_options": "Tùy chọn:",
|
|
"tools.label_command": "Lệnh:",
|
|
"tools.all_groups": "Tất cả nhóm",
|
|
"tools.no_groups": "(không tìm thấy nhóm)",
|
|
|
|
# ── Tools — Check Names ──────────────────────────────────────────────────
|
|
"tools.cn_desc": (
|
|
"Quét thư mục mod và so sánh với máy chủ. "
|
|
"Báo cáo tên không khớp (MISMATCH), thư mục không nhận ra "
|
|
"(NOT_ON_SERVER) và Steam ID sai trong meta.cpp (ID_COLLISION)."
|
|
),
|
|
"tools.cn_fix_chk": "Tự động sửa tên thư mục không khớp (--fix)",
|
|
"tools.cn_fix_ids_chk": "Tự động sửa Steam ID sai trong meta.cpp (--fix-ids)",
|
|
"tools.cn_warn": (
|
|
"⚠ --fix đổi tên thư mục và cập nhật junction. "
|
|
"--fix-ids ghi đè tệp meta.cpp."
|
|
),
|
|
"tools.cn_btn": "Chạy kiểm tra tên",
|
|
|
|
# ── Tools — Update Mods ──────────────────────────────────────────────────
|
|
"tools.um_desc": (
|
|
"Tải lại tệp mod có kích thước khác với bản trên máy chủ. "
|
|
"Dùng --force để tải lại tất cả bất kể kích thước."
|
|
),
|
|
"tools.um_mod_label": "Thư mục mod:",
|
|
"tools.um_mod_placeholder": "Không bắt buộc — ví dụ @ace",
|
|
"tools.um_mod_hint": "(chỉ dùng khi chọn một nhóm cụ thể)",
|
|
"tools.um_force_chk": "Buộc tải lại tất cả tệp (--force)",
|
|
"tools.um_warn": (
|
|
"⚠ --force tải lại mọi tệp bất kể kích thước. "
|
|
"Điều này có thể truyền một lượng dữ liệu lớn."
|
|
),
|
|
"tools.um_btn": "Chạy cập nhật",
|
|
|
|
# ── Tools — Link Mods ────────────────────────────────────────────────────
|
|
"tools.lm_desc": (
|
|
"Quản lý liên kết junction/symlink giữa thư mục downloads "
|
|
"và thư mục Arma 3.\n"
|
|
"Status — xem liên kết hiện có. "
|
|
"Link — tạo junction còn thiếu. "
|
|
"Unlink — xóa junction (tệp mod KHÔNG bị xóa)."
|
|
),
|
|
"tools.lm_warn": (
|
|
"⚠ Unlink xóa liên kết junction khỏi thư mục Arma 3. "
|
|
"Tệp mod trong downloads/ KHÔNG bị xóa."
|
|
),
|
|
"tools.lm_show_status": "Xem trạng thái",
|
|
"tools.lm_create_links": "Tạo liên kết",
|
|
"tools.lm_remove_links": "Xóa liên kết",
|
|
"tools.lm_no_group_title": "Chưa chọn nhóm",
|
|
"tools.lm_no_group_body": "Vui lòng chọn một nhóm từ danh sách.",
|
|
"tools.lm_confirm_title": "Xác nhận xóa liên kết",
|
|
"tools.lm_confirm_body": (
|
|
"Xóa liên kết junction cho nhóm '{group}'?\n\n"
|
|
"Thao tác này xóa liên kết khỏi thư mục Arma 3 "
|
|
"nhưng KHÔNG xóa tệp mod trong downloads/."
|
|
),
|
|
|
|
# ── Tools — Sync Missing ─────────────────────────────────────────────────
|
|
"tools.sm_desc": (
|
|
"Thử tải lại các mod bị thiếu trên máy chủ khi chạy pipeline lần trước. "
|
|
"Kiểm tra lại máy chủ và tải về nếu mod đã xuất hiện."
|
|
),
|
|
"tools.sm_btn": "Chạy đồng bộ mod thiếu",
|
|
"tools.sm_count": "{count} mod đang được liệt kê là thiếu.",
|
|
"tools.sm_no_report": "Chưa có missing_report.json — hãy chạy pipeline trước.",
|
|
|
|
# ── Tools — Report Missing ───────────────────────────────────────────────
|
|
"tools.rm_desc": (
|
|
"Kiểm tra mod nào trong comparison.json không có trên máy chủ. "
|
|
"Lưu missing_report.json để theo dõi mod cần bổ sung."
|
|
),
|
|
"tools.rm_btn": "Tạo báo cáo",
|
|
"tools.rm_last": "Tạo lần cuối: {ts}",
|
|
"tools.rm_none": "Chưa có báo cáo.",
|
|
|
|
# ── Tools — Clean Orphans ────────────────────────────────────────────────
|
|
"tools.oc_desc": (
|
|
"Quét thư mục downloads để tìm các thư mục mod không còn được "
|
|
"tham chiếu trong comparison.json. Các mod mồ côi này tích tụ khi "
|
|
"bạn xóa mod khỏi preset và chạy lại pipeline. "
|
|
"Chọn các thư mục muốn xóa để giải phóng dung lượng."
|
|
),
|
|
"tools.oc_warn": (
|
|
"⚠ Xóa mod mồ côi sẽ xóa vĩnh viễn tệp mod khỏi ổ đĩa. "
|
|
"Thao tác này không thể hoàn tác."
|
|
),
|
|
"tools.oc_scan_btn": "Quét mod mồ côi",
|
|
"tools.oc_scanning": "Đang quét…",
|
|
"tools.oc_no_config": "Chưa tìm thấy cấu hình. Vui lòng hoàn thành thiết lập.",
|
|
"tools.oc_no_comparison": "Chưa có comparison.json — hãy chạy pipeline trước.",
|
|
"tools.oc_none_found": "Không tìm thấy mod mồ côi. Thư mục downloads sạch.",
|
|
"tools.oc_found": "Tìm thấy {count} mod mồ côi — tổng {size}",
|
|
"tools.oc_sel_all": "Chọn tất cả",
|
|
"tools.oc_sel_none": "Bỏ chọn",
|
|
"tools.oc_delete_btn": "Xóa đã chọn",
|
|
"tools.oc_confirm_title": "Xác nhận xóa",
|
|
"tools.oc_confirm_body": (
|
|
"Xóa vĩnh viễn {count} thư mục mồ côi ({size})?\n\n"
|
|
"Thao tác này không thể hoàn tác."
|
|
),
|
|
"tools.oc_done": "Đã xóa {count} thư mục, giải phóng {size}.",
|
|
"tools.oc_error": "Lỗi khi xóa {path}: {e}",
|
|
"tools.oc_error_title": "Lỗi xóa",
|
|
"tools.oc_scan_error": "Lỗi quét: {e}",
|
|
}
|
|
|
|
# Guard: both dicts must have identical key sets
|
|
assert set(_EN.keys()) == set(_VI.keys()), (
|
|
"EN/VI key mismatch: "
|
|
+ str(set(_EN.keys()) ^ set(_VI.keys()))
|
|
)
|
|
|
|
_STRINGS: dict[str, dict[str, str]] = {"en": _EN, "vi": _VI}
|
|
|
|
|
|
def set_language(lang: str) -> None:
|
|
"""Set the active language. Unknown codes fall back to English."""
|
|
global _LANG
|
|
_LANG = lang if lang in _STRINGS else "en"
|
|
|
|
|
|
def get_language() -> str:
|
|
"""Return the currently active language code."""
|
|
return _LANG
|
|
|
|
|
|
def t(key: str, **kwargs: object) -> str:
|
|
"""Look up *key* in the active language, falling back to English then the key itself.
|
|
|
|
Dynamic placeholders use str.format_map with keyword arguments::
|
|
|
|
t("dashboard.sel_count", n_sel=2, n_total=5)
|
|
# dict entry: "{n_sel} of {n_total} selected"
|
|
"""
|
|
text = _STRINGS[_LANG].get(key) or _STRINGS["en"].get(key, key)
|
|
if kwargs:
|
|
try:
|
|
return text.format_map(kwargs)
|
|
except (KeyError, IndexError):
|
|
return text
|
|
return text
|