v0.2.4
[v0.2.4] — 2026-07-05
Added
- Add Korean to the language dropdown
Korean was fully implemented in the backend (ko translation prompt, ko word
checker, Hangul detection in QualityChecker, "Korean"->"ko" in app_settings)
but the ("Korean", "ko") entry was never added to MainWindow.SUPPORTED_LANGUAGES,
so it could not be selected as a source/target language at all. Add it in
alphabetical position (after Japanese, before Polish).
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(67e1dc2)
- Add Claude MCP connector to the AI Assistant chat panel
Wire the Messages API MCP connector (beta mcp-client-2025-11-20) into
the Claude client so the chat panel can call tools on remote MCP servers
Anthropic connects to and runs server-side.
- claude_client: ClaudeClient(mcp_servers=…); _mcp_request_kwargs() pairs
each server with its mcp_toolset (skips rows missing name/url); chat_mcp()
runs a non-streaming loop that collects text, reports each mcp_tool_use
via on_tool, and resumes multi-round turns on stop_reason=pause_turn. - claude_chat_panel: _ChatWorker uses chat_mcp when servers are configured
and surfaces live tool use in the thinking label (tool_note signal). - app_settings: enable_mcp + mcp_servers (CONFIG_VERSION 38); each entry's
authorization_token XOR-obfuscated on disk (plaintext in memory), same
policy as the NexusMods key. - settings_dialog: Claude MCP Servers group (enable toggle + Name/URL/token
table, Add/Remove); _collect_mcp_servers drops incomplete rows. - main_window: _apply_claude_mcp_settings pushes config to the panel on
startup and after Settings is applied. - tests/test_claude_mcp.py: 13 tests (request shaping, pause_turn resume,
token obfuscation, migration) with no anthropic package or network.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(e9c4fef)
Changed
- Update NexusMods header version v0.2.2 → v0.2.3
Repaint the subtitle version label in the header banner. Surgical
pixel edit: only the trailing digit changed (Roboto 12, matching the
existing text colour/size), with the diagonal background streak
preserved via per-column inpainting; the rest of the image is byte-
for-byte unchanged in appearance.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(10d0121)
Fixed
- Fix segfault when Settings model poll touches a freed fetcher thread
The 8s auto-refresh timer in SettingsDialog could call .isRunning() on a
_OllamaModelsFetcher whose underlying C++ QThread had already been freed by
the finished->deleteLater connection. shiboken raised "Internal C++ object
already deleted" inside the timer slot, which PySide6 escalated to a segfault.
Clear self._model_fetcher when the thread finishes (via _clear_fetcher) so the
next poll never sees a dangling wrapper, and guard the isRunning() probe with a
try/except RuntimeError as a fallback.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(ae0878c)
- Fix crash formatting string-keyed IDs as hex (TXT interface files)
Starfield interface TXT files (translate_en.txt / translate_ru.txt) key strings
by plain-string names, not integer FormIDs. Several call sites formatted the ID
with the hex format code (:08X / :06X), which only works on ints, so opening the
Quality Check dialog, exporting to xTranslator SST XML, or exporting to TXT all
raised "ValueError: Unknown format code 'X' for object of type 'str'".
Add a shared bethesda_strings.format_string_id() helper (int -> zero-padded
uppercase hex, non-int -> verbatim) and use it at the crashing sites:
- quality_dialog.py: all 7 report.string_id / string_id hex sites
- xml_handler.py: SST sID (width=6, no 0x prefix)
- main_window.py: TXT export (both export modes)
For integer IDs the output is byte-for-byte identical to before; string IDs now
render verbatim instead of crashing.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(f52fe04)
- Fix unreadable (white) first-run "Quick-start tips" dialog on themed UI
The welcome/first-run tips dialog put its tip rows inside a QScrollArea whose
viewport and inner content widget default to the light palette base. With a dark
theme active that meant light themed QLabel text (#ececec) on a white (#efefef)
background — invisible. The QDialog itself was themed correctly, which is why
the directly-added "Quick-start tips:" header was readable but the scroll rows
were not.
Apply the project's existing transparent-scroll idiom (already used by the main
welcome widget and the settings dialog): give the scroll area + inner body an
objectName, disable the viewport's autofill, and set transparent backgrounds so
the themed dialog background shows through on every theme.
Verified by rendering the dialog with the Starfield theme: the scroll body now
samples #0a0e1a (dark) instead of #efefef (white).
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(05766bc)
- Fix _clean_translation blanking valid short RU→UK output
A full-game RU→UK run with mamaylm produced ~156 empty translations for short
strings ("Небесная", "Торговец", "Есть!", "Давай! Давай!" …). The live model
translates all of them correctly — the loss happened in _clean_translation,
whose heuristics were written for EN→UK and misfire on closely-related
East-Slavic pairs where the UK output shares prefixes/substrings with the RU
source or is legitimately shorter:
- echo-prefix strip: "Торговець" starts with RU "Торговец", so the 8-char
prefix was stripped to "ь", then blanked as a 1-char fragment. - substring garbage check: "Небесна" ⊂ "Небесная" → blanked.
- short-source shrink check: "Есть!" (5) → "Є!" (2) → blanked.
- repetition de-dup: legitimately doubled source ("Давай! Давай!") collapsed to
"Давай!" then blanked as a substring.
Fix:
- Thread source_lang into _clean_translation (all 5 call sites pass
req.source_lang); add _norm_lang / _are_closely_related (ru/uk/be) helpers. - Skip the echo-prefix strip and the short-shrink + substring garbage checks for
closely-related pairs (EN→UK behaviour unchanged — verified 1-char garbage is
still blanked). - Skip repetition de-dup when the source itself repeats (_source_has_repetition),
language-agnostic.
7 regression tests added; full suite 438 passed.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(8661df8)
- Fix "QThread: Destroyed while thread is still running" on SSO sign-in
The NexusMods SSO worker (_NexusSSOWorker) is parented to the settings dialog
and can block for its full timeout while the user authorises in the browser.
Closing the dialog mid sign-in destroyed the still-running QThread child, which
aborts the process (SIGABRT).
Mirror the existing Ollama-fetcher teardown: SettingsDialog.done() now also
calls _stop_nexus_sso(), which disconnects the worker's UI-bound signals,
cancels it (polled on a ~2s cadence), and wait()s. If it's still blocked (e.g.
mid TLS handshake) it's detached from the dialog and parked in a module-level
set so Python won't GC it mid-run; it self-removes on finish.
Test: start a worker on a stubbed long-running request and assert cancel() lets
wait() return promptly (offscreen Qt, skipped if PySide6 is absent).
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(34e65ff)
Other
- Poll GPU stats off the UI thread; stop Windows console flashing
The status-bar GPU monitor called read_gpu_stats() (which shells out to
nvidia-smi, up to a 3 s timeout) every 2 s directly on the main thread via
QTimer, briefly freezing the UI whenever the call was slow. On Windows the
nvidia-smi subprocess also lacked CREATE_NO_WINDOW, so a console window flashed
on every poll.
- Move polling into a _GpuPollWorker(QThread) that does the blocking read on
its own thread and emits each reading via a queued signal; the widget starts
hidden and reveals itself once the worker confirms a GPU (so even the
detection probe never blocks the UI thread). - Pass creationflags=CREATE_NO_WINDOW to the nvidia-smi call (0 on non-Windows)
so no console window flashes. - Stop the worker cleanly on aboutToQuit / closeEvent to avoid a still-running
QThread at teardown.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(c1c13f5)
- Sweep remaining string-id hex-format crashes in TXT mode
Follow-up to the earlier QC/XML/TXT-export fix: the same ":08X"-on-a-string-id
bug crashed several other table-driven dialogs when a Starfield interface TXT
file (string-keyed IDs) was loaded. Advanced Search was the first one hit
("ValueError: Unknown format code 'X' for object of type 'str'").
Route every model-driven ID format through bethesda_strings.format_string_id
(int -> zero-padded hex, str -> verbatim):
- advanced_search_dialog.py (reported)
- gender_dialog.py, register_dialog.py
- font_checker_dialog.py (table + TXT/HTML export)
- translation_dialog.py, diff_viewer.py (badge + header), claude_chat_panel.py
- main_window.py glossary-compliance report
Verified safe / left unchanged: ESP/FormID-only sites (dialogue tree, VMAD,
ESP migrate, speaker panel), version compare (.strings only), audio panel and
string_table (already isinstance/mode-guarded), and focus_overlay /
translation_editor_pane (read a non-existent "string_id" key -> int -1 default).
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(1a017fa)
- Document server-side Ollama GPU env vars in Settings help
Add a theme-aware help label to the Ollama section of the Settings dialog
explaining the server-side environment variables the app cannot set itself
(it only sends per-request options):
- OLLAMA_NUM_PARALLEL=N — concurrent GPU slots; match to the app's parallel
workers for two-stream throughput. Explains the VRAM ≈ weights + N×ctx×KV
relationship and the model-reload/eviction symptom when it's too high. - HSA_ENABLE_SDMA=0 — fixes ROCm GPU ring hangs on AMD gfx10xx (RX 6800/6700).
- Tip: OLLAMA_KV_CACHE_TYPE=q8_0 + OLLAMA_FLASH_ATTENTION=1 ~halve KV VRAM.
Uses palette(mid) styling so it stays readable on every theme. Placed next to
the existing ROCm-related force-stop command field.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(b5321f8)
- Docs: give a real local-GGUF install path for the translation model
The custom translation fine-tunes (translategemma3-st etc.) are private/local
only and are not published on any hub — there is no ollama pull for them.
The install docs implied a pullable model and left the GGUF source unspecified,
so users had no working way to obtain a translation model (the old hub name
0xra/bethesda-translate 404s). The bundled Modelfile also hardcoded a
machine-specific blob path, so ollama create couldn't work out of the box.
- README & NexusMods description: state plainly that the translation models
aren't on any hub, and document two working paths — (A) the Claude API
backend (no local model), and (B) building from a GGUF (download a public
model such as MamayLM, point the Modelfile FROM at it,ollama create).
Keep the optional QC-model hub pull. - Modelfile: replace the private blob
FROMpath with a clear placeholder +
comment pointing at a public GGUF source, so the documented build steps work.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(b98760e)
- Tune quality checker for Starfield interface TXT files
Interface TXT (translate_*.txt) is almost entirely short UI labels ($OK, $YES,
$ABORT) that legitimately expand 2–3× in other languages, and uses Scaleform
markup. The quality checker ran on these files but produced
length-expansion noise on nearly every label and didn't recognise tags.
- QualityChecker gains a
ui_stringsflag (default False, so .strings/ESP
behaviour is unchanged). When set, the INFO-level LENGTH_INCREASE flag — which
only ever fires on short sources — is suppressed. UI_OVERFLOW (bounding-box)
and the >5× SUSPICIOUSLY_LONG outlier check still apply, as does
SUSPICIOUSLY_SHORT for truncation. - main_window passes ui_strings=isinstance(current_file, TxtStringFile) when
building the quality map. - Tag patterns now recognise <img …> (Scaleform/HTML image), so a dropped image
is reported as MISSING_TAG. The pattern also covers the older <image …>. - 5 new tests cover the interface-TXT path; existing behaviour is asserted
unchanged for the default checker.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(a9367fc)
A real Starfield interface file (Data/Interface/translate_uk.txt, 3117 entries)
has zero angle-bracket tags — no , , or
. The placeholders are
{0}/{1}/{2} brace tokens (already covered by the existing brace_var pattern) and
a couple of % specifiers. My earlier tag addition was an unverified guess.
- Restore the original <image[^>]*> tag pattern (drop the
change).
- Replace the two
-based tests with brace-placeholder tests that match the
real format ({0} dropped -> MISSING_TAG; {0} preserved -> clean).
The ui_strings length-noise suppression is kept — validated by the real file
being almost entirely short UI labels that legitimately expand.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(cd43ffa)
- CI: drop runner's Microsoft apt repos before apt-get update
The GitHub runner ships pre-baked third-party repos (azure-cli and
packages.microsoft.com) that intermittently return 403 Forbidden. apt-get
update fails the whole step with exit 100 when any single repo errors, even
though the test job only needs the Ubuntu archive (libegl1, libhunspell-dev).
Remove the Microsoft source lists (by name, plus a grep fallback for any file
referencing packages.microsoft.com) before refreshing the index, so a transient
403 on an unrelated repo can't break the test workflow.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(10c54e2)
- Cut RU→UK quality-checker false positives (coverage, untranslated, leak)
Quality reports from a full RU→UK run were dominated by false errors on
perfectly good Ukrainian — the checks were written for cross-script EN→UK and
misfire on closely-related East-Slavic pairs that share script and vocabulary.
Measured against the actual reports:
- LOW_UKRAINIAN_COVERAGE: 134 → 11. The lemmatised dictionary misses inflected
forms and all proper nouns, tanking "coverage" on real Ukrainian. Skip the
check entirely when the text contains any Ukrainian-exclusive char (і/ї/є/ґ) —
a definitive "this is Ukrainian" signal. (+ add ґ/Ґ to the per-word set.) - UNTRANSLATED: 39 → 0. Whole short phrases are legitimately identical in RU and
UK ("Просто не знаю", "Я не знаю!"). For East-Slavic pairs, only flag an
identical copy when it carries Russian-exclusive letters (ы/э/ё/ъ); drop the
≥3-word coincidence rule that flagged shared phrases. - SOURCE_LANGUAGE_LEAK: 10 → 0. Pass-2 (Russian vocabulary) tripped on shared
Slavic roots. Skip it when the text has і/ї/є/ґ, and raise the word threshold
5 → 8 so short shared-root phrases don't fire (Pass-1 ы/э/ё/ъ char check is
unchanged).
MISSING_NUMBER (numbers legitimately spelled out, "100,000"→"сто тисяч") and
UI_OVERFLOW are intentionally left as-is. Added _is_east_slavic_pair / _norm_lang
helpers; existing tests updated to the corrected behaviour; 5 regression tests
added. Full suite 441 passed.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(f15e381)
- Heal \н escape + [TK:] hallucinations; stop mixed-script re-breaking \n
Quality report on the mamaylm full-game RU→UK interface-TXT run surfaced
three recurring artifacts the cleaners missed:
-
\н(backslash + Cyrillic н): the model's Cyrillic substitution bleeds
into the literal\nescape, and the broken escape glues the next word
onto a preceding URL — tripping MISSING_URL (reports $CreateAcct…,
$LoginError…, $LegalScreen). The lone fix lived in_clean_translation
and emitted a real newline, wrong for TXT's literal escapes, and was
absent from the cache-hit_heal_known_artifactspath. New shared
_heal_cyrillic_escapes()restores the form the source uses (literal
\nvs real newline) and runs in both paths. -
[TK:…]translation-key markers the model invents on short titles
("ЛУНА"→"МІСЯЦЬ\n\n[TK:0001256_00000004]\n\n"). New
_strip_hallucinated_tk()cuts the fabricated tail for short single-line
sources and removes bare markers otherwise; wired into both paths. -
_fix_mixed_scriptwas converting the freshly-restored Latinnin
\nПомилкаstraight back to\нПомилка(and would corrupt any
model-correct literal\nescape stuck to a Cyrillic word). Anchored
_MIXED_WORD_REwith(?<!\\)so an escape char never starts a word.
Adds 16 regression tests (453 total pass).
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(564e165)
- QC: stop malformed "\ n" escapes + glued URLs faking MISSING_URL/EXTRA_TAG
After the \н→\n heal a faithful translation of the Bethesda.net account
strings still tripped MISSING_URL, EXTRA_TAG and NEWLINE_COUNT_MISMATCH —
not because the translation was wrong, but because the source carries
malformed line-break escapes (\ n = backslash + space + n) that the engine
still renders as a break. The checkers counted only clean \n, so any correct
\n output looked like an added tag/newline, and the whitespace-greedy URL
regex glued the trailing \n…/punctuation onto the address differently on each
side. Both fire on any correct translation of these strings, with or without
the heal.
- New
_normalize_escapes()collapses\ n/\ tto\n/\t; applied in
_check_tagsand_check_newlinesso both sides count line breaks the same. - New
_normalize_url()trims a trailing escape sequence and sentence
punctuation before comparing, so the same URL matches regardless of glued junk.
Adds 5 regression tests (457 total pass).
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(41b0ead)
- Build: cut AV false positives — source bootloader, version-info, SignPath
The PyInstaller release zips were auto-quarantined on NexusMods by VirusTotal
heuristics (generic/ML hits), not by any real malware. Three zero-cost
hardening changes that lower the detection score on the next tagged build:
-
release.yml: install PyInstaller with
--no-binary pyinstallerso the
bootloader is compiled from source. The prebuilt wheel's bootloader bytes are
in many AV signature DBs (shared with malware using the same wheel) — the
biggest single driver of these false positives. Runners already have a C
toolchain, so no extra setup. -
spec: embed a Windows VSVersionInfo resource (CompanyName / ProductName /
FileVersion / copyright) generated from _version.py. A bare unsigned exe with
no metadata scores higher on heuristics and SmartScreen. Windows-only;
returns None on Linux. Verified both code paths. -
release.yml: replace the paid EV-cert comment block with a ready-to-enable
SignPath.io step (free for open source). Left commented until the project is
enrolled and the org/project/policy secrets are added. -
gitignore the build-time version_info.txt.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(236e7f4)
- Build: fix dead SignPath URL — point to SignPath Foundation (signpath.org)
The old /open-source path returns 404. The free OSS code-signing program is
run by the SignPath Foundation at https://signpath.org/ (apply there; terms at
/terms.html). Project/policy setup docs live at docs.signpath.io/projects and
docs.signpath.io/signing-code. Added the qualification note so it's clear this
repo is eligible (MIT/OSI-approved, public, maintained, no proprietary parts).
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(866faa3)
- NexusMods: add SSO sign-in to comply with API Acceptable Use Policy
Nexus Mods quarantined the app for a Terms-of-Service violation (not malware):
public-facing apps may not use pasted personal API keys. The compliant path
is their Single Sign-On flow, which issues a per-user key through the browser.
- gui/nexusmods_sso.py: minimal RFC-6455 WebSocket client over stdlib
socket/ssl (no new dependency, so it works in the frozen build) implementing
the SSO handshake. encode_frame/decode_frame/build_sso_url are pure. - settings_dialog.py: "Sign in with Nexus Mods" button + _NexusSSOWorker
(QThread) that opens the browser and fills the API-key field on success.
Key-paste field reworded to note it's personal/testing use only. - app_settings.py: persist the reusable SSO connection_token
(nexusmods_sso_token, obfuscated) so re-auth can skip the browser; config
v36 migration. - tests/test_nexusmods_sso.py: 11 tests for the frame codec + URL builder.
- nexusmods_client.py / CLAUDE.md: document SSO as the required auth path.
NOTE: SSO needs the app registered with Nexus Mods (they assign the
APPLICATION_SLUG). Until registered it times out and manual key entry remains
the fallback.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(ac0b579)
- NexusMods SSO: make application slug configurable ("Application ID was invalid")
"Application ID was invalid" is shown by the SSO web page when ?application=
isn't a NexusMods-registered application. The hardcoded placeholder slug
("bethesda-strings-editor") is unregistered, so it always fails — and because
protocol-2 validates the slug browser-side, the WebSocket just times out rather
than erroring. Verified the wire protocol against Nexus' own sso-integration-
demo: {id, token, protocol:2} + ?id=&application= is correct, so this was never
a protocol bug — only an unregistered/hardcoded slug.
- nexusmods_sso.py: APPLICATION_SLUG now honors the NEXUSMODS_SSO_SLUG env var;
timeout/error messages name the slug and explain the "Application ID was
invalid" cause + how to fix it. - app_settings.py: new nexusmods_sso_slug setting (config v37 migration).
- settings_dialog.py: "SSO App Slug" field; passed through _NexusSSOWorker to
request_api_key(slug=…); blank falls back to the built-in default. - tests: env-var slug override + default resolution.
The slug must still be approved by NexusMods staff (register the app: name +
description + logo). Once they assign one, enter it in Settings → NexusMods.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(31bd963)
- Docs: add Nexus Mods API application-registration package
Ready-to-send materials for registering the app as a public-facing Nexus Mods
API application (the manual, staff-handled process required to obtain the SSO
slug): application name, short description, logo (resources/app_icon.png,
512×512, dark-background-safe), an intended-API-usage / rate-limit summary
demonstrating compliance, testing-build instructions, a copy-pasteable email to
support@nexusmods.com, and the post-registration steps for entering the issued
slug (Settings → NexusMods → "SSO App Slug").
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(1bc1482)
- Docs: correct banner dimensions (1300×300) in registration package
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(c5d8024)
- Ollama_worker: heal MamayLM EN-number placeholder + appended-label artifacts
A fresh (non-cached) RU→UK batch with the mamaylm model produced two
deterministic artifacts flagged in quality_report_20260624_152755:
-
EN-number placeholder hallucinations ($LegalScreen): the model emits tokens
like "EN900016" / "EN900031EN900032…" (and the Cyrillic look-alike
"ЕН900001") in place of segments it failed to translate — dropping a URL and
a number (MISSING_URL / MISSING_NUMBER). These never appear in Bethesda
source strings. _has_placeholder_artifacts() now flags them in
_needs_ru_to_uk_retry / needs_en_to_uk_retry, so a fresh pass re-translates
the dropped segments from the source; strip_placeholder_artifacts() is the
last-resort safety net (also tidies the " … __" wrappers + doubled spaces)
so a literal token can never ship even if every retry fails. The main path
captures the artifact BEFORE cleaning so the strip doesn't hide the retry
signal. -
Fabricated heading appended to a short UI label ($AUTO BUILD "АВТО" →
"АВТО\n\nКОМПЛЕКТНІСТЬ…", $MAP "КАРТА" → …): _strip_appended_after_short_label
keeps only the first line when the source is a short, single-line label but
the translation grew newlines (NEWLINE_COUNT_MISMATCH).
Both are wired into _clean_translation (fresh path) and _heal_known_artifacts
(cache path). 16 new tests off the real report cases; full suite 483 passed.
The report's EMPTY_TRANSLATION / SUSPICIOUSLY_SHORT / UI_OVERFLOW items are
model-content failures (mamaylm returned empty/truncated/over-long output), not
deterministic cleaner bugs — the existing empty-retry and rewrite paths already
cover them; they re-roll on a fresh translation.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(5ed6dad)
- Docs: add nexusmods_registration to a toctree (fix -W Sphinx build)
The new docs/nexusmods_registration.md wasn't referenced by any toctree, which
Sphinx reports as toc.not_included — fatal under the CI's -W (warnings as
errors). Add it under a "Deployment" caption in index.rst. Verified locally:
sphinx-build -b html docs docs/_build/html -W --keep-going exits 0 with no
warnings.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(df9f324)
- Docs: note current moderation/quarantine status in registration package
Record that the mod page is hidden under moderation review (Myrddin:
"Violation of API policy") and document how to proceed: don't edit the
entry or post on public forums, reply civilly to the support thread,
acknowledge the personal-API-key violation, and send the registration
request so the app can switch to SSO and be un-quarantined.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(a665b31)
- Ollama_worker: strip Ukrainian stress/accent marks from mamaylm output
MamayLM glues наголос marks onto Ukrainian vowels — a grave accent
("робо`та"), an acute accent ("робо´та") or their combining forms
(U+0300/U+0301). Bethesda game strings never carry stress accents, so
they are artifacts.
Add _STRESS_ACCENT_RE + _strip_stress_accents(), wired into both
_clean_translation (fresh output) and _heal_known_artifacts (cache hits).
Source-checked like the other strippers: if the source itself uses the
character (e.g. backticks in code/terminal text) the string is left
untouched. Collapses the doubled space a standalone mark leaves behind.
Adds 7 tests covering all four accent forms, the source-preserved case,
clean-text noop, orphan-space tidy, and the clean/heal pipelines.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(0dc1395)
- Ollama_worker: heal MamayLM Russian-word leakage (Ukrainian output)
MamayLM (INSAIT MamayLM-Gemma-3-12B-IT-v2.0) is Ukrainian-trained but
leaks Russian words into otherwise-Ukrainian output more often than
translategemma3-st. The existing detectors were blind to this: both
_needs_ru_to_uk_retry word-checks and text_has_russian_words bail out
as soon as any Ukrainian-specific letter (і/ї/є/ґ) is present, so a
Russian word inside a mostly-Ukrainian sentence was never caught, and
_needs_en_to_uk_retry had no Russian detection at all.
Fixes, layered cheapest-first to avoid extra GPU calls (ROCm hang risk):
- Deterministic, case-preserving, agreement-free function-word map
(_RU_FUNCTION_WORDS + _fix_russian_function_words), wired as Step 0 of
_fix_known_errors so it runs before ы/э/ё/ъ substitution and before the
retry detectors — cleaned function words no longer over-trigger rewrites. - _needs_en_to_uk_retry now flags Russian-only letters and heavy Russian
word content, so residual leakage routes to retranslate. - Hardened the uk target-style prompt and _force_english_retranslate
system prompt to forbid Russian words / Russian-only letters and prefer
Ukrainian-divergent vocabulary.
8 new tests (word swap, case preservation, boundary guards, multi-word
equivalent, clean-Ukrainian noop, pre-substitution ordering, EN→UK retry
firing/ignoring). Full suite: 498 passed.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(8bb9fc4)
- Strings: stop triplet-merge contamination + add translation-folder validator
Diagnosed an in-game "<Error: Unknown lstring ID XXXXXXXX>" report. The root
insight: Bethesda's .strings/.dlstrings/.ilstrings companion files have
INDEPENDENT string-ID spaces — the same numeric ID is a different string in
each (the engine picks the file from the field type, not the ID). Two problems
flowed from that:
-
Companion-merge contamination. main_window._offer_triplet_load merged the
sibling files into the open BethesdaStringFile, deduping by bare ID, and then
saved the bloated result — writing a file contaminated with foreign-ID-space
entries (real signature on disk: starfield_uk.ilstrings held 130279 entries
vs the source's 130276; .strings +100, .dlstrings +6). Fixed: companions now
load as a read-only bethesda_strings.triplet.TripletReference that keeps the
three ID spaces separate and is NEVER appended into / saved from the open
file, so saves stay pure. Browse it via Translation > Companion Strings
(companion_strings_dialog). -
No way to catch missing/incomplete translations before launching the game.
Added bethesda_strings.strings_validator.validate_translation (pure) +
Translation > Validate Translation Folder (validate_translation_dialog): scans
a translated Strings folder against the English sources (loose files + .ba2
archives) and lists every file/ID that will show "Unknown lstring ID"
in-game (missing / empty / unparseable / incomplete), and flags extra_ids as
cross-file-type contamination. On the user's own data it correctly found 3
error-causing files and the 4 contaminated starfield.* files.
No data-fix scripts — codebase fix + tests only. 14 new tests
(test_triplet.py x6, test_strings_validator.py x8); full suite 512 passed.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(15bb504)
- Ollama: transliterate names under "Protect English text", pin via glossary
When translating a full non-English localization (e.g. Russian → Ukrainian)
with "Protect English text" on, _protect_english_text() used to blanket-protect
every Capitalised Latin word as a proper noun. Character and planet names left
in the source (Sarah Morgan, New Atlantis, Jemison) were therefore kept English
instead of being transliterated into the target script — the reported bug.
Fix (deterministic, no extra model call):
- Rule 4 dropped: a bare Capitalised proper noun with no glossary entry is left
untouched so the AI transliterates it. - Still protected: ALL-CAPS game codes/acronyms and lowercase English content
words (dictionary-checked) — unchanged. - New Phase 0 _glossary_localize(): before the per-word scan, each glossary hit
is replaced with its target_term behind the same EN…… restore token, so
multi-word names collapse to one token, the AI can't mangle them, and a
glossary entry beats even ALL-CAPS protection. This is how a user pins an
exact spelling (Sarah Morgan → Сара Морган). Empty target, missing manager,
and lookup errors all no-op safely.
The empty protected_terms_starfield_hq.txt means the "Protected Terms" list was
not the culprit; the separate "Protect proper nouns and lore terms" opt-in is
untouched. Claude backend has no English-protection path (Ollama-only).
Updated the "Protect English text" tooltip to match. Codebase fix + tests only.
12 new tests; full suite 524 passed.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(aee2f54)
- Register: drop the ти/ви checker, fold the rule into the translation prompt
Remove the standalone Ukrainian register (ти/ви) consistency checker
(register_checker.py + register_dialog.py, the "Check Register (ти/ви)…"
menu action and its Ctrl+Alt+R shortcut, the enable toggle, and the
_check_register handler).
Its intent now lives in the translation system prompt: _TARGET_STYLE["uk"]
tells the model to keep informal ти / formal ви consistent per speaker and
never mix the two (including the possessives твій/ваш), matching the source
tone and speaker relationship. Because both backends build their system
prompt via TranslationRequest.to_system_prompt() (claude_client.translate()
calls it too), this covers Ollama and Claude with one change — and register
is now enforced during translation instead of flagged after the fact.
Updated the settings tip and CLAUDE.md accordingly. No tests referenced the
removed checker; added test_uk_register_prompt.py (3 tests) to lock the rule
into the uk prompt and keep it out of other targets. Full suite 527 passed.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(2b1804d)
- Nexusmods: set registered SSO slug, remove all personal-API-key entry
Nexus Mods staff registered the app (slug: 0xra-bethesdastringseditor) and
required removing personal API key entry/storage per their API Acceptable Use
Policy. Both done:
-
nexusmods_sso._DEFAULT_SLUG = "0xra-bethesdastringseditor" (the approved slug;
still overridable via Settings, NEXUSMODS_SSO_SLUG, or slug= arg). Pinned by a
new test so it can't be reverted silently. -
Removed the two manual personal-API-key paste fields:
- settings_dialog: dropped the nexusmods_api_key QLineEdit (+Show toggle).
NexusMods section is now Sign in / Sign out + a signed-in/not-signed-in
status. The SSO-issued key is held in self._nexus_api_key (never an editable
widget) and persisted to AppSettings.nexusmods_api_key; Sign out clears the
key and SSO token on the device. - nexusmods_upload_dialog: dropped the "Paste your NexusMods API key…" field
and its "Get your API key" link to the personal-keys page. It now reads the
SSO key from settings and shows a status line, erroring "sign in via
Settings → NexusMods first" when absent. _save_to_settings no longer writes
the key.
- settings_dialog: dropped the nexusmods_api_key QLineEdit (+Show toggle).
SSO is now the only key-acquisition path. The SSO-issued key (app-scoped,
per-user, revocable) is still stored in the config — that is permitted; the AUP
prohibition is specifically on pasted personal account keys, which can no
longer be entered anywhere. Updated the sign-in-failed message (no more "paste a
personal key" fallback) and CLAUDE.md. Full suite 528 passed.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(d07cba9)
- Ci(docs): supersede in-flight Pages deploys instead of queueing
The Pages deploy history showed a pile-up: with concurrency
cancel-in-progress: false, each push queued a full 10-minute deploy-timeout
behind the previous stuck deploy when the GitHub Pages backend was slow
(16:35 → 16:41 → 16:55, each burning 10+ min). A docs site only ever
publishes the latest main, so a newer push should cancel the older in-flight
deploy rather than wait for it. Superseding a not-yet-live deployment is safe:
the last successful deployment stays published until the new one flips over.
Note: this does not change action versions — all are already on their latest
majors (checkout@v4, setup-python@v5, upload-pages-artifact@v3,
deploy-pages@v4). The "Node 20 deprecated" line is an informational runner
warning emitted from inside those actions (GitHub already force-runs them on
Node 24); it is not the cause of the deploy timeout, which is a transient
GitHub Pages backend stall.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(aa668cf)
- Release v0.2.4
Bump version to 0.2.4 (main.py, pyproject.toml) and add the [0.2.4]
changelog section covering the commits since v0.2.3: the Claude MCP
connector in the chat panel, the translation-folder validator, the
companion-strings viewer, NexusMods SSO sign-in, inline ти/ви register
enforcement, RU→UK name transliteration + glossary pinning, the
triplet-merge contamination fix, and a batch of MamayLM RU→UK output
healing and quality-checker false-positive fixes.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(0302425)
Removed
- Remove redundant "Translate Starfield Interface TXT" menu action
Starfield interface TXT files (translate_en.txt / translate_ru.txt) already
have their own open path: opening a .txt detects is_starfield_txt() and loads it
into the table (_load_txt_file), after which the normal Translate-All pipeline
and Save (via TxtStringFile) handle it like any other file.
The separate Translation-menu action duplicated that with its own file picker,
its own output-file picker, and a parallel translation/write code path
(_txt_translation_data / _translatable_items / _finish_txt_translation), gated
by an _is_translating_txt flag threaded through the result-dispatch hooks.
Remove the menu action and the whole standalone flow, and simplify the dispatch
hooks back to a single model path:
- _on_translation_ready buffers plain (index, translated) tuples
- _flush_translation_updates just calls set_translated_text_batch(updates)
- _on_ollama_finished drops the _is_translating_txt early-return branch
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(2af37cf)