Files
arma-modlist-tools/CLAUDE.md
Tran G. (Revernomad) Khoa 903cd366e2 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
2026-04-08 16:58:41 +07:00

8.8 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Common Commands

# Run all tests (no network required)
python test_suite.py

# Check Python version and dependencies
python check_deps.py

# Full pipeline (parse → compare → fetch → link)
python run.py

# Parse + compare only (no download, no linking)
python run.py --skip-fetch --skip-link

# Diagnose mod folder name / steam_id issues
python check_names.py
python check_names.py --fix --fix-ids

# Launch the GUI
python gui.py

There is no build step, linter config, or package install beyond pip install -r requirements.txt.

Architecture

Package vs CLI layer

arma_modlist_tools/ is a pure library — no I/O side effects, no sys.exit, no print. All CLI scripts (run.py, fetch_mods.py, link_mods.py, etc.) sit at the project root and call into the package. New functionality goes in the package first, then a CLI script wraps it.

Data flow

modlist_html/*.html
    └─ parser.parse_modlist_dir()
           └─ compare.compare_presets()
                  └─ comparison.json  ←─ source of truth for groups + mod identity
                         ├─ fetcher.build_server_index()  ←─ Caddy JSON API
                         │      └─ fetcher.find_mod_folder()  (steam_id first, name fallback)
                         │             └─ downloads/{group}/@ModName/
                         │                    └─ linker.link_group()
                         │                           └─ arma_dir/@ModName  (junction/symlink)
                         └─ reporter.build_missing_report()  →  missing_report.json

Group naming convention

  • "shared" — mods present in all compared presets
  • "<preset_name>" — mods unique to one preset (key from comparison["unique"])

This group label is stored in missing_report.json per-mod so sync_missing.py knows where to place newly available mods without re-reading comparison.json.

Server index structure

build_server_index() returns:

{
    "by_steam_id": {"450814997": "https://server/@cba_a3/"},  # primary lookup
    "by_name":     {"cbaa3":     "https://server/@cba_a3/"},  # normalized fallback
    "folders":     [...]                                       # raw Caddy listing
}

_normalize_name strips @, lowercases, removes all non-alphanumeric: "@CBA_A3""cbaa3". Used in both the index builder and every lookup.

Detection: os.path.islink() returns False for Windows junctions. Always use _is_junction() from linker.py, which checks st_file_attributes & 0x400 (FILE_ATTRIBUTE_REPARSE_POINT) on Windows.

Removal: Use os.rmdir() on Windows and os.unlink() on Linux. Never shutil.rmtree() — it follows the junction and deletes the target mod files.

Creation: cmd /c mklink /J <link> <target> on Windows, os.symlink() on Linux.

check_names.py classification (two-pass)

Pass 1 collects raw (server_name, local_steam_id) for every disk folder. Pass 2 builds ok_disk_names — the set of disk names that already match the server exactly. Any MISMATCH whose proposed server name is in ok_disk_names is reclassified as ID_COLLISION (the local meta.cpp has a wrong publishedid that belongs to a different mod). This prevents false rename suggestions caused by shared/duplicate steam IDs on the server.

--fix-ids corrects meta.cpp using steam IDs from comparison.json (sourced from Steam Workshop URLs in the HTML presets) as the authoritative source.

GUI package

gui/ is a CustomTkinter desktop application wrapping the CLI toolchain. Entry point is gui.py at the project root, which calls gui.run_app().

Key files:

  • gui/__init__.py — sets dark theme + blue color scheme; exports run_app()
  • gui/app.pyArmaModManagerApp main window; manages view routing, config loading, thread-safe log queue, and background pipeline execution
  • gui/wizard.pySetupWizard dialog shown on first launch when no config.json exists
  • gui/_constants.py — window dimensions, status color constants, file paths
  • gui/_io.py_QueueWriter redirects stdout/stderr to a thread-safe queue so pipeline output streams into the Logs view

Views (gui/views/): each inherits BaseView; build() runs once on creation, refresh() runs on each navigation:

  • dashboard.py — overview, status, quick stats
  • mods.py — browse and manage downloaded mods by group
  • tools.py — link/unlink, rename folders, sync missing mods, check server
  • logs.py — real-time log viewer fed from the stdout/stderr queue
  • settings.py — in-app editor for config.json (server URL, paths, credentials)

_find_folder (mods.py) — three-level name matching: The mods view resolves a mod's local folder by mod name from comparison.json, which may differ from the server-canonical folder name used by the fetcher. Lookup order:

  1. Exact: @{mod_name}
  2. Case-insensitive: @CBA_A3 matches CBA_A3
  3. Normalized (_normalize_name): strips all non-alphanumeric — handles punctuation/spacing differences, e.g. @US GEAr- Units (IFA3) matches US GEAr: Units (IFA3) (both → usgearunitsifa3)

selection.json — GUI selection state file, tracked in git. Persists which mods/groups are selected between GUI sessions. Written by the GUI; safe to delete (GUI recreates it on next save).

run_tool subprocess streaming: Tool scripts are launched via subprocess.Popen (not subprocess.run) with stdout=PIPE, stderr=STDOUT, read line-by-line via iter(proc.stdout.readline, ""), and posted to the log queue immediately. Python's own output buffering is disabled with the -u flag and PYTHONUNBUFFERED=1 in the environment — without these, output would batch inside the pipe and only appear when the script exits.

GUI localization (gui/locales.py)

All user-facing strings are centralised in gui/locales.py. Two languages are supported: English ("en") and Vietnamese ("vi").

API:

from gui.locales import t, set_language, get_language

t("nav.dashboard")                          # → "Dashboard" or "Tổng quan"
t("dashboard.stats", total=42, shared=10)   # → "42 mods · 10 shared"
set_language("vi")                          # switch active language
get_language()                              # → "vi"

Key naming: flat dot-notation — "<view>.<widget_purpose>", e.g. "dashboard.run_btn", "wizard.step1_title", "tools.cn_warn".

Dynamic strings use str.format_map with keyword args. The dict value contains {placeholder} and the caller passes t("key", placeholder=value).

Hot-swap: app.switch_language(lang) calls set_language(), saves the preference to config.json under "ui": {"language": "..."}, retranslates sidebar nav buttons, then calls view.refresh() on every cached view. Views that build all content in refresh() (Settings, Mods) update automatically. Views with static build()-time widgets (Dashboard, Logs, Tools) store widget references and retranslate them at the top of refresh().

Constraints:

  • CTkTabview tab names in tools.py are kept in English — they double as frame lookup keys (tv.tab("Check Names")) and cannot be renamed after creation.
  • Segmented button values in tools.py ("Status", "Link", "Unlink") are kept in English — they drive the logic in _lm_on_change().
  • _VIEW_NAMES routing keys ("Dashboard", "Mods", etc.) are kept in English — they are _view_cache dict keys.

Adding a new string: Add the key to both _EN and _VI dicts in locales.py. The assert set(_EN.keys()) == set(_VI.keys()) guard at module load will catch any mismatch.

Python Version Compatibility

Minimum is Python 3.9. All files that use X | Y union type annotations must have from __future__ import annotations as the first import. Without it, the | syntax raises TypeError at runtime on Python < 3.10. Every module in arma_modlist_tools/ already has it; any new CLI script you add must include it too.

Test Suite

test_suite.py uses a custom harness (no pytest/unittest dependency). Structure:

group("section name")   # prints header
test("description", callable)  # runs fn, catches exceptions, tracks pass/fail
skip("description", "reason")  # marks skipped

Tests that exercise the linker use tempfile.TemporaryDirectory() — never the real arma_dir. Tests that would require network calls mock list_mod_files with unittest.mock.patch.

Key Files Not in Git

  • config.json — credentials + paths (copy from config.template.json)
  • downloads/ — downloaded mod files, can be several GB
  • modlist_json/ — generated JSON output

The .html preset files in modlist_html/ are tracked as example inputs.