Three issues caused the Logs view to appear blank during a real pipeline run:
1. `from run import step_fetch, step_link` was outside the worker's
try/except/finally. An import failure silently killed the thread,
leaving _pipeline_done uncalled and the Run button stuck disabled
forever. Now wrapped in its own try/except that posts the error to
the log and resets the UI.
2. `build_server_index` makes N sequential HTTP requests (one per mod
folder's meta.cpp) with no output during the scan. Added an optional
`progress_fn(current, total, name)` callback; step_fetch wires it to
print progress every 25 folders so the log never goes silent.
3. No immediate feedback after clicking Start — the log was blank until
the worker thread started printing. Now posts a "Pipeline started"
banner from the main thread before the worker launches.
GUI log batching (_poll_log now drains queue into a single CTkTextbox.insert
call per 80 ms tick instead of N calls, each with see("end") scroll).
_QueueWriter strips ANSI/CSI escape codes and bare \r before enqueuing so
tqdm progress output is legible in the log textbox. OSC sequences terminated
by both BEL (\x07) and ST (\x1b\) are handled.
Wizard "Test Connection" moved off the main thread: requests.get runs in a
daemon thread; result posted back via after(0, ...). Widget refs captured
before thread launch to prevent stale updates if user navigates away. Bare
except narrowed to TclError (destroyed-widget guard only).
Code quality: import os moved to module level in app.py; _read_raw_config()
helper extracted to deduplicate dual raw config.json reads; return type
annotations added to _get_view_class, _get_dashboard, and cfg property.
Tests: 11 new unit tests for _QueueWriter (RED -> GREEN on OSC-ST fix).
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
subprocess.run with capture_output=True blocked until process exit,
dumping all output at once. Now uses Popen with line-by-line reading,
-u flag, and PYTHONUNBUFFERED=1 so logs stream in real time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_find_folder() used a plain lowercase compare which failed when the
server canonical folder name differs from the comparison.json mod name
in more than just case (e.g. spaces vs underscores: "NIArms All in One"
vs "@NIArms_All_In_One"). These mods showed ✗ even though the pipeline
found and linked them correctly.
Add a normalized-name fallback (strips non-alphanumeric, same logic as
fetcher._normalize_name) so the lookup matches the same way the fetcher
resolves mods from the server index.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>