v0.2.3
[v0.2.3] — 2026-06-21
Added
- Add NexusMods Translation Browser: search, download, and import as TM
New gui/nexusmods_client.py provides a thin NexusMods v1 API + search
wrapper (search, mod files, download links, streaming download).
New gui/nexusmods_browser_dialog.py is a two-panel QDialog: left panel
has game selector + search box + results table; right panel shows mod
detail, file list, and three actions — open mod page in browser,
Download & Import as TM, Download & Merge into Current. Zip archives
are automatically unpacked to find .strings/.dlstrings/.ilstrings files.
Free-account download 403s are handled gracefully by opening the mod
page in the browser as a fallback.
TranslationMemory.load_strings_file() added to load a BethesdaStringFile
directly as a TM source by string ID.
Wired into File → Browse NexusMods for Translations… in MainWindow;
imported TMs are applied to the active worker and any open file via
the existing import_translations() path.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(acbb0c4)
- Add Visual Context Preview: render strings in Bethesda UI boxes with game fonts
New gui/visual_context_preview.py is a dockable panel (View → Visual
Context Preview, Ctrl+Shift+P) that renders the current string inside
a faithful simulation of the Starfield in-game UI box so translators
can see exactly how text will wrap and whether it overflows the box.
Font setup: RF_35_M (Cyrillic body), RF_55_M (Cyrillic bold),
NB_Architekt_Light (Latin body), NB_Architekt (Latin bold) — all
extracted from fonts_en.swf / fonts_uk.swf via JPEXS and stored in
data/fonts/. Registered with QFontDatabase at runtime; falls back to
system sans-serif when files are absent.
Context presets (Dialogue 680px/3 lines, Quest 400px, Book 480px/22
lines, Note 440px, Terminal 560px monospace, UI 340px, General 520px)
are auto-detected from StringType and can be overridden by the user.
Box dimensions match Starfield's 1280×720 native Scaleform canvas.
Per-string stats bar shows char delta (Src/Trl/%), line count, and a
colour-coded ✓ Fits / OVERFLOW badge. Overflow lines are tinted red
inside the box; an OVERFLOW badge appears when the text exceeds the
box height. Source / Translation / Both view modes available.
Wired into MainWindow._on_selection_changed(); updates only when the
panel is visible (no cost when hidden).
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(6694774)
- Add _restore_missing_newlines to translation pipeline
When the Ollama model merges two lines into one (e.g. translates
"Line1.\nLine2." as "Line1. Line2."), the pipeline now detects the
dropped newline and restores it using proportional positioning —
advancing past sentence-ending punctuation and absorbing the
inter-sentence space the model inserted.
Applied at three sites:
- Batch-loop cache hit: corrects already-cached bad translations
before emitting translation_ready, so a previous bad result is
silently fixed without requiring a manual retranslate. - _translate_single single-call post-processing: covers strings that
bypass the line-by-line path (e.g. lines > 200 chars). - _translate_chunked post-processing: covers chunked translations.
The line-by-line path (>= 2 lines, each < 200 chars) still takes
priority and preserves newlines by construction; this is a safety net
for the cases that fall through to a single model call.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(d32bc0a)
- Add UI Constraint Enforcer: flag translations >40% longer than English original
Introduce UI_OVERFLOW QC code (WARNING severity) that fires when a translation
exceeds 1.4× the English source length on strings of 20+ characters — the
threshold at which Bethesda hardcoded UI bounding boxes (dialogue panels,
pip-boy entries, loading-screen tips) begin to clip text.
- UI_OVERFLOW_RATIO = 1.40 constant at module level for easy tuning
- Detail field carries an exact character budget (orig×1.4) and the overshoot
so the retry hint gives the AI a concrete target: "shorten by N+ chars" - Added to RETRANSLATE_CODES so Auto-Retranslate Issues picks it up
automatically and re-queues the string with a "shorten while retaining
meaning" prompt before it reaches a human reviewer - Short originals (< 20 chars) are excluded — single words and short labels
have noisy ratios and are unlikely to be clipped by a few extra characters
(they fall through to the existing LENGTH_INCREASE / SUSPICIOUSLY_LONG codes)
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(ff306fa)
- Add Vim Macro Recording for batch string operations
MacroRecorder stores a sequence of steps (regex replace, set status) and
replays them against StringTableModel rows in one batch — fixing repetitive
formatting issues across thousands of strings without manual per-row edits.
UI: MacroDialog (Ctrl+M or 'q' in table) lets users build a step list, preview
match count before applying, choose scope (all/translated/pending/selected),
and play with a cancellable progress dialog. '@' key replays the last macro on
the focused row. Both actions are registered in KeyboardManager/CommandPalette.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(3b938d7)
- Add micro-animations: progress bar pulse and QA success badge
On successful batch translation (zero failures): the progress bar flashes green
and does a two-pulse opacity animation (1→0.5→1→0.5→1→0 over ~1.1s) before
hiding, giving clear visual confirmation that the batch completed cleanly.
On manual quality check (Ctrl+F7) with zero issues: skips the empty dialog and
instead shows a floating "✓ Quality check passed" toast that fades in (220ms),
holds (1.5s), and fades out (450ms), then self-destructs. More satisfying than
an empty results window.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(fdfb415)
- Add 492 unfinished translation placeholders for new UI strings across all 6 locales
New GUI dialogs and features added since last translation pass (audio preview,
advanced search/replace, NexusMods browser, macro recorder, dialogue tree,
spell checker, focus overlay, weblate sync, etc.) now have empty
<translation type="unfinished"> entries in all .ts files so translators
can fill them in. Recompiled all .qm binaries.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(6dbc0f5)
- Add ти/ви register consistency checker (Ctrl+Alt+R)
New Quality menu entry "Check Register (ти/ви)…" scans all translated
strings and groups them by inferred NPC speaker (EDID prefix in ESP mode,
whole-file bucket for .strings/.dlstrings). Any speaker whose lines mix
informal ти-address (ти/тебе/твій…) with formal ви-address (ви/вас/ваш…)
is flagged. The results dialog shows a speaker table with counts and two
side-by-side evidence panels with one-click jump-to-row navigation.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(2cff1ce)
- Add Ukrainian gender agreement checker (Ctrl+Alt+G)
New Quality menu entry "Check Gender Agreement…" detects adjective/noun
gender mismatches in translated Ukrainian strings. Covers adj→noun and
noun←adj (predicative) patterns.
Implementation:
- gender_checker.py: curated ~250-noun gender dictionary (M/F/N) plus
determiner/possessive table (цей/ця/це, наш/наша/наше, etc.).
Masculine adj detection: -ий/-ій suffix (reliable, no verb collision).
Neuter adj detection: -е/-є suffix with stem-validation against the
Ukrainian word list (e.g. "переможе"→"переможий"✗ = verb, not adj). - gender_dialog.py: splitter dialog — mismatch table + HTML context
preview with adj/noun highlighted, jump-to-row navigation. - main_window.py: Ctrl+Alt+G action wired in Quality menu, enabled
alongside other file-level checks.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(bfa01e4)
- Add named Translation Sessions (Ctrl+Shift+N / Ctrl+Shift+S)
New "Sessions" top-level menu lets users save and resume named work
contexts that are completely separate from crash-recovery snapshots.
What a session captures:
• Session name + optional note
• Source file path and type
• Current cursor row and vertical scroll position
• Last advanced-search filter (query, column, status, flags)
• Set of string IDs translated during this session (tracked live
via table_model.dataChanged; only strings newly-translated after
the session was started are counted)
Session storage:
• gui/session_manager.py — WorkSession / SearchState dataclasses +
SessionStore (one JSON file per session in
~/.config/BethesdaModTools/sessions/.json)
• Atomic write (tmp → rename), version-tagged, slug-deduplication
Session dialog (gui/session_dialog.py):
• SessionManagerDialog — table of all sessions with per-session
translated count, last-modified date, detail panel; Resume /
Rename / Delete actions; rename propagated to store on close
• NewSessionDialog / RenameSessionDialog
Main-window integration:
• New Session (Ctrl+Shift+N) — snapshots baseline translated IDs
• Save Session (Ctrl+Shift+S) — writes cursor + scroll + file path
• Save Session As… — clone with new name
• Manage Sessions… — opens SessionManagerDialog
• Recent Sessions submenu (up to 8, rebuilt after every save)
• Resume — re-runs saved search programmatically via
AdvancedSearchDialog, then restores scroll + cursor
• Window title includes active session name in brackets
• Session auto-saved on clean app exit
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(6e4098a)
- Add five animation layers across the UI
• Table row flash: translated rows fade from green on each AI update
(~330 ms, 60 fps QTimer decay via _FLASH_ROLE in StringTableModel)
• Smooth progress bar: SmoothProgressBar eases value changes (200 ms
OutCubic tween) instead of jumping; wired to translation progress
• Dialog fade-in: FadeInMixin applied to QualityDialog, GenderDialog,
RegisterDialog, ConsistencyDialog (180 ms opacity 0→1, effect removed)
• Welcome card pulse: slow infinite opacity breathe (0.80↔1.0, 2.4 s)
paused on drag-enter, resumed on drag-leave; DropOverlay fades in
• In-app toasts: slide-up + fade-in _ToastWidget anchored bottom-right,
stacking multiple toasts; shown on translation-complete and file-save
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(689e43c)
- Add NexusMods API key field to Settings dialog
Previously the key was only editable inside the upload dialog itself;
now it has a dedicated Settings → NexusMods group with a password-masked
key field (Show/Hide toggle) and the optional File Group ID field.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(d6169e7)
-
Add Download & Open in Editor for ESM/ESP/ESL files from NexusMods
-
NexusModFile.is_plugin property for .esp/.esm/.esl detection
-
PLUGIN_EXTS exported from nexusmods_client
-
_extract_plugins() helper: handles direct plugin files and zips
-
New "Download & Open in Editor" button enabled for plugin/archive files
-
open_file_requested signal emitted after successful download; dialog
closes itself automatically -
main_window connects the signal to _open_file_path()
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(a39145b)
-
Add GPG release signing and SHA256 verification
-
release-signing-key.asc: RSA-4096 project public key
(fingerprint D50C3274546FE1FB0653DA01E750D9A94177134B, expires 2030-06-11) -
VERIFICATION.md: step-by-step guide for verifying downloads
-
release.yml: generate SHA256SUMS, sign with GPG (secret GPG_PRIVATE_KEY +
GPG_PASSPHRASE), upload SHA256SUMS + SHA256SUMS.asc alongside release ZIPs
Required GitHub Secrets:
GPG_PRIVATE_KEY — gpg --armor --export-secret-keys KEYID | base64 -w0
GPG_PASSPHRASE — key passphrase (empty string if none)
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(3adfeaf)
- Add MIT License badge to README
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(0a1188f)
- Add free-user NexusMods downloads via browser cookies + curl-cffi
Premium API path is tried first; on a 403, the client automatically
falls back to the DownloadPopUp flow: extract browser session cookies
from Firefox or Chromium, hit the DownloadPopUp endpoint with Chrome
TLS impersonation (curl_cffi), parse the signed key+expires tokens, and
pass them to the download_link API — the same technique NXM managers use.
Also fixes a latent bug where _DownloadWorker called stream_download()
but NexusClient only defined download_file() (premium downloads were
silently failing). Renamed to stream_download() with a cancelled event
parameter to match the worker's existing call site.
Free-user error codes FREE_NO_CURL_CFFI / FREE_NO_COOKIES /
FREE_SESSION_EXPIRED / FREE_POPUP_PARSE surface targeted hints in the
browser dialog instead of a generic "requires premium" message.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(ac8369a)
- Add curl-cffi to requirements.txt as optional dependency
Required for free-user NexusMods downloads (Chrome TLS impersonation
to bypass Cloudflare on the DownloadPopUp endpoint).
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(4a7f121)
- Add Cookies JSON file setting for free-user NexusMods downloads
Adds Settings → NexusMods → Cookies JSON field with a Browse button.
Point it at a Cookie-Editor JSON export as a fallback when auto-detection
from Firefox/Chromium fails.
- AppSettings.nexusmods_cookies_file (CONFIG_VERSION 28→29)
- find_browser_cookies(cookies_file="") loads file first if given;
supports both Cookie-Editor list format and plain dict format - NexusClient.init accepts cookies_file= and passes it through to
_free_download_url via find_browser_cookies - NexusModsBrowserDialog.init accepts cookies_file= forwarded from
main_window using settings.nexusmods_cookies_file
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(3939433)
- Add SECURITY.md with vulnerability reporting policy
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(1e9c526)
- Add minimal GPU monitor widget to the status bar
Shows GPU%, VRAM used/total, and temperature — the three metrics relevant
for monitoring Ollama AI translation workloads — updated every 2 seconds.
Backend priority:
- AMD sysfs (/sys/class/drm/card*/device/{gpu_busy_percent,
mem_info_vram_*} + hwmon temp, no dependencies) - NVIDIA nvidia-smi subprocess (no dependencies)
Color coding: green (low) → yellow (medium) → red (high) for all three
metrics. Widget hides itself automatically if no supported GPU is found.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(b87fa5e)
-
Add Korean (ko) language support
-
_LANG_DISPLAY: "ko" → "Korean"
-
_TARGET_STYLE["ko"]: formal polite endings (합쇼체/해요체), Hangul
transliteration for proper nouns, NASApunk sci-fi tone guidance -
_LANG_EXAMPLES[("en","ko")]: three representative string pairs
-
quality_checker: _HANGUL_TARGETS frozenset; _check_script_coverage
extended to detect missing Hangul (가-힣 / Jamo) alongside existing
CJK/kana checks; fires LOW_SCRIPT_COVERAGE if translation has no
Hangul in a sufficiently long string -
ko_word_checker.py: lazy-loading frequency dictionary wrapper +
has_hangul() script-presence helper (no data file required to load) -
settings_dialog.py: "Korean" added to SUPPORTED_LANGUAGES
-
app_settings.py: "Korean" → "ko" in legacy display-name migration map
claude_client.py reuses _LANG_DISPLAY from ollama_worker so Korean
is automatically available in the Claude API backend as well.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(8a2bee3)
-
Add Korean frequency word list and fix Hangul min-length filtering
-
data/korean_words.txt: 50,000 words from hermitdave/FrequencyWords
(ko_50k.txt, subtitle corpus, "word count" format) -
_word_checker_base.py: add min_word_len parameter (default 3, unchanged
for all existing Latin-script checkers) -
ko_word_checker.py: use min_word_len=1 since Hangul syllable blocks are
single Unicode characters; text_has_korean_words() counts Hangul chars
directly as the primary signal (faster, no dict load needed) and falls
back to frequency list for stricter matching
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(744b1a3)
-
Add Korean (ko_KR) UI translation template
-
gui/translations/ko_KR.ts: 1,356 source strings extracted via
pyside6-lupdate from all 78 Python source files; all entries marked
type="unfinished" ready for translation -
settings_dialog.py: added ("ko_KR", "Korean", "한국어", False) to the
Interface Language combo
Run ./scripts/compile_translations.sh after translating to generate ko_KR.qm.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(576583c)
- Add UNCLOSED_GUILLEMET QC check + fix TerraBrew glossary entry
Adds a new auto-fixable quality check that detects when a translation
opens «guillemets without a matching closing ». The auto-fix appends
the missing » before any trailing sentence punctuation or at end-of-line.
Also fixes the glossary: Terrabrew→Террабрю corrected to TerraBrew→ТерраБрю,
and adds a Cyrillic source entry ТерраБрю→ТерраБрю so GLOSSARY_MISMATCH
fires when a Ukrainian translation uses the English form instead of the
Cyrillic brand name.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(100e985)
- Add Starfield interface TXT file support (translate_en.txt / translate_ru.txt)
New TxtStringFile parser in bethesda_strings/txt_handler.py handles UTF-16
$KEYVALUE line format with lossless round-trip save. StringTableModel
gets a 'txt' mode (load_from_txt_file / apply_changes_to_txt_file) that
displays the $KEY string as the ID column instead of a hex offset. Opening
a .txt via File > Open or drag-and-drop auto-detects the Starfield format
and routes to the new _open_txt_file() handler; save/save-as write back
UTF-16 LE with BOM. Non-Starfield .txt files get a clear error message.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(1827a9e)
- Add AI-fix mode, TXT interface file support, and QC false-positive fixes
AI-fix mode (Ollama):
- New fix_translation field on TranslationRequest; to_prompt() / to_system_prompt()
build a proofreader prompt instead of a translation prompt when set - QualityDialog: "AI Fix Selected" button populates pending_ai_fixes list
- MainWindow._ai_fix_with_hints() dispatches fix requests to the running worker
- is_retry also triggers for fix_translation to ensure cache/TM are always bypassed
- "Already Ukrainian" early-exit guard skipped in fix mode (source is always English)
- build_retry_hint: added UNCLOSED_GUILLEMET, UNMATCHED_BRACKET, ENCODING_ERROR,
and a catch-all fallback so hint is never silently empty
TXT interface file support:
- bethesda_strings/txt_handler.py: TxtStringFile + TxtStringEntry; UTF-16-LE round-trip
- StringTableModel: mode="txt", load_from_txt_file, apply_changes_to_txt_file
- MainWindow: open/save/save-as wiring, encoding label exclusion
QC false-positive fixes:
- MISSING_NUMBER: exclude pure-zero groups (thousands separators like "10 000")
- REPETITIVE_CONTENT: skip flag when source text is itself repetitive (list strings)
- SOURCE_LANGUAGE_LEAK: expanded _UK_EXCLUSION with Ukrainian infinitives, adverbs,
verb conjugations that appear in the Russian dictionary
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(34e5dbd)
- Add Auto-Fix All button and SUSPICIOUSLY_LONG auto-fix
QualityDialog: "Auto-Fix All" button fixes every string with auto-fixable
issues in one click — no selection required. With 1,205 unclosed guillemets
from old translations, selecting them one by one was not practical.
Button shows live count (Auto-Fix All (1205)) and updates after applying.
QualityChecker: SUSPICIOUSLY_LONG added to AUTOFIX_CODES with a fix that
truncates hallucinated multi-string output to the first meaningful line when
the source has no newlines but the translation has many. This catches the
pattern where the model continued generating content for the next string.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(2945273)
- Add case and line-prefix fixes to translation pipeline
_fix_case_and_line_prefix() added to both post-translation blocks so these
errors never reach the output for future translations:
-
CASE_MISMATCH: if source starts uppercase and translation starts lowercase
(or vice-versa), match the first-character case to the source. Catches the
common model error of returning a suffix like "ство" instead of "Мародерство". -
LINE_PREFIX_DROPPED: if a source line starts with '-' and the translated
line dropped it (e.g. '-Costs' → 'Вартість' instead of '-Вартість'), restore
the prefix. Only applied when line counts match to avoid false insertions.
Both fixes run after _restore_missing_newlines in the post-processing chain,
so the line count is stable before the line-by-line prefix check runs.
The existing XML was translated before this fix; use QC → Auto-Fix All there.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(745927b)
- Add per-code hide filter to QC dialog
Filter bar now has a "Hide" button next to the code combo. When a specific
code is selected and "Hide" is clicked, all rows for that code are excluded
from the table. A "Hidden: CODE1, CODE2" strip appears below the filter bar
with a "Show all codes" button to reset.
Useful for suppressing noise: e.g. hide EMPTY_TRANSLATION (4997 rows) and
UNTRANSLATED (85) so the table shows only MISSING_NUMBER, EXTRA_TAG etc.
A report with both a hidden and a visible code stays visible (only rows
where ALL issue codes are hidden are removed).
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(a63f5f4)
- Add 6 new built-in themes: Gruvbox, Tokyo Night, Monokai, One Dark, Solarized Light, Sepia
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(4e6cb65)
- Add Starfield and Starfield Terminal themes based on game assets
Colors reverse-engineered from Data/Interface SWF/GFX files:
- Starfield: dark navy (#0a0e1a) + cyan accent (#3ff0ff) matching the
in-game inventory/dialogue/mission menus; speaker gold (#e8e8ac) for
primary buttons; selection bg/color matching dialoguemenu.swf (#152030/#3ff0ff) - Starfield Terminal: green-on-black (#030e03/#00cc00) from the game's
computer terminal screens (visual_context_preview Terminal context)
Both themes include QTabBar and QDockWidget rules.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(a87a9a5)
- Add Built with Claude badge to README
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(1ae9c10)
- Add custom background / wallpaper support with theme integration
BackgroundManager (gui/background_manager.py):
- Static images: PNG, JPG, JPEG, BMP, TIFF, WEBP, ICO, SVG (9 formats)
- Animated GIF via QMovie (loops automatically)
- Video: MP4, AVI, MKV, WEBM, MOV, WMV, FLV, M4V, MPEG, OGV (11 formats)
via QMediaPlayer + QVideoSink; falls back to static load if
PySide6-Multimedia / GStreamer unavailable - Fit modes: cover, contain, stretch, tile, center
- Opacity control 0–100%
- _BackgroundWidget is WA_TransparentForMouseEvents; positioned below all
other main-window children via lower(); resizes with the window
AppSettings: +background_enabled, +background_path, +background_opacity,
+background_fit_mode; CONFIG_VERSION bumped to 30
Settings dialog: new "Background / Wallpaper" group (enable checkbox, file
browser with combined image+video filter, fit mode combo, opacity slider)
MainWindow: instantiates BackgroundManager before _setup_ui(); applies on
init and after every settings dialog save; resizeEvent keeps it full-size
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(fcfc291)
- Add automatic update checker with download dialog
gui/updater.py (new):
- UpdateChecker (QThread): hits GitHub releases API, compares semantic
versions, emits update_available(version, changelog, assets) or no_update - DownloadWorker (QThread): streams asset to ~/Downloads with progress(0-100)
gui/update_dialog.py (new):
- Shows new version, full GitHub release changelog, per-platform asset size
- Download button with live QProgressBar; auto-selects linux/win/mac zip
- Falls back to "Open in Browser" when no matching asset found
- After download: "Open Downloads Folder" button
AppSettings: +check_updates_on_startup (default True), +last_known_update
(suppresses re-notification for already-seen versions); CONFIG_VERSION → 31
Settings dialog: new "Updates" group with startup toggle + "Check Now" button
MainWindow:
- Help → "Check for Updates…" menu item
- Silent check fires 8 s after startup (skipped in dev builds)
- Manual check always shows result (up-to-date or failure message)
- _on_update_available_silent skips dialog if version already seen
main.py: bump applicationVersion 0.2.1 → 0.2.2
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(414e76c)
- Add 58-tip rotating advice panel to settings dialog
Adds _GENERAL_TIPS class constant with 58 workflow tips covering
keyboard shortcuts, QC pipeline, translation memory, glossary,
focus mode, NexusMods, spell/gender/register checkers, and more.
Shows a random tip on open with a 'Next tip →' button to cycle
through the full pool one by one.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(6e74b73)
- Add NexusMods banner image and version to mod detail panel
When a mod card is clicked, fire a second background request to
GET /v1/games/{domain}/mods/{mod_id}.json to fetch the full mod
info. Use the returned picture_url (full banner, not the small
thumbnail used in search cards) to display a 180px-tall banner
image at the top of the detail panel, and add the version number
to the meta label. Results for a superseded mod are discarded via
_banner_mod_id guard to prevent race conditions. Banner images are
cached in banner_cache/ alongside the existing thumb_cache/.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(1f5431c)
- Add .txt chip to NexusMods header (Starfield Interface strings)
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(62cea76)
- Add cleanup for doubled-percent artifact "% % f" in _clean_translation
When the old space-flag printf_var pattern protected "% for" as a token,
the model would re-add "%" after the number (e.g. "15%"), then the restored
token "% f" would follow, giving "15 % % f протягом" in the output.
The term_protector fix (previous commit) prevents new occurrences. This
cleanup handles cached translations and any future model hallucinations of
the same pattern: "% % <format_char>" is collapsed
to a single "%" since space-separated doubled percents are never valid in
Bethesda game strings. "%%f" (no whitespace between) is left untouched.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(6508a9f)
- Add CONDITIONAL_BLOCKS complexity check to pre_translation_estimator
The test expected a CONDITIONAL_BLOCKS issue for strings containing Bethesda
gender/player conditional tags ([MALE]/[FEMALE]/[PLYR] etc.) but the check
was never implemented — the estimator only emitted MODERATE_TAG_DENSITY.
Adds _CONDITIONAL_TAG_RE pattern, _check_conditional_blocks() static method
(penalty 12, SEVERITY_WARNING when ≥2 conditional tags found), wires it into
_estimate_inner and _active_features, and adds "conditional_blocks" to
_DEFAULT_WEIGHTS.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(a3e41af)
- Add automatic post-translation self-review (fix critical issues, skip visual)
After every translation batch, the app now runs a fully automatic self-review
loop — no dialogs, no manual checking — and ends with one summary message:
- Mechanical auto-fix of every fixable issue (newlines, whitespace, tags,
Russian-char leakage, spurious quotes, …) via QualityChecker.fix_all. - Re-check, then AI-retranslate (with quality-feedback hints) any string
still carrying a CRITICAL (non-visual) issue. - Repeat, bounded by _SELF_REVIEW_MAX_PASSES (2) and a no-progress guard
so it always terminates (e.g. RU proper nouns the model echoes verbatim
stop after one stalled pass instead of looping forever).
Critical vs visual classification (new VISUAL_ONLY_CODES in quality_checker):
cosmetic issues — UI_OVERFLOW, LENGTH_INCREASE, CASE_MISMATCH, SPURIOUS_QUOTES,
NEWLINE_COUNT_MISMATCH, whitespace mismatches, SENTENCE_COUNT_MISMATCH,
LINE_PREFIX_DROPPED, SIZE_TAG_RESTRUCTURED — are still mechanically fixed when
possible but never trigger a (slow, GPU-bound) retranslation on their own.
Everything else (EMPTY_TRANSLATION, UNTRANSLATED, MISSING/EXTRA_TAG,
SOURCE_LANGUAGE_LEAK, ENGLISH_LEAK, LOW_*_COVERAGE, ENCODING_ERROR, …) is
treated as critical and fixed automatically.
Details:
- quality_checker.py: VISUAL_ONLY_CODES, QualityReport.has_critical_issue /
.critical_issues(), QualityChecker.issue_is_visual_only(). - main_window.py: _self_review_begin / _run_pass / _finish state machine wired
into _on_ollama_finished; the same handler re-enters the loop after each
retranslation batch and suppresses the normal per-batch popups during the
cycle. Extracted _announce_batch_complete. _retranslate_with_hints now returns
bool so the loop can detect an empty batch. Respects the Stop button (won't
start or continue the loop when the user stopped) and mechanical fixes are
applied via set_translated_text_batch for one repaint. - app_settings.py: auto_self_review (default True), CONFIG_VERSION 31→32 + migration.
- settings_dialog.py: "Automatic self-review after translation" checkbox.
Opt-out via Settings → AI Quality Check → uncheck the box; disabling falls back
to the previous announce + silent-QC behaviour.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(0d6049e)
- Add colored [INFO]/[WARN]/[ERROR] logging system
Introduce gui/log_setup.py: a LevelTagFormatter that renders each log
record's level as a bracketed, color-coded tag ([INFO] green, [WARN]
yellow, [ERROR] red, plus [DEBUG] grey and [CRIT] bold-on-red). Colors
are emitted only on a TTY (honoring NO_COLOR/FORCE_COLOR); the
translator.log file handler stays ANSI-free and grep-friendly.
Wire setup_logging() into main.py, replacing the plain basicConfig.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(054b120)
- Add ko_KR to supported locales list in CLAUDE.md
Korean was added to gui/translations and settings_dialog but the docs
locale list was never updated.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(e41da44)
- Add benchmarks for core I/O, fuzzy matching, and translation cache
Extends the benchmark suite beyond TermProtector to cover the other
performance-critical pure-Python hot paths, in the same self-contained,
dependency-free style:
- bench_core_io.py: BethesdaStringFile parse/serialize throughput for
.strings vs .dlstrings, get_by_id lookup, and dedup-on-save. This is
the path every file open and save runs through. - bench_fuzzy_match.py: Levenshtein / longest-common-substring primitives,
fuzzy_score throughput, and best_fuzzy_match translation-memory lookup
(the per-candidate cost that scales with TM size). - bench_translation_cache.py: make_key (sha256), LRU set+evict, get
hit/miss, and 10-thread concurrent throughput to expose lock/GIL
contention.
Add benchmarks/README.md listing all four scripts and how to run them.
Sizes are tuned so each script completes in a few seconds; the fuzzy
benchmark deliberately uses short, game-realistic strings since the DP
primitives are O(n·m) in pure Python.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(25e3004)
- Add native Starfield voice playback (Wwise .wem) to the audio panel
Translators need the original voice acting (not just TTS) to match actor
tone, pacing, and lip-sync timing. Starfield ships voice as Wwise .wem
(RIFF/WAVE, Vorbis tag 0xFFFF) inside Voices.ba2 — not .fuz like
Skyrim/FO4 — at sound/voice/.esm//<8hex-formid>.wem.
- bethesda_strings/wwise_voice.py: new pure-Python VoiceIndex. Scans the
English voice archives in a Data dir, maps FormID -> [voice clips] (one
FormID is recorded by several voice types), extracts the .wem, and
decodes to a cached WAV via vgmstream-cli (ffmpeg cannot decode Wwise
Vorbis). Pure helpers: classify_archive_language / form_id_from_name /
voice_type_from_name. - audio_preview_panel.py: FormID box + voice-type combo + "Load voice"
button. Index builds in a background worker; decode runs off the UI
thread. ESP/ESM mode auto-fills the FormID from the row id and warms the
index; .strings mode takes a manual hex FormID. The decoded clip feeds
the existing original-audio slot (player, duration label, timing bar).
Also fixes the original-audio auto-locate, which read a non-existent
"string_id" row key and so never matched (model keys it as "id"). - app_settings.py: voice_data_dir, vgmstream_binary, voice_language;
CONFIG_VERSION 32 -> 33. - settings_dialog.py: Settings -> Audio rows for the three voice fields
with browse buttons and a language picker (English default). - tests/test_wwise_voice.py: 26 tests. Pure-function tests always run; the
end-to-end BA2 decode test self-skips without the game / vgmstream-cli.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(931244b)
- Add NPC & Speaker Mapping panel
Give translators immediate "who is speaking" context for Starfield's
branching dialogue, derived from the voice-type folder of each line's
voice clip (the realistic per-line speaker signal) via the shared
FormID-keyed VoiceIndex.
- gui/speaker_map.py: pure-Python layered parser turning a raw Wwise
voice-type folder (npcfsarahmorgan, crimsonfleetfemale03, announcerf…,
cr_…, robot…, playervoice…, sfbgs001_…) into a friendly
name/gender/faction/category SpeakerInfo. Curated NAMED_NPCS +
_FACTION_LABELS; describe_voice_types() dedups. Names starting with
f/m/x (npcfrankrenick) resolve via dict before gender-splitting. - gui/speaker_panel.py: SpeakerPanel dock — thin renderer of the current
line's speaker + "also voiced by" shared-line alternatives + manual
FormID lookup. Calls a resolver; renders async results. - audio_preview_panel.py: own the single shared VoiceIndex; add
resolve_speaker(form_id) + speakerResolved signal (queues lookups
until the index build finishes; emits None when unconfigured). - main_window.py: create SpeakerPanel tabified with the Audio panel,
wire resolver + signal, push current row on selection, share toggle
and settings-driven visibility. - tests/test_speaker_map.py: 47 pure-function tests (named curated +
fallback, gender f/m/x, generic faction+variant, creature/announcer/
robot/player/test/expressions, cut + content-pack markers, dedup).
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(39a0d2b)
- Add VMAD (Papyrus script property) analysis with safe lock/warn editing
Starfield records carry a VMAD subrecord (compiled Papyrus script
attachments). Its String/ArrayString properties occasionally hold display
text worth translating, but most are script identifiers, event names,
keyword EditorIDs or resource paths that break the mod if edited. This
mirrors xTranslator: parse VMAD, classify every embedded string, lock the
risky ones, and only let translators edit values that look like real text.
- bethesda_strings/vmad_handler.py: pure-Python parser (never raises),
risk classifier, and byte-splice editor that rewrites only edited value
spans so unmodelled VMAD sub-structures survive byte-for-byte. - esp_handler.py: scan_vmad / apply_vmad_translations — GRUP-aware walk,
decompress + recompress, XXXX oversize handling, record/GRUP size
recompute, .bak backup. Works on localized and non-localized plugins. - gui/vmad_dialog.py: risk-coloured table, locked rows read-only unless an
advanced override is ticked; deliberately separate from the main table to
prevent accidental bulk-AI-translation of risky identifiers. - Wired into the Translation menu via _open_vmad_dialog.
- tests/test_vmad_handler.py: 34 tests (parse/classify/splice + ESP-level
scan/apply round-trips, compressed records, records inside GRUPs).
Full suite: 324 passed.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(249a4bd)
- Add ESP/ESM Mod Update Migration tool (xTranslator-style)
When a mod updates (MyMod v1.0 -> v1.2), diff the two plugin versions and carry
existing translations forward — only added/changed strings need new work.
- bethesda_strings/esp_diff.py: pure-Python diff/migration, the plugin-file
counterpart of version_diff.py. Keys strings on
(form_id, record_sig, field_sig, occurrence) — stable across mod releases.
compute_esp_diff() classifies ADDED/REMOVED/MODIFIED/UNCHANGED and attaches the
prior translation (read from a previously-translated plugin's .original).
build_migration_items()/index_by_key() drive the carry-forward. CSV + HTML
reports reuse version_diff's DiffStatus + CSS. - gui/esp_migrate_dialog.py: setup (old/new/prior plugins) + 7-column diff dialog
with filter, changed-only, migrate, CSV/HTML export. - main_window: Translation menu -> 'Mod Update Migration (ESP/ESM)...';
_apply_esp_migration matches items to the open plugin's rows by composite key
and fills only pending/empty rows (never clobbers in-progress work). - tests/test_esp_diff.py (9 tests, synthetic EspStringEntry). Full suite 355 pass.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(783b032)
- Add force-stop: restart Ollama on Stop to free a wedged GPU instantly
Soft Stop only closed client sockets; a wedged ROCm GPU keeps generating
in-flight requests server-side, so Stop felt frozen for many seconds and the
model stayed resident. New optional force-stop runs a user-configured command
(e.g. sv restart ollama, sudo sv restart ollama, systemctl restart ollama,
pkill -x ollama) on Stop to restart/kill the server and free the GPU.
- gui/ollama_control.py: pure helpers detect_restart_command()/restart_ollama()/
build_restart_argv(); stdin closed + hard timeout + process-group kill so a
sudo prompt with no NOPASSWD rule fails fast instead of hanging. - AppSettings.ollama_restart_command (CONFIG_VERSION 34, migration + QSettings
mirror); empty = soft stop only (unchanged behaviour). - Settings dialog: "Force-stop command" field + Auto-detect button.
- MainWindow._stop_translation escalates to _force_restart_ollama(), running the
command via non-blocking QProcess with a 20s watchdog; reports result/sudo hint
in the status bar. No-op on the Claude backend. - tests/test_ollama_control.py: 12 tests (detect order, argv wrap, success/
nonzero/timeout/sudo-hint).
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(670d212)
- Add app-themed Qt sudo password dialog for force-stop elevation
The 'Requires root' force-stop path relied on external, unthemed dialogs
(ssh-askpass for sudo -A, or pkexec's polkit prompt), which ignore the app
theme and may be absent on a minimal tiling-WM setup. Add an in-app QDialog
(SudoPasswordDialog) that inherits the application QSS and feeds the password
to sudo -S via stdin.
- gui/sudo_dialog.py: themed password prompt (lock heading, command shown,
hidden echo + reveal toggle, Enter-to-submit); clears/drops the password on
close; never logged. - gui/ollama_control.py: sudo_available() + build_sudo_stdin_argv() pure
helpers (sudo -S -p '', strips any leading sudo the user typed). - gui/main_window._force_restart_ollama: when elevation is on and sudo is
present, prompt with the themed dialog and write the password to the
QProcess stdin; fall back to prepare_restart (sudo -A askpass / pkexec)
otherwise. Friendlier failure hints (incorrect password / auth failed). - gui/settings_dialog.py: relabel the checkbox/tooltip to reflect the app's
own themed dialog. - tests: +5 for sudo_available / build_sudo_stdin_argv (402 passing).
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(3179a94)
- Add fetch_dictionaries.py to bundle Hunspell dicts for Windows/macOS
gui/spell_checker.py now searches an app-bundled dicts/ directory first,
but nothing populated it — so Windows/macOS builds (which lack system
Hunspell dictionaries) had no spell-check.
Add scripts/fetch_dictionaries.py: downloads .aff/.dic pairs from the
LibreOffice dictionaries project into dicts/, renamed to the names
spell_checker expects (uk_UA, de_DE, …), and pulls each language's
upstream licence/readme alongside so the bundle stays compliant. It
sanity-checks that each download is real Hunspell data.
Default set is the UI locales minus Korean (uk_UA de_DE es_ES fr_FR
pl_PL cs_CZ, ~25 MB); pass names or --all for more. dicts/ is gitignored
so the binaries never enter history — the packaging step runs the script
and the installer ships the result.
Verified: all 6 resolve via spell_checker._find_dict_files and the files
are valid Hunspell (.aff "SET UTF-8", .dic word-count header).
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(e1b59d0)
Changed
- Update README: add all features through v0.14 (audio preview, visual context, dialogue tree, lore RAG, font checker, character personas, NexusMods browser, Weblate sync, Zen mode, detached panes, review tools, project structure)
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(b41124f)
- Update dialogue tree visualizer with pixel-exact SWF colors; refine Modelfile square-bracket rules
Dialogue visualizer: Replace hand-calibrated colors with values pixel-measured from the actual
game SWF files (dialoguemenu.swf + hudmessagesmenu.swf) via JPEXS decompiler.
- _C_PLAYER_BG: QColor(5,10,22,196) → QColor(0,0,0,153) (SWF frame 1 Normal_Unselected)
- _C_NPC_BG: QColor(0,0,0,218) → QColor(0,0,0,153) (TextBackground sprite — same 60% black)
- Selected state: solid grey QColor(183,183,183,255) replacing cyan border (SWF frame 10)
- Indicator bar: 4×30px teal #8ecfd3 at left edge when selected (SWF shape 19)
- Speaker label: #E8E8AC warm yellow (Subtitles.as SPEAKER_NAME_COLOR_STR)
- NPC text: #D3D3D3 light grey (Subtitles.as TEXT_COLOR_STR)
- Dark text on grey (selected) / white text on dark (normal)
- Remove noise-tile overlay and dead _bg_tile / _DATA_DIR code
Modelfile: tighten square-bracket token rules — distinguish input-binding/NPC-name tokens
(preserve unchanged), dialogue-choice tokens (keep brackets, translate content), and
ALL-CAPS status codes (translate to Ukrainian equivalents).
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(a9f8318)
- Update CLAUDE.md: remove Weblate, bump CONFIG_VERSION to 28
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(03411fe)
- Update NexusMods mod page description
Adds sections and bullets for features added since the last update:
- NexusMods Integration section (browser, free-user downloads, .7z/.rar
extraction, TM import, upload) - GPU monitor widget (status bar, AMD + NVIDIA, color-coded)
- Ukrainian gender agreement checker (Ctrl+Alt+G)
- ти/ви register consistency checker (Ctrl+Alt+R)
- Named sessions (Ctrl+Shift+N / Ctrl+Shift+S)
- Macro recorder (Ctrl+M)
- Focus mode, Dialogue tree viewer, Lore RAG
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(617a2cb)
-
Update README with current feature set
-
Languages: add Korean (ko/ko_KR), count 12
-
Models: add translategemma3-st-2, mamaylm, gemma4-opus48-st with descriptions
-
Translation: add AI-fix mode, pipeline post-processing section, curl-cffi optional dep
-
File support: add Starfield interface TXT, 7z/rar archives, free-user NexusMods downloads
-
QC: add RU→UK false-positive reduction, Auto-Fix All, per-code hide filter,
unclosed guillemets check, SUSPICIOUSLY_LONG auto-fix -
UI: update themes to 16 (named), add GPU monitor widget, add Korean UI locale
-
Project structure: add txt_handler.py, gpu_monitor.py, ko_word_checker.py,
character_profiles.py, font_checker.py, lore_db.py, dialogue_tree.py,
nexusmods_client.py, theme_manager.py, aur/ directory -
Remove Weblate (removed from codebase)
-
Optional deps: add curl-cffi
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(00c3141)
- Update Ukrainian UI translation with new strings
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(de6129d)
-
Update NexusMods header image with accurate feature info
-
Changed 'Ollama AI' chip to 'Ollama + Claude' (app now supports both backends)
-
Changed 'ESP/ESM' to 'ESP/ESM/ESL' (.esl files are also supported)
-
Regenerated from scratch; all language badges and overall style preserved
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(90daf6b)
- Update NexusMods header banner to new space station art
New cinematic deep-space orbital station command deck image:
holographic EN/UK translation panels + QC dashboard visible,
amber-teal nebula and Saturn gas giant in background.
Cropped to 1300x300 banner format from the generated artwork.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(e18735e)
- Update localization (.ts) files with lupdate — add self-review + drifted strings
Ran pyside6-lupdate over gui/*.py to bring all 7 locale catalogs back in sync
with the source (-locations absolute, matching the existing format):
uk_UA, de_DE +192 messages
cs_CZ, es_ES, fr_FR, pl_PL +183 messages
ko_KR +91 messages (was lupdated more recently)
New strings include the automatic self-review feature and its settings toggle
("Self-Review Complete", "Automatic self-review after translation", the pass
status lines, etc.) plus pre-existing source-string drift that had accumulated
since the last lupdate run. New entries are marked translation type="unfinished"
for translators to fill in.
Verified via XML parse that every existing non-empty translation is preserved
byte-for-byte (no translation text lost; 5 source strings that no longer exist
were kept as type="vanished" with their text intact). All catalogs are
well-formed XML and compile cleanly with lrelease. Location line numbers were
refreshed to current source positions.
de_DE.ts additionally includes in-progress German translations that were already
present (uncommitted) in the working tree before this change; lupdate preserved
and extended them.
.qm binaries are gitignored build artifacts (regenerated via
scripts/compile_translations.sh) and are not committed.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(14f55f4)
-
Update TRANSLATING.md to match current locale state
-
Add the Korean (ko_KR) locale, which was missing from the status table.
-
de/es/fr/pl/cs are now fully translated (machine-assisted) — mark them
"Translated · review welcome" instead of "Needs translation", matching
the .ts files (0-1 unfinished) and settings_dialog's complete=False. -
Fix the File structure section: .qm binaries are gitignored
(.gitignore:233), not committed — the old text contradicted itself. -
Refresh the manual-workflow commit example accordingly.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(c280d43)
- Update README with VMAD, ESP migration, voice/speaker, sessions, macros, changelog & cross-platform features
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(0a26312)
- Update NexusMods description: ESP migration, VMAD, voice/speaker, force-stop, notifications, What's New, auto-update
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(595112d)
- Update UI translations for all 7 locales with new feature strings
Regenerate .ts files via pyside6-lupdate (1585 messages each):
- Add ~145 new UI strings: VMAD script-property analysis, ESP mod-update
migration, native Starfield voice playback + speaker panel, translation
sessions, macro recorder, Ollama force-stop, "What's New" changelog,
auto-update check, cross-platform notifications. - Translate all new strings into uk/de/es/fr/pl/cs/ko and mark finished.
- Drop 54 stale obsolete entries (-no-obsolete).
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(03f168f)
Fixed
-
Fix ruff F401/F821 and four pyright errors from CI
-
tts_engine.py: remove unused
structimport (F401) -
tts_engine.py: fix
bool | Literal['']return type in
piper_available() — uselen(binary) > 0instead of truthybinary
so both operands ofandare unambiguouslybool(reportReturnType) -
main_window.py: suppress reportUndefinedVariable on
_detached_tableannotation — class is imported lazily inside the
method that uses it, consistent with other gui.* lazy imports here
(F821 / reportUndefinedVariable) -
main_window.py: guard profile_manager.get() result before .name
access — get() can return None when accepted_profile_id is falsy
(reportOptionalMemberAccess) -
ollama_worker.py: suppress reportGeneralTypeIssues on
_translate_single — function exceeds Pyright's complexity budget;
logic is correct, refactor deferred (reportGeneralTypeIssues)
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(9579d12)
- Fix QMainWindow::saveState() warning: set objectName on main toolbar
QMainWindow::saveState() requires every QToolBar and QDockWidget to
have a unique objectName set; without it the state cannot be saved and
Qt logs a warning. Add setObjectName("MainToolBar") immediately after
the toolbar is constructed.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(19c900d)
- Fix ruff F821 on DetachedTableWindow forward reference
The class is imported lazily inside the method that uses it; suppress
the undefined-name lint error with noqa: F821 alongside the existing
pyright ignore comment.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(6b8d0dc)
-
Fix three translation bugs: size-tag spacer leak, format-prefix loss, inline format-spec drop
-
_restore_size_spacers: detect <size=N%> spacer in original that
the model restructured into a full wrapper <size=N%>TEXT; restore
to original spacer-before-content form (e.g. Bug 1 example) -
_restore_missing_format_specs: re-insert printf specs (%.0f / %d / %s …)
that the model dropped from mid-sentence positions; uses proportional
forward-word-boundary insertion ordered right-to-left (e.g. "totaled
%.0f credits" → restored before "кредитів") -
_clean_translation: restore leading "%.0f - " format-prefix and "-Header"
dash that the model strips from single-line ledger/list entries (e.g.
"%.0f - Cost1" → "%.0f - Вартість1", "-Costs" → "-Вартість")
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(caa14d3)
- Fix 2-line paragraph format spec corruption
Lower line-by-line translation threshold from >=3 to >=2 non-empty lines
so paragraphs like "%.0f - Stipend\n%.0f - Bonus" are no longer sent as a
single model call, which was causing both specs to land on line 1.
Also make _restore_missing_format_specs process line pairs independently
(via an inner _fix_single helper) so format specs can never be inserted
across a newline boundary even when a multi-line string falls through to
the single-call path.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(39020e9)
- Fix _fix_newlines inserting newline before sentence-ending punctuation
The snap-forward loop stopped AT "." (a stop character), placing the
restored newline before the period instead of after it — producing
"<Alias=PlayerShip>\n. Оператор" rather than "<Alias=PlayerShip>.\nОператор".
Two fixes:
- After snapping onto ".!?", advance past the punctuation (and any
following closing quotes/brackets) so the insertion point is after
the full stop. - When applying the insertion, absorb any inter-sentence space at the
insertion point (model writes ". Next" when joining two lines; we want
".\nNext" not ".\n Next").
Insertions tuple extended to (pos, token, skip) to carry the space count.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(3adece2)
-
Fix book string translation: raise line-by-line threshold to 400, clean \н artifact, fix 2-char mixed-script words
-
Raise max-line-length threshold from 200→400 so Fueling (~283 chars) and
Maintenance (~245 chars) paragraphs enter the line-by-line path instead of
falling through to a single model call that garbles their structure -
Add \н→\n replacement in _clean_translation: model occasionally writes
backslash + Cyrillic н (U+043D) instead of the \n escape token due to
script confusion bleeding into the escape sequence -
Relax _fix_mixed_script guard from cyr>=2 and cyr>lat to cyr>=1 and cyr>=lat
so 2-char mixed words like будь-dе (cyr=1, lat=1) are also corrected
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(0bfc4e2)
- Fix ruff linting errors in gender checker and session dialog
Remove 8 duplicate dict key literals (F601) from NOUN_GENDER in
gender_checker.py; split two semicolon-joined statements (E702) in
gender_dialog.py and session_dialog.py into separate lines.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(6be7411)
- Fix AttributeError when config has null source/target lang
AppSettings.from_dict was passing None values from the JSON file into
the dataclass constructor, overriding declared defaults. Filter them out
so any null field falls back to its dataclass default.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(ed88e20)
- Fix .7z (and .rar) archive extraction in NexusMods browser
_extract_plugins and _extract_strings only handled .zip, so any mod
delivered as a .7z silently returned no files. Added unified
_extract_from_archive() that covers .zip (stdlib zipfile), .7z (py7zr
library or 7z CLI fallback), and .rar (rarfile library or 7z CLI
fallback). Also adds py7zr>=0.20 as an optional requirement.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(5ba9a4e)
- Fix ruff E701 errors in gpu_monitor.py
Expand single-line if/return statements to two lines.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(d15a049)
- Fix AttributeError: QTableWidget has no currentRowChanged signal
currentRowChanged lives on the selection model, not QTableWidget itself.
Connect via selectionModel().currentRowChanged and adapt the QModelIndex
argument to the row integer _on_row_changed expects.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(a541817)
- Fix ko_KR.ts XML escaping so lrelease compiles successfully
Raw HTML tags (, ,
), Korean text in angle brackets,
regex lookbehind syntax, and Qt menu accelerator & chars were
all unescaped inside elements, making the file
invalid XML. Escape everything to <, >, & entities.
All 1356 Korean strings now compile with 0 unfinished.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(934b8f6)
- Fix AI model guillemet and Cyrillic brand-name errors at two layers
Prompt layer (OllamaWorker):
- Extend _TARGET_STYLE["uk"] to explicitly require closing every «
with », and to keep Cyrillic brand names (ТерраБрю, Чанкс, …)
in Cyrillic — never switch to the English Latin-script form. - Add two concrete ru→uk examples showing correct guillemet pairs
and Cyrillic brand names to _LANG_EXAMPLES[("ru","uk")].
Post-processing layer (both workers):
- Add _restore_unclosed_guillemets() to OllamaWorker — called after
_restore_dropped_tags and _restore_size_spacers at both translate
call sites. - Add _close_unclosed_guillemets() module-level helper to
claude_translation_worker.py — called after term restore.
Inserts missing » before trailing sentence punctuation or at
end-of-line, matching the QC auto-fix logic.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(5e0ff56)
- Fix dropped opening [ bracket at QC and post-processing layers
Adds UNMATCHED_BRACKET: detects when the translation has more ] than [
(model translated the content but dropped the opening [). Auto-fix
prepends the missing [ at the start of each affected line, using the
original line's indentation as a reference.
Wired in three places:
- quality_checker.py: _check_unmatched_brackets() + _fix_unmatched_brackets()
added to AUTOFIX_CODES, check_one, and auto_fix pipeline - ollama_worker.py: _restore_dropped_opening_brackets() called after
every translation at both call sites - claude_translation_worker.py: same helper called after term restore
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(1ee37a6)
- Fix three QC false positives found in quality_report_20260613_165508
MISSING_NUMBER: update _STANDALONE_NUM_RE to exclude pure-zero groups like
"000" that arise from thousands-separator spacing in "10 000" — only "10"
is now flagged as missing, not the spurious "000" fragment.
REPETITIVE_CONTENT: pass original text to _check_repetition so structured
list strings (mine assignments, status grids) that are already repetitive
in the source are skipped — only novel AI-hallucinated repetition is flagged.
SOURCE_LANGUAGE_LEAK: expand _UK_EXCLUSION in ru_word_checker.py with common
Ukrainian words that lack Ukrainian-specific chars (і/ї/є/ґ) and are also
present in the Russian dictionary — fixes the false positive on
"Ви не можете спати, коли поруч перебувають вороги." which is valid Ukrainian.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(8b82ea9)
- Fix crash when hovering/editing TXT mode strings (row_id is str not int)
String ID tooltip and edit dialog both used :08X hex format on row_id,
which crashes with ValueError when mode='txt' because the key is a string
(e.g. '$iHaveGoods') not an integer.
- Tooltip: added elif mode=='txt' branch showing 'Key: {row_id}'
- Edit dialog title/label: check isinstance(row_id, int) before formatting
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(a9fe257)
- Fix QC regressions: tighten UNTRANSLATED threshold and length-skip condition
UNTRANSLATED: lower alpha-word threshold from 4 to 3 so that 3-word Cyrillic
phrases like 'Привіт як справи' are correctly flagged. 1-2-word strings that
are legitimately identical in Russian and Ukrainian (Лоб, Марк I) still pass.
LENGTH_INCREASE/SUSPICIOUSLY_LONG: only skip ratio checks for very short
Cyrillic sources (≤ 8 chars), not for Latin short words like 'Hi'. 'Hi' with a
100-char translation is still flagged as SUSPICIOUSLY_LONG; 'КНР' expanding to
'Китайська Народна Республіка' is still suppressed.
Fixes three test failures: test_untranslated_identical, test_suspiciously_long,
test_check_all_returns_only_issues.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(2332dcb)
- Fix mamaylm timeouts: cap num_predict, reduce num_ctx, add max_concurrent + timeout
mamaylm (Gemma 3 12B) was using the same num_predict=4096 as the 27B model
and had no timeout or concurrency override, causing every request to race
against the 300s session default while 10 workers queued simultaneously.
Changes to MODEL_CONFIGS["mamaylm"]:
- num_predict: 4096 → 512 (game strings are short; matches translategemma3-st-2)
- num_ctx: 16384 → 8192 (halved to reduce KV-cache pressure)
- max_concurrent: 3 (serialised GPU inference; >3 just adds queue depth)
- timeout: 480 (gives 480s wall-clock instead of 300s session default)
Also fixes the timeout error message to show the actual per-model timeout
value instead of always printing the 300s session default.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(134ded5)
-
Fix ruff F401 unused imports and pyright type errors in update/settings/main
-
Remove QDialogButtonBox, QScrollArea, QWidget unused imports from update_dialog.py
-
Use //= instead of /= in _human_size to keep n typed as int throughout
-
Refactor "Check Now" handler in settings_dialog to avoid pyright Optional member access
-
Guard QApplication.instance() against None in _current_version
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(aa11ce1)
- Fix IndexError in focus overlay when current row is out of data bounds
Clamp start = min(_current_row + 1, n) so the wrap-around loop
range(0, start) never exceeds len(data). Triggered when model data
shrinks while the overlay is open (e.g. filter change during a batch).
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(711bc99)
- Fix Ollama translation stop: switch to streaming for immediate cancellation
Previously stream=False caused session.post() to block the worker thread
for the entire generation duration (up to 600s). stop() could not interrupt
these calls — it only cancelled queued futures, not running HTTP requests.
Fix:
- Add _stream_ollama() helper: POSTs with stream=True and reads tokens via
iter_lines(), checking _stop_flag between each token. Registers the live
response in _active_responses[thread_id] so stop() can close it. - Add _active_responses dict + _responses_lock to init.
- stop() now closes all active responses after cancelling pending futures.
Closing the socket causes iter_lines() to raise OSError immediately,
unblocking the worker thread in milliseconds instead of waiting for the
full generation to complete. - translate_batch exception handler suppresses connection-abort error signals
that are side-effects of stop() rather than genuine translation failures. - Remove stream=False from all payload dicts (now set in _stream_ollama).
- Remove stale unused url= local variables in _translate_single and
_call_ollama_chunk.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(6702a95)
- Fix .txt chip width to match other file-format chips in header
All four extension chips (.strings/.dlstrings/.ilstrings/.txt) now
share the same minimum width (sized to the widest one), with text
centered. Tool chips (ESP/ESM/ESL, BA2, etc.) keep natural sizing.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(e467e54)
- Fix chip text legibility: solid fill + cyan-blue text
Replaced frosted-glass (near-transparent) chip background with a
solid dark navy fill and a blue border, and changed the text color
from white to cyan-blue so it reads clearly against the dark fill.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(0729706)
- Fix IOT crash on Ctrl+C during close: catch BaseException in shutdown path
KeyboardInterrupt is a BaseException, not Exception, so it was escaping
executor.shutdown(wait=True) and the closeEvent handler, leaving
ollama_thread without terminate() and crashing with 'QThread destroyed
while still running'.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(0aa88a8)
- Fix KeyboardInterrupt crash in gpu_monitor polling loop
subprocess.run() and Path.read_text() propagate KeyboardInterrupt
(a BaseException) through except Exception handlers, printing a
traceback on every Ctrl+C. Widen both catch clauses to BaseException.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(09b8732)
- Fix spurious trailing newline appended to translations
Two-pronged fix in OllamaWorker:
- _restore_missing_newlines: skip insertion when mapped position falls
at or past end of translation — trailing \n in originals is a Bethesda
file format artifact, not semantic content. - _translate_single / _translate_chunked: belt-and-suspenders strip of
any trailing \n / \n the model appended when the original has none.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(87153b7)
- Fix single-line English prose in [] not being translated
The bracket_id regex in TermProtector matched any bracket span with no
Cyrillic, including multi-word sentences like [The data slate contains
information on a remote mission]. This caused the entire string to be
protected as a game-code token, triggering the "all content protected"
early exit so the AI was never called.
Fix: add \s to the bracket_id exclusion set so only space-free codes
([MALE], [PLYR], [TK:000001]) are protected. Prose brackets with spaces
pass through to the AI unchanged.
Also strengthen the post-translation safety net: remove the single-line
exclusion and widen scope from EN→UK to all EN→* pairs, so any bracket
prose the model still outputs in English gets sub-translated.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(ac84607)
- Fix <Token.Name=X> strings not translating when all protected-text retries return empty
Fine-tuned models (translategemma3-st) can emit immediately when the
prompt starts with [[TK_…]] placeholder tokens, because their training data
only contained raw Bethesda strings without TermProtector tokens. When all
three standard retries return empty, add a fallback that retries with the
original unprotected text (containing familiar <Token.Name=…> / <Token=…>
Bethesda tags). _restore_dropped_tags handles any tags the model drops.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(640eef8)
- Fix printf_var pattern incorrectly protecting "% for" as a format specifier
The printf_var regex included space in its allowed flags ([-+0 #]*), causing
"% for" in strings like "50% for 20 seconds" to match as a space-flag %f
format specifier. The three matched chars "% f" were replaced with [[TK_…]],
leaving the model with "50[[TK_…]]or 20 seconds" — it then re-added "%" to
recover the number, and the restored token gave "50% % f" in the output.
Bethesda game strings never use the printf space flag, so removing the space
from the character class is safe and prevents this class of garbled output.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(031742b)
- Fix _restore_missing_format_specs re-inserting "% f" artifact after _clean_translation removed it
The _FS_RE pattern in _restore_missing_format_specs included a space in its
flag character class ([-+0 #]*), causing "% f" in strings like "15% for 20
seconds" to be matched as a valid printf format specifier. _clean_translation
runs first and removes the "% % f" artifact, but _restore_missing_format_specs
then sees one format spec in the original and zero in the (now-clean)
translation and re-inserts "% f" — undoing the cleanup.
Removing the space from the flag class mirrors the same fix already applied
to TermProtector's printf_var pattern and permanently prevents false matches
on "% for"-type substrings.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(b946c06)
- Fix quality_checker false MISSING_TAG errors for "% for" / "% chance" patterns
The printf_var pattern in TAG_PATTERNS and _FORMAT_SPEC_RE both used
[-+ #0]* for flags, matching the space character. This caused substrings
like "% f" in "15% for" and "% c" in "25% chance" to be treated as
valid printf format specifiers. The QC then reported MISSING_TAG errors
because the (correct) Ukrainian translation didn't contain "% f"/"% c".
Removing the space from the flag character class (now [-+#0]*) mirrors the
same fix already applied to TermProtector and _restore_missing_format_specs.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(55a2f5d)
- Fix hallucinated precision format specifiers (%.2f) not removed from translations
_clean_translation's _FMT_RE only matched bare specifiers like %s/%d/%f,
missing precision forms like %.0f and %.2f entirely. When the model
hallucinated %.2f into a string whose original only had %.0f twice, the
count check saw 0 vs 0 and skipped cleanup.
Two changes:
- Expand _FMT_RE to match full printf specs including flags and precision
(%[-+#0]*(?:*|\d+)?(?:.\d+)?[sdfioxXeEfFgGcpn%]), no space flag. - Switch from tail-removal to per-spec multiset matching: each translation
spec is consumed from the original multiset; any leftover spec (wrong
precision or surplus copy) is removed regardless of position.
This correctly strips %.2f from "%.0f їжі та %.2f одиниць %.0f напоїв"
leaving the two legitimate %.0f specifiers intact.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(129de3b)
- Fix model substituting wrong format spec types (%s/%d instead of %.0f)
When the model outputs the correct number of format specifiers but with
the wrong types (e.g. %s and %d instead of %.0f and %.0f), neither the
extra-spec removal nor _restore_missing_format_specs caught it — both only
check counts, not types.
Add step 2 to the _clean_translation format-spec block: after extra-spec
removal, if counts match but the spec list differs, replace each wrong-type
spec in-order with the corresponding original spec. Positional order is
preserved because the game engine fills specs sequentially regardless of
word order in the translation.
Fixes MISSING_TAG %.0f / EXTRA_TAG %s / EXTRA_TAG %d errors on strings
like "Cargo hold has %.0f food, and %.0f drinks remaining".
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(6c3ae20)
- Fix mamaylm timeouts: cap concurrency to 4 and raise timeout to 720s
With max_concurrent unset, 10 parallel requests queued at Ollama's single
GPU serialiser — queue wait time alone (~9×50s) could exceed the 480s
timeout before generation even started. Cap at 4 workers (same as the
other 12B model) to keep queue depth shallow, and raise the base timeout
to 720s to handle both queue wait and generation on slower hardware.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(cfae0a5)
- Fix shutdown hang: drain executor threads before QThread.terminate()
stop() uses wait=False so in-flight HTTP threads outlive the QThread
when terminate() fires. Python's atexit then blocks joining those
orphaned threads, causing the KeyboardInterrupt traceback on exit.
Fix: call close() (wait=True) immediately after stop() so all executor
threads are fully drained before we touch the QThread. Also raise the
fallback terminate() timeout from 2 s → 5 s since the QThread should
exit quickly once the executor is drained.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(82f4b3a)
- Fix empty translations for same-word RU→UK pairs and [tag]path/ no-trans detection
Two bugs diagnosed from quality_report_20260615_141656 (3822 EMPTY_TRANSLATION errors
when translating starfield_ru.strings.xml with mamaylm):
-
_clean_translation echo-strip consumed the entire output when Russian and Ukrainian
share the same spelling (e.g. Брови, Люди, Кнопка, Хамелеон, Неон — ~3662 of the
3822 empty strings were single-word/short Cyrillic matches). The guard now preserves
temp_text when the strip would leave nothing behind. -
_INPUT_NOTRANS_RE missed strings like "[Default Ship]Crew/HomeShip.txt" because the
path-detection branch required \w at position 0; strings starting with "[tag]" fell
through and caused the model to output garbage (NEWLINE_COUNT_MISMATCH).
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(1018126)
- Fix EXTRA_TAG, MISSING_TAG, MISSING_NEWLINES, NEWLINE_COUNT_MISMATCH post-translation issues
Diagnosed from quality_report_20260615_141656:
NEWLINE_COUNT_MISMATCH (99 of 112 cases) / MISSING_NEWLINES:
Source strings ending with \r\n had their trailing newline silently dropped by
the model; the post-translation strip-guard only removed extras but never
restored the missing one. Added symmetric restore in both _translate_single and
_translate_chunked: when original ends with \n or \n and translation doesn't,
append the matching token.
MISSING_TAG / NEWLINE_COUNT_MISMATCH (file paths without extension):
INPUT_NOTRANS_RE caught Data\Interface\x.dds (has .ext) but not
Architecture\City\neon\ (no extension, ≥3 backslash-separated ASCII components).
Added pattern ^[A-Za-z0-9.]+(?:\[A-Za-z0-9_.]+){2,}\?$ to skip these.
EXTRA_TAG — stray (mamaylm hallucination):
Model appended to strings that had no <Alias= opener in the source
(e.g. "Навтілус Бастіон S80"). Existing _clean_translation code only
removed hallucinated opening <Alias=...> tags; extended it to also strip stray
closers when the original has no Alias openers at all.
EXTRA_TAG — extra opening tags:
Model injected openers not present in the source. Extended the existing
excess-removal logic to also remove surplus <font ...> opening tags.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(8b0599d)
- Fix mamaylm timeout on large strings: scale per-request timeout by length
The single-call (non-chunked) translation path only scaled the Ollama
read timeout with text length for thinking models (think_disabled). For
non-thinking models like mamaylm the timeout was a flat 720s regardless
of string size, so a big book/document string from an XML import — below
the 12000-char chunk threshold, hence sent in one call — reproducibly hit
the 720s read timeout on slower GPUs once queue wait behind the other
concurrent workers + time-to-first-token + generation exceeded it.
Apply the same + len // 20 length scaling to every model (the chunked
path already did this). Also scale the rewrite/self-review path by length,
and log the actual scaled timeout + char count with a tuning hint instead
of recomputing the flat base.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(d7e75ff)
- Fix mamaylm timeouts: scale streaming read timeout by GPU queue depth
In streaming mode requests raises ReadTimeout when no token arrives
within the timeout window, so the per-request timeout actually bounds
time-to-first-token, not generation time. Ollama serialises GPU work, so
when effective_workers requests are posted at once the trailing ones sit
behind the others' full generations before their first token streams
back. The batch sorts long strings to the end, clustering the slow
dialogue strings together — so the tail reproducibly hit the flat
per-model timeout before it ever started generating (e.g. mamaylm's 749s
on a 580-char string with 4 requests in flight).
Add concurrency-scaled queue-wait headroom: budget for up to 3 queued
generations ahead of this request, using a per-generation estimate that
is independent of the model's flat base so high-concurrency models don't
get an absurd timeout. The headroom auto-tracks effective_workers, so
lowering max workers shrinks both the real queue and the timeout. A
promptly served request still returns immediately; the larger budget
only matters when the GPU queue is genuinely deep.
For the failing case the per-request timeout grows 748s -> ~1504s (4
workers), comfortably covering the queue, while serial (1-worker) use is
unchanged.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(e46a82c)
- Fix mamaylm batch timeouts: cover GPU queue depth + add stall watchdog
The previous fix scaled the streaming read timeout additively for queue
wait, but it still underbudgeted slow models: mamaylm reproducibly timed
out at exactly 1706s on a 768-char string with 4 requests in flight
(720 base + 768//20 length + 3*(60 + 768//3) queue = 1706), before its
first token ever streamed back. Single-string translation worked because
there was no queue.
In streaming mode requests raises ReadTimeout when no token arrives
within the read timeout, so that timeout bounds time-to-first-token, not
generation. Ollama serialises GPU work, so a string posted N-deep in the
batch waits behind N full generations before its first token. Two issues
followed: the additive queue estimate was too small, and the same flat
number also had to serve as the inter-token bound, so it could not be
raised safely (a frozen generation would burn the whole budget).
Fix both:
-
Size the first-token budget to actually cover the queue. At the point
of computation model_timeout is the budget for this string's own
generation (base + length); multiply by queue depth (1 + queue_slots)
so a string behind N comparable generations waits them out. This
self-calibrates with the model's base and the string length where the
old additive guess did not. For the failing case the budget grows
1706s -> 3032s (4 workers); serial (1-worker) use is unchanged. -
Add an inter-token stall watchdog in _stream_ollama. Once generation
starts, a token must arrive within _STREAM_STALL_TIMEOUT (180s) or the
socket is closed and the string skipped as a ReadTimeout. This makes
the larger first-token budget safe: steady-but-slow generation streams
within the gap and completes; only a genuine freeze aborts, in seconds
instead of burning the full budget. stop() semantics are preserved (a
socket closed by stop() still returns None, not a timeout).
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(4732acd)
- Fix mamaylm batch wedge: detect a stuck backend, stop inflating timeouts
The reported "timed out after 2996s (576 chars)" came from requests
raising ReadTimeout because no byte arrived for the whole budget — the
Ollama backend wedged under sustained parallel load (the run translated
3000+ strings fine, then the final concurrent requests produced zero output
for ~50 min). Single-translate works because there is no concurrent load to
wedge it. A bigger first-token budget cannot translate through a wedged GPU,
so the previous additive (e46a82c) and multiplicative (4732acd) timeout
growth only delayed the failure.
- Revert the queue-depth timeout multiplier to a bounded additive
(+90s/slot). A wedge now surfaces in minutes, not ~50 minutes, while still
covering real first-token queue waits (well under a minute at 4 workers). - Add a wedged-backend circuit breaker: N consecutive read timeouts with no
success between them aborts the batch (closes in-flight sockets) and emits
an actionable error — restart Ollama, lower Max workers — instead of
burning every remaining string's full budget. Threshold = effective
workers + 1; the counter resets on any successful translation, so sporadic
timeouts never trip it. - Extract stop()'s abort logic into shared _abort_inflight(); feed both the
main and chunked timeout paths into the breaker via _note_timeout().
Tests: tests/test_ollama_wedge_breaker.py (5 new). Full suite 183 passed.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(55d3d15)
- Fix pyright CI: install pytest so test imports resolve
The pyright lint job installed requirements.txt + pyright but not pytest,
so tests/test_ollama_stall_watchdog.py's 'import pytest' failed with
reportMissingImports. The test job already installs pytest; mirror that
in the lint job.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(4c4374c)
- Fix mamaylm NEWLINE_COUNT_MISMATCH: CRLF trailing runs + retry-hint leak
Two deterministic fixes for NEWLINE_COUNT_MISMATCH flagged in a RU→UK
mamaylm batch (Russian source XML is CRLF; mamaylm emits LF-only output).
-
_match_trailing_newlines under-counted CRLF trailing runs. The source
ends "\r\n\r\n" (2 breaks) but the naive \n+$ capture stops at the \r
between the breaks and matched only the final "\n", so the healed
translation kept 1 trailing newline against the source's 2. Now the run
is captured as [\r\n]+$ and the break count (CRLF/CR/LF each = one) is
re-emitted as plain \n to match the model's LF body. Verified against all
12 trailing-CRLF cases in the report. -
Retry-hint feedback leak: the English QC hint was placed in the user turn
after the "To {tgt}:" anchor, so the translation-tuned model translated it
into the output (e.g. "Переклад зворотного зв'язку — попередня спроба…").
The hint is already appended to the system prompt, which is the correct
channel, so the user-turn copy is removed.
Adds CRLF trailing-newline and retry-hint-leak guard tests.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(02562e8)
- Fix CI apt 404: refresh package index before installing system deps
The Test workflow's bare apt-get install libegl1 libhunspell-dev failed
with a 404 — the runner's pre-baked apt index pins a libegl-mesa0 point
release that was superseded in the Ubuntu archive, so the cached URL no
longer exists. Run apt-get update first so the index resolves the current
version.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(4124751)
- Fix mamaylm reload: honour pin_num_ctx in the rewrite path too
The "mamaylm keeps restarting mid-batch" symptom survived the 06-18
pin_num_ctx package because _call_ollama_rewrite never honoured the flag.
mamaylm-v2.0 leaks Russian on ~half the strings, so the rewrite call
fires constantly; it kept its adaptive ladder and sent num_ctx=4096 for
short strings while the main translate/chunk paths send the model's 8192.
Ollama reloads on any load-time num_ctx delta, so the runner reloaded
(8192<->4096) on roughly every leakage cleanup.
Fix: _call_ollama_rewrite now pins to model_max_ctx like the other paths.
Also unify the pin branches in _translate_single and _call_ollama_chunk to
plain effective_num_ctx = model_max_ctx so the pinned value is
byte-identical across all three paths and not clamped by the user's
num_ctx slider — all three must agree exactly or the runner reloads.
Guard tests capture the actual payload num_ctx from each path (stub
_stream_ollama): pinned rewrite+chunk both send 8192 for short input,
are identical, ignore the slider (4096 and 32768 -> 8192), and an
unpinned model still takes the adaptive 4096 step.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(966fc82)
- Fix mamaylm newline-count mismatch on multi-paragraph notes
A 3-paragraph data slate (GergelEbanex) came back from mamaylm with the
first blank-line break collapsed and a spurious \n inserted mid-sentence,
drifting the QC newline count 6 -> 5 (NEWLINE_COUNT_MISMATCH). The
single-call restores re-insert missing newlines proportionally but
cannot undo a misplaced one, so the count stayed wrong; and they never
removed extra newlines the model invented.
Two targeted fixes in the worker:
-
Per-paragraph re-fill safety net. After all single-call restores, if a
blank-line-delimited string's final newline count still disagrees with
the source, retranslate paragraph-by-paragraph and rejoin with
app-controlled \n\n — structure then matches the source by
construction. Fires ONLY on a real residual mismatch and only for the
primary string, so the common case pays nothing and it can't recurse
(each sub-paragraph has no \n\n). This deliberately does NOT lower the
_PP_SPLIT_MIN_CHARS gate (which exists to avoid call-count explosion on
the big mamaylm batch) — it's reactive, not proactive. The existing
proactive split now shares the same _translate_paragraphs helper. -
Sentence-boundary snapping in _restore_line_structure. The proportional
rebuild that handles the frequent "dropped break" case now prefers a
space following sentence-ending punctuation over the nearest plain
space, so it stops cutting mid-phrase ("...ПОТОЧНОЇ\n\nАНІМАЦІЇ!").
Pure placement; never changes the count.
Tests (+6): _translate_paragraphs rejoin/None/structure, _nl_count matches
the QC formula, end-to-end repair of an extra-break drift, no fan-out on a
clean translation, and sentence-boundary snap selection. Full suite 366
passed.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(d73192e)
-
Fix more PyInstaller bundle gaps: log path, missing data files, theme dir
-
log_setup: FileHandler('translator.log') wrote to cwd, which for a frozen app
launched from a desktop entry is often '/' (read-only) — it raised at
main.py:27 before any GUI, so the app silently failed to start. _make_file_handler
now tries the requested path, then the per-user config dir, then the temp dir,
and falls back to console-only rather than crash. -
spec: bundled only 3 of 10 word lists (en/ru/uk) — de/fr/es/it/pl/pt/ko language
detection broke. Now globs data/_words.txt. Also bundles data/.png +
data/fonts/*.ttf for the visual-context preview (previously absent → broken). -
theme_manager: custom themes were saved under Path(file).parent.parent/themes
= the read-only _MEIPASS bundle dir when frozen; save_custom_theme failed. When
sys.frozen, custom_dir now resolves to the user config dir. -
tests/test_log_setup.py: 4 tests (requested path, fallback, all-fail → None,
console-only).
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(40fa809)
- Fix Windows force-stop: no console flash, 'not running' isn't a failure
Two Windows-specific problems in the non-root taskkill force-stop path (the
app is windowed on Windows, console=False):
- Console-window flash: a console child (cmd/taskkill) launched from a
windowed process pops its own console. Suppress it with CREATE_NO_WINDOW —
via QProcess.setCreateProcessArgumentsModifier in the GUI, and
subprocess creationflags in the CLI restart_ollama fallback. - Spurious "restart failed":
taskkill /IM ollama.exeexits 128 ("process
not found") when Ollama isn't running, so pressing Stop with no live server
showed an error. New is_already_stopped() treats that (and the Unix
pkillexit-1 "no match") as success — the GPU is already free. Real
failures (access denied, service-manager errors) still surface.
The finished handler / restart_ollama now stash & classify the command so a
benign "nothing was running" exit reads as success.
+9 tests (is_already_stopped, sudo helpers already covered) — 408 passing.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(47ef10c)
- Fix Windows tray icon + post-translation notifications
The tray icon and the "Translation complete" desktop notification worked on
Linux but were unreliable on Windows:
-
No AppUserModelID — Windows attributed the taskbar icon and tray balloon
to the host interpreter ("Python") instead of the app, and could suppress
the toast entirely. main.py now calls
SetCurrentProcessExplicitAppUserModelID early (win32 only, best-effort). -
send_notification was Linux-first — it probed notify-send/dbus-send (absent
on Windows) before the Qt tray balloon, and the Qt path never checked that
the tray was visible or that messages are supported, so it could silently
no-op. Now: Windows/macOS go straight to the Qt balloon (the native
Shell_NotifyIcon / NSUserNotification path); the Qt helper verifies
supportsMessages() and shows the tray if it wasn't visible yet.
+5 routing tests (test_desktop_notify.py) — 413 passing.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(72914f2)
- Fix white/unthemed "What's New" changelog panel on welcome screen
The QTextBrowser had a per-widget stylesheet forcing a near-transparent
background and no color. Since that overrode the theme's QTextEdit rule
(QTextBrowser is a QTextEdit subclass), the panel fell back to Qt's
default white document page instead of the active dark theme.
Drop the hardcoded background so the theme's background-color/color win
(it's the only declaration, so no cascade tie). Keep border/radius/padding.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(94a3aa2)
- Fix white/unthemed welcome card (QGraphicsEffect offscreen render)
The welcome card uses a translucent, stylesheet-themed background. Themes
are applied via QSS only (the Qt palette stays default white). A
QGraphicsOpacityEffect renders its target into an offscreen pixmap filled
with the palette colour, ignoring the QSS background — so once the
"What's New" changelog wrapped the welcome content in a QScrollArea, the
idle opacity pulse made the whole card render solid white, ignoring the
active dark theme.
Replace the opacity-effect pulse with a stylesheet-driven dashed-border
"breathing" pulse (no QGraphicsEffect, no offscreen step), so the card
keeps its theming/transparency. Updates quantised to 0.02 alpha steps to
avoid re-polishing the stylesheet on every animation tick. Other
QGraphicsOpacityEffect uses (progress flash, toasts, fade-ins) are on
opaque/transient widgets and are unaffected.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(85e0ed1)
- Fix white welcome screen: make scroll viewport + body transparent
Root cause of the white/unthemed welcome screen: when the "What's New"
changelog wrapped the welcome content in a QScrollArea, the scroll
viewport and the inner body QWidget aren't matched by the theme's
QScrollArea rule, so they painted with the default (white) palette base
(themes are QSS-only and never set the palette). The translucent welcome
card and its labels sat on that white widget, so the whole upper region
rendered white regardless of the active theme; only the opaque themed
changelog QTextBrowser stayed dark.
Apply the same transparency idiom the settings dialog already uses:
viewport().setAutoFillBackground(False), an objectName'd QScrollArea with
#X > QWidget > QWidget { background: transparent }, and a
WA_StyledBackground transparent body widget — so the themed window
background shows through on every theme (Starfield included).
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(75936ed)
- Fix release: pass changelog as file artifact, not inline body
softprops/action-gh-release receives the release body via the
INPUT_BODY env var, and a large git-cliff changelog (180 commits this
release) exceeds the per-string execve limit, so node fails to start
with "Argument list too long". Upload the generated CHANGES.md as a
"release-notes" artifact from the changelog job and consume it via
body_path in the release job, bypassing the env-var size limit.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(42ea484)
Other
- Upgrade dialogue preview to game-accurate Starfield subtitle panel
Extracted the following assets from the game's Scaleform SWF
(dialoguemenu.swf via JPEXS, Starfield - Interface.ba2):
data/dialogue_bg_tile.png — 50×50 white noise tile (DefineBitsLossless2
id 78) tiled across the subtitle panel fill
data/dialogue_panel_ref.png — 597×147 subtitle panel sprite reference
New _render_dialogue_scene() replaces the generic box renderer for the
"Dialogue" context. It faithfully recreates the Starfield subtitle UI:
Background dark atmospheric gradient (#0e1420 → #06080e) + star dots
Panel fill black alpha=127 (50 %) over background
Noise tile dialogue_bg_tile.png tiled at 28 % opacity for the grain
Border top white alpha=51 (20 %) — measured pixel-by-pixel from sprite
Border L/R white alpha=38 (15 %)
Border btm white alpha=19 ( 7 %) — fade effect
Speaker "NPC Name" placeholder in muted colour above box
Font RF_35_M ($NB_Grotesk_Semibold per fontconfig_uk.txt),
fontHeight=520 twips = 26 pt, scaled proportionally to canvas
Stage reference: 1920×1080 (Scaleform full-screen). All geometry (box
width, height, margins, font) scales as a fixed proportion of canvas_w so
the panel looks correct at any dock size.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(2bc3096)
- Upgrade Dialogue Tree Visualizer to Starfield dark theme
Applies the same game-accurate visual treatment as the Visual Context
Preview dialogue renderer: RF_35_M font via _game_font(), noise tile
overlay from dialogue_bg_tile.png, pixel-exact border opacities from
dialoguemenu.swf sprite analysis (top 20 %, L/R 15 %, bottom 7 %),
Starfield cyan (#3ff0ff) selection highlight, dark space background
(#080d16), and a left indicator bar on the player-prompt section.
_Arrow pens/brushes, chain separator, tree widget, topic label,
detail group box, and text browser all updated to the dark palette.
Old light-theme (blue/green/grey) colours removed throughout.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(6bfdb13)
- Replace QMediaPlayer with subprocess player to fix PulseAudio on systems without PipeWire
Qt Multimedia's FFmpeg backend probes PipeWire before PulseAudio; when
PipeWire is installed but not running it logs "Failed to connect to
pipewire instance: Host is down" and audio output fails entirely.
Replace QMediaPlayer + QAudioOutput with _SubprocessPlayer, a small
QObject that shells out to paplay / ffplay / aplay (tried in that order)
and tracks playback state via a 150 ms poll timer. The same signal
interface (playbackStateChanged) is preserved so the rest of the panel
needs no structural changes. Qt Multimedia is no longer imported.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(d2ab87d)
-
QC: detect size-tag spacer restructuring, line-prefix loss, and full printf specs
-
printf_var pattern now uses full format-spec regex so %.0f / %5d / %-10s
etc. trigger MISSING_TAG (previously only bare %f / %d were detected) -
SIZE_TAG_RESTRUCTURED (warning + autofix + retranslate): fires when a
<size=N%> whitespace spacer in the original is restructured into
a <size=N%>TEXT content wrapper in the translation; _fix_size_spacers
restores the spacer form mechanically -
LINE_PREFIX_DROPPED (error + autofix + retranslate): fires when a '-Header'
line (dash immediately before a letter, e.g. '-Costs') has its leading dash
stripped in the translation; _fix_line_prefixes restores it -
_fix_missing_tags: prepends rather than appends when the missing tag leads
the original string (e.g. '%.0f - Label' → prepend '%.0f' to front) -
build_retry_hint: added hints for both new codes
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(4b7fe39)
- Restyle Dialogue Tree cards to match Starfield in-game UI (sc1/sc2)
Player choice section now matches sc1.jpg choice rows: larger bold white text
(11pt), very dark blue-black fill (rgba 5,10,22), bright white top separator
(alpha 52), faint white hover overlay instead of opacity-dimming.
NPC section matches sc2.jpg subtitle bar: near-pure-black fill (rgba 0,0,0,218),
10pt near-white text, dim steel-blue 'NPC ▸' speaker tag above the line (mirrors
the 'Name: text' format seen in-game), very subtle noise texture.
Card geometry widened to 420px, row heights raised to 76/84px for legibility at
the larger font sizes. Side edges reduced to barely-visible alpha 22. Graph
background darkened to #050a14 with a 1px rgba border.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(abee27d)
- Restore dropped Bethesda game tags (, , etc.) after AI translation
Expands _restore_dropped_tags() in OllamaWorker to cover the full range of
Bethesda variable placeholder tags (mag, dur, area, repetitions, relat,
basename, N.Prop) with sign-prefix awareness (+ → sign kept on insert).
Near-end tags (frac ≥ 0.70) are now inserted before the last space-separated
token instead of appended, so inline-suffix forms like с land before
the unit character (e.g. "протягом с" not "протягом с ").
Improves _fix_missing_tags() in QualityChecker to detect and reconstruct
sign-prefixed tags (+/-) when autofixing QC MISSING_TAG issues.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(281c0cd)
- Show clean error when NexusMods search DNS fails
Instead of dumping the raw HTTPSConnectionPool traceback, catch
ConnectionError + Timeout separately and emit human-readable messages.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(39ed957)
- Use NexusMods GraphQL API (v2) for mod search
search.nexusmods.com (unofficial Elasticsearch) fails DNS resolution on
some networks. Switch to api.nexusmods.com/v2/graphql (nameStemmed MATCHES)
as the primary search path, which lives on the same host as all other API
calls. The old endpoint is kept as a fallback.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(024e711)
-
Replace NexusMods search table with card grid (NexusMods-style UI)
-
Mod cards with thumbnail images (215×121 16:9, loaded async with local cache),
mod name, author, endorsements/downloads, update date -
Selected card gets an orange (#DA8B15) highlight border
-
3-column grid in a QScrollArea; dialog resized to 1300×750
-
Thumbnails fetched via thread pool, cached in thumb_cache/ under cache_dir
-
QPixmap created in main thread from raw bytes emitted by worker signal
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(d2003cc)
- Filter OLD_VERSION files from NexusMods file list
Skip files with category_name == OLD_VERSION so only current
releases are shown. Also sort by upload date descending (newest
first) instead of size.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(c39df06)
- Filter both OLD_VERSION (catId=4) and ARCHIVED (catId=7) files
The v1 REST API uses two different categories for non-current files.
Previously only OLD_VERSION was filtered, leaving 20+ ARCHIVED entries
visible. Now both category name and numeric id are checked so the filter
is robust against API variants.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(18ace69)
- Obfuscate API keys in config JSON (XOR + base64)
nexusmods_api_key and weblate_api_token are now stored as
"enc:<base64(xor(value, fixed_salt))>" instead of plaintext.
In-memory values remain plain strings; only the on-disk JSON form
is wrapped. Legacy plaintext values are detected by the absence of
the "enc:" prefix and loaded as-is for backward compatibility.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(ed913ba)
- Regenerate CLAUDE.md to reflect current codebase state
Adds all modules added since last update: micro_animations, session_manager,
session_dialog, macro_recorder, macro_dialog, gender_checker, gender_dialog,
register_checker, register_dialog, spell_checker, font_checker_dialog,
ai_qc_worker, lore_rag_manager, lore_rag_dialog, audio_preview_panel,
tts_engine, translation_editor_pane, detached_table_window, focus_overlay,
dialogue_tree_dialog, visual_context_preview, profile_editor/assign_dialog,
weblate_client, weblate_sync_dialog, nexusmods_client/browser overhaul.
Updates CONFIG_VERSION (27), documents tag restoration, obfuscation scheme,
GraphQL search, and all new key design notes.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(83979cc)
- Reduce QC false positives for RU→UK translations
UNTRANSLATED: 2,241 → 85 (-96%)
- Cyrillic-to-Cyrillic check now only fires when source has Russian-exclusive
characters (ы/э/ё/ъ) OR the string is ≥ 4 alpha words long. - Short words identical in both languages (Лоб, Рот, Люк, Марк I) no longer
trigger UNTRANSLATED. Genuine Russian text (Брэдбери → has э, Малый → has ы)
still flagged correctly.
LENGTH_INCREASE / SUSPICIOUSLY_LONG: 52 → 1 (-98%)
- Skip ratio-based length checks for very short sources (≤ 8 chars), since
expanding an abbreviation or acronym to its full form is correct localization
practice (Амф→Амфетамін, КНР→Китайська Народна Республіка). - SUSPICIOUSLY_SHORT still fires for short sources to catch model truncation.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(d80b633)
- Sync Modelfile system prompts: add guillemet closing rule
All tracked Modelfile fallback system prompts now include:
"Always close guillemet quotation marks: every opening « must have a
matching closing »."
Only affects direct ollama run use (app always overrides at runtime),
but keeps Modelfiles aligned with the UNCLOSED_GUILLEMET QC check
added in 100e985.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(d44d86c)
- Bump AUR package to v0.2.2
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(bffe1bd)
- Relax mamaylm config: restore num_predict/num_ctx, remove max_concurrent
The only genuine fix needed was timeout: 480 — the 300s session default
fires before Ollama even starts generating when requests queue up.
num_predict=512 and num_ctx=8192 were borrowed from translategemma3-st-2
which is a 2.5× larger GGUF (16 GB vs ~7 GB for mamaylm Q4_K_M).
At standard 12B size, mamaylm should match translategemma3-st: 4096/16384
with no max_concurrent restriction.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(63965b2)
- Clarify AUR package is not yet published; document manual install
yay -S bethesda-strings-editor-bin would fail — the package has not
been submitted to aur.archlinux.org yet. Both the NexusMods description
and README project structure now say "manual AUR install" and show the
git clone + makepkg -si steps instead.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(fac58ab)
- Bring NexusMods description up to date with current feature set
Added: AI-fix mode, pipeline post-processing, Character Persona Profiling,
Lore RAG, pre-translation estimator, Claude cost estimator, Starfield TXT
support, Font & Glyph Checker, Auto-Fix All, per-code hide filter,
retranslation queue, plugin validator, Visual Context Preview, Dialogue Tree,
Audio/TTS Preview, diff viewer, advanced search, Focus Mode, multi-monitor,
Claude AI Assistant dock, 16 themes (named), Korean UI, crash recovery,
security audit log. Removed outdated "both models on hub" wording.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(6e4b9d9)
-
Redesign app icon with polished Starfield-themed aesthetic
-
Deep space radial gradient background with procedural star field
(80 stars + 3 bright hero stars with multi-ring glow) -
Subtle cyan nebula band behind the translation graphic
-
EN source block: dark tile with cyan glow border + white EN text
-
Glowing arrow with circuit dots along shaft
-
2×2 language grid: УК/DE (cyan), FR/JA (gold)
- УК tile uses actual Cyrillic RF_55_SB.ttf (Starfield game font)
- Other tiles use NB_Architekt.ttf (Starfield Latin game font)
-
Horizontal cyan divider line with glow
-
STRINGS wordmark + BETHESDA LOCALIZATION EDITOR subtitle
both rendered in actual Starfield NB_Architekt fonts -
Subtle scan-line texture overlay
-
Rounded corners (r=54 at 512px)
-
Outputs: app_icon.png (512px), app_icon_64.png, app_icon.ico (multi-res)
Generator: scripts/generate_icon.py (reproducible, deterministic stars)
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(69999cf)
- Trim protected terms to token names only (163 → 23)
Company names, faction names, ship names, lore proper nouns, and branded
proper nouns are all translatable and should not be protected — the AI
handles them correctly. Only UI abbreviations (AI, AP, HP, XP, HQ, HUD,
NG+, NPC, REV-8, UC, VFX, VIP, DLC, FX, CEO, Digipick, Scanner, Cutter,
Contraband, Wanted) and game-system acronyms (MAST, SPECIAL, V.A.T.S.)
are kept as they must remain unchanged in every locale.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(6f9d918)
- Clear protected terms — all Starfield terms are translatable
UI abbreviations (AI, HP, XP, NG+, etc.) and game-system acronyms
(MAST, SPECIAL, V.A.T.S.) also need to be translated per locale.
File kept as an empty template for future locale-specific overrides.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(4e487c7)
- Make model advice in settings dynamically update on model combo change
Replace hardcoded translategemma3-st tip with a live label that calls
_update_model_hint() whenever the Ollama model combo text changes.
Provides model-specific advice for translategemma3-st, gemma4-opus48-st,
Claude, and generic custom models.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(a825e9d)
- Replace minimal About dialog with full-featured info panel
Replaces the QMessageBox.about one-liner with a proper QDialog
showing the actual version (from _version.version), the real
feature set (file formats, AI backends, QA tools, glossary,
workflow), Python/PySide6 version, and a clickable GitHub link.
The old text mentioned only Ollama models and gave no version info.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(ee175f7)
- Scale up NexusMods header — larger fonts, tighter spacing
Title 74→90px, subtitle 22→26px, badges/chips 14→16px. Reduced
vertical gaps so content fills the canvas instead of feeling distant.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(a7ae101)
- Crop header height 372→300px to eliminate dead space
Removes the large empty band between the chips row and the wave
lines so the image feels dense and close-up when rendered on GitHub
at its natural content column width.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(496c6cd)
- Redesign NexusMods header in app-icon style
Replaces the sci-fi hex pattern with a clean software product banner:
- Left: blue→teal gradient app icon (rounded square) with stylised
string-lines symbol and cursor dot - Background: dark dot grid instead of hexagonal wireframe
- Chip borders changed to frosted glass style (white/translucent)
- Glow halo behind the icon for depth
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(aea0231)
- Refresh README badges with for-the-badge style and branded colors
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(6846fd4)
- Downgrade Ollama timeout from ERROR+traceback to WARNING+skip
Timeout was re-raised as Exception, which bubbled to translate_batch's
generic except handler and logged a full traceback at ERROR level. Since
a timeout just means the model was too slow for this string, treat it the
same as an empty response: log a WARNING in the worker thread and return
None so the batch skips the string silently and continues.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(901805c)
- Redesign NexusMods header banner with feature chips and stat boxes
New 1300×300 banner uses the space station background with a left-to-right
gradient overlay. Left panel shows 8 color-coded feature chips (Ollama,
Claude API, QC Checker, Glossary, ESP/ESM, BA2, Batch, NexusMods). Right
panel shows four key stats (16 QC Checks, 8K+ Terms, 12 Languages, 50K Cache).
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(69a3d72)
- Apply trailing-newline restore to cache hits; extract _apply_trailing_newline helper
Two problems with the previous fix:
-
Cache hits bypassed the new trailing-newline restore entirely.
- Pre-flight (translate_batch): applied _restore_missing_newlines but not the
new trailing-NL fix, so stale cache entries missing the trailing \n were
emitted unchanged. - _translate_single: returned cached results with zero post-processing, missing
both _restore_missing_newlines and the trailing-NL fix.
Now both paths call _apply_trailing_newline after _restore_missing_newlines, so
cache entries produced by the old code are healed on first read without needing
the cache to be cleared. - Pre-flight (translate_batch): applied _restore_missing_newlines but not the
-
Extracted the inline trailing-newline restore into a _apply_trailing_newline
static method shared by all four call sites (pre-flight, _translate_single cache
hit, _translate_single fresh result, _translate_chunked fresh result).
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(92a006e)
- Translate the self-review UI strings into all 7 locales
Filled in the 15 self-review feature strings (13 MainWindow status/summary
messages + the SettingsDialog checkbox and its tooltip) that lupdate had added
as type="unfinished", for uk_UA, de_DE, es_ES, fr_FR, pl_PL, cs_CZ and ko_KR.
Translations authored directly (no API). Menu-path references ("Translation →
Quality Check") reuse each locale's existing menu/action translations for
consistency; placeholders ({n}/{c}/{p}), the → arrow, — em-dash and … ellipsis
are preserved, French uses the project's space-before-colon convention, and the
multi-line tooltip keeps its line structure.
Only the elements changed — verified no
structural churn, all catalogs remain well-formed XML and compile cleanly, and
placeholder braces are valid (subset of each source string).
.qm binaries remain gitignored build artifacts (regenerated via
scripts/compile_translations.sh).
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(8705cae)
- Finish UI localization: translate all remaining unfinished strings (7 locales)
Apply authored translations to complete every translatable UI string across
all seven locales (de_DE, es_ES, fr_FR, pl_PL, cs_CZ, uk_UA, ko_KR), marking
them finished. Covers the strings drafted in a prior session (b1/b2) plus the
remaining ~380 distinct sources across MainWindow, SettingsDialog,
FontCheckerDialog, LoreRAGDialog, NexusModsBrowserDialog, ProfileEditorDialog,
GenderDialog and other dialogs.
de_DE and ko_KR reach 0 unfinished; es/fr/pl/cs/uk reach 0 except one
intentionally-empty source string (font_checker_dialog.py:191). uk_UA reuses
existing provisional text where present. All seven files compile cleanly with
lrelease.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(4c6bf0c)
- Replace app icon with Starfield "Constellation" design
New icon based on the AI-generated z-image-turbo_00010 concept: a glowing
blue Big-Dipper-style constellation on a rounded-square tile. Center-cropped
to tighten composition for small-size legibility, then exported to all three
consumed assets: app_icon.png (window), app_icon_64.png (tray/notifications),
app_icon.ico (Windows, sizes 16-256).
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(7af6cdd)
- Switch app icon to compass-rose + cursor design
New icon based on the AI-generated z-image-turbo_00011 concept: a metallic
Starfield-style compass rose with a glowing cyan cursor. Lightly center-cropped
(~94%) to tighten composition while keeping the bottom-right cursor intact,
then exported to all three consumed assets: app_icon.png (window),
app_icon_64.png (tray/notifications), app_icon.ico (Windows, sizes 16-256).
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(3a27d50)
- Trip the wedge breaker on a tail freeze (lower threshold to 3)
Ollama server logs from the reported failure confirm a hard GPU lockup: every
request completed healthily at 20+ tok/s until 14:35:03, then the server went
totally silent for 76 minutes until it was restarted (AMD RX 6800, ROCm
gfx1030). The freeze stranded only the final in-flight wave of strings, so the
worker-count-scaled threshold (effective_workers + 1 = 5) never tripped — the
user saw generic per-string timeouts instead of the actionable wedge message.
Use a fixed threshold of 3 consecutive read timeouts. A read timeout means zero
bytes for the whole budget, which on a working GPU never happens (the first
token always arrives), and the counter resets on any success — so 3 in a row is
an unambiguous backend freeze regardless of worker count, and it now fires even
when only the last few strings remain.