Skip to content

Releases: 0xra0/bethesda-strings-editor

v0.2.5

Choose a tag to compare

@github-actions github-actions released this 20 Jul 07:45

[v0.2.5] — 2026-07-20

Changed

  • Update NexusMods header version v0.2.3 → v0.2.4

Repaint just the version digit in the banner subtitle (3 → 4) to match
the v0.2.4 release. The glyph is re-rendered in the original font
(Roboto 12, colour #829EC4) with the background behind the old digit
reconstructed by inpainting, so the streak/gradient is preserved; only
84 px in the digit box change and the rest of the image is byte-identical.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(ae219f5)

  • Refactor(settings): move GPU tuning help into rotating tips (translated)

The server-side GPU tuning block (OLLAMA_NUM_PARALLEL, HSA_ENABLE_SDMA,
OLLAMA_KV_CACHE_TYPE) was a permanent inline QLabel in the Ollama settings
group. Move it into the rotating general tips as three concise entries.

_show_tip() now routes tips through self.tr(), and each of the 7 UI locales
gets the three new tips translated (adapted from the prior block's wording),
replacing the now-orphaned "Server-side GPU tuning" message. Untranslated
tips still fall back to their English source.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(382654d)

Fixed

  • Fix: repair Claude backend cache + term-protection wiring

Four bugs (found by auditing the KR fork against this repo), all confined to
the Claude backend and the batch-folder dialog — the Ollama path was correct:

  1. Term restore called a non-existent TermProtector.restore(); the method is
    restore_text(). The AttributeError was swallowed, so protected terms were
    never restored (placeholder tokens leaked into saved translations).
  2. if self.translation_cache: — TranslationCache defines len but no
    bool, so an empty cache is falsy and disabled cache read+write forever
    (it could never populate). Use is not None.
  3. Cache/TM were read with no is_retry guard, so a QC retranslation
    (retry_hint) got the same stale string back, silently defeating the retry.
  4. Cache write called a non-existent .put(); the method is .set(). This was
    masked by bug 2 and would have raised once the write path was unblocked.

batch_translate_dialog had the same broken names: protect()->protect_text()
(term protection was silently a no-op there), restore()->restore_text(),
put()->set().

Adds tests/test_claude_worker_cache_terms.py (4 tests) driving translate_batch
synchronously with a fake client + fake protector. Full suite: 601 passed.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(a3228b9)

  • Fix(themes): stop the group-box border from striking through its title

Every built-in theme placed the QGroupBox title with
subcontrol-origin: margin, which paints it in the band above the border,
anchored to the widget's top edge. That band is exactly margin-top tall,
and it was 8px — while the title text is ~14px at the default 9pt font. The
border landed in the middle of the words, in every dialog in the app.

Qt resolves the QSS em unit to the font's full text height, so 1.0em is
the exact break-even point at every font size (verified 8pt–18pt). Use
margin-top: 1.4em for a constant 40% clearance that tracks the user's
font; a fixed px value breaks again as soon as the font grows (18px still
struck through at 14pt). The old padding-top: 16px only existed to push
content clear of the overlapping title, so it drops to 8px.

Fixes all 16 themes plus the same latent bug in dialogue_tree_dialog's
inline stylesheet. Adds tests/test_theme_groupbox_title.py (32 tests) to
pin the invariant: margin-band titles must reserve their band in em (>= 1.2).

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(fe563f1)

  • Fix(tests): narrow re.Match before .group() so pyright passes

The Lint workflow's pyright job failed on fe563f1: _TITLE_RE.search(qss)
and _GROUPBOX_RE.search(qss) return Match | None, and calling .group(1)
straight on the result is a reportOptionalMemberAccess error.

Assert each match first. This also gives the test a real failure message when
a theme is missing the rule entirely, instead of an AttributeError on None.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com(6097239)

  • Fix(qa): never pair two fields of one menu as large-font variants (#9)

The _lrg worst-case check keyed variants on (base_swf, field_name,
usable_width). That key is not unique within a single menu: one SWF
commonly reuses a field name for several boxes of the same width
(text_tf appears 146 times across the shipped UI). When it did, the
unrelated fields were grouped as "variants" of each other and the
tightest won — so chargenmenu › text_tf (844px @ 29px) was measured
against a 126px title in the same menu. That is a six-character budget,
which calls every translation an overflow; 33 widgets also "swapped" to a
build that was not the large-font one at all.

variants() now requires an unambiguous 1:1 standard↔large pairing and
otherwise refuses, which removes both faults on the real install (105
swaps, all to a genuine _lrg build; 0 anomalies). Refusing is the honest
move — the same reason the character-id key was discarded — but it must
not be silent, so worst_case_with_reason() reports which build it
measured and why, and the dialog says so on every path, including
"⚠ its large-font twin cannot be told apart — it was NOT checked".

Two claims in the docs did not survive the data and are corrected: the
large-font build is not universally tighter (of 589 widgets in both
builds, 251 grow the font, 336 keep it and 2 SHRINK it — missionboard
goes 49px → 26px), so the UI names the build it measured instead of
asserting it was the accessibility one.

The flagship case is unaffected: «Розташування корабля» still fits
shipcrewmenu › Location_tf at 61 % (18px) and clips at 122 % in
shipcrewmenu_lrg (36px), same 264px box.

Co-authored-by: BuildBot build@local
Co-authored-by: Claude Opus 4.8 noreply@anthropic.com(e6ae11c)

  • Fix(startup): preload system libva so bundled FFmpeg finds vaMapBuffer2 (#10)

PySide6's wheels ship FFmpeg through stub libraries
(libQt6FFmpegStub-va*.so) that forward VA-API calls to the system libva.
The bundled libavutil.so.59 needs vaMapBuffer2 (libva >= 2.21), but the
shipped stub predates that symbol and does not export it. When VA-API
hardware video decoding kicks in -- e.g. the animated video background --
the loader can't resolve vaMapBuffer2 and the process aborts with a hard
"symbol lookup error" that Python can't catch.

Preload the real system libva with RTLD_GLOBAL at the start of main(),
before Qt Multimedia loads libavutil, so vaMapBuffer2 resolves against
the system library (2.23 exports it). Keeps hardware decoding working.
No-op off Linux or when libva is absent (no libva -> software decode ->
symbol never referenced).

Co-authored-by: BuildBot build@local
Co-authored-by: Claude Opus 4.8 noreply@anthropic.com(c29fe2a)

  • Fix(gender-dialog): survive empty results and use the right table signal (#11)

Two crashes in GenderDialog, both reachable from _check_gender_agreement:

  1. With zero mismatches, _setup_ui() returns early before building
    self._table (it shows only the "no issues found" label + Close), but
    init still called _populate(), which unconditionally touched
    self._table -> AttributeError. Guard _populate() to no-op when there
    are no mismatches, mirroring _setup_ui().

  2. With any mismatches, _setup_ui() connected self._table.currentRowChanged
    -- a signal QTableWidget does not have (it belongs to QListWidget) --
    so the dialog crashed at construction. Use the correct
    QTableWidget.currentCellChanged(row, col, prevRow, prevCol) via a small
    lambda adapter feeding the existing _on_row_changed(row) slot.

The dialog now works whether or not the check finds issues.

Co-authored-by: BuildBot build@local
Co-authored-by: Claude Opus 4.8 noreply@anthropic.com(f5f76e5)

  • Fix: revive source-keyed translation memory, plus 7 fixes found reviewing the KR fork (#12)

Audit of fngpsldk-ops/bethesda-strings-editor-KR_Edit for defects that still
apply here. Most of their fixes are already upstream; these eight are not. The
OpenAI-compat/Gemini backend work and their table-sort repairs (which fix a bug
their own click-to-sort feature introduced) are deliberately not carried over.

Correctness

  • TranslationMemory.len/bool read only _by_id, so a memory keyed purely
    by source text reported itself empty. That is everything the Official-TM miner
    produces and everything a TMX import produces, and five separate gates test
    exactly that value: worker attachment, the lookup gate in both backends,
    the save-on-exit snapshot, the browser dialog and the status-bar indicator.
    The TM half of "Mine Official Terminology" was dead on arrival. Both indexes
    now count, via max() rather than sum() because load() fills both maps for one
    logical entry.

  • ClaudeTranslationWorker called TranslationMemory.get(), which does not exist —
    the method is get_by_id. Not inside a try/except, so it propagated to
    fut.result() and failed every string in the batch with AttributeError the
    moment any TM was loaded. It also only ever consulted string IDs, leaving
    source-keyed memories unusable on this backend; it...

Read more

v0.2.4

Choose a tag to compare

@github-actions github-actions released this 05 Jul 10:22

[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 focu...

Read more

v0.2.3

Choose a tag to compare

@github-actions github-actions released this 21 Jun 11:10

[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…...

Read more

v0.2.2

Choose a tag to compare

@github-actions github-actions released this 11 Jun 05:48

[v0.2.2] — 2026-06-11

Added

  • Add anthropic to requirements.txt to fix pyright CI

The anthropic package was used in gui/claude_client.py but missing from
requirements.txt, causing pyright to report reportMissingImports in CI.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(bd60038)

  • Add Modelfile.qc for fine-tuned Gemma 4 E4B quality-check model

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(e283b06)

  • Add protect_named_entities setting; default to not protecting proper nouns

Previously, faction/company/ship/character/location/creature/resource/system/
ui/game_term categories were only excluded when the source language was
English. For all other source languages, the term protector would replace
these names with opaque tokens, preventing the AI from translating them
(e.g. "United Colonies" could never become "Об'єднані колонії").

Changes:

  • term_protector.py: export SOFT_CATEGORIES frozenset listing all proper-
    noun/lore categories. This is the single source of truth used everywhere.
  • app_settings.py: new protect_named_entities: bool = False field.
    Config version bumped 22→23 with migration.
  • ollama_worker.py: remove the source-lang gate; instead exclude
    SOFT_CATEGORIES unless self.protect_named_entities is True.
    Added protect_named_entities param to init and update_config.
  • claude_translation_worker.py: same logic; also fixes a latent bug
    where protect() (non-existent method) was called instead of protect_text(),
    causing term protection to silently fail with AttributeError every time.
  • settings_dialog.py: new checkbox "Protect proper nouns and lore terms"
    (unchecked by default) with tooltip explaining the trade-off. Updated
    the stale info label that listed faction names as always protected.
  • main_window.py: pass protect_named_entities when constructing both
    worker types and in update_config.

Format tags, game IDs, XML/alias tokens, structural newline tokens, and
user-added custom terms are always protected regardless of this setting.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(6928b7c)

  • Add Hunspell spell-check QC check for translated strings

New gui/spell_checker.py wraps hunspell with three fallback backends:

  • hunspell pip package (C bindings, fastest)
  • spylls pip package (pure Python Hunspell)
  • hunspell CLI subprocess (no Python package needed)

Silently disables per-language when no dictionary is installed.
Fires SPELL_ERROR (warning) QC code when misspelled lowercase words are
found; skips ALL-CAPS, proper-noun-capitalised, and digit tokens to keep
false positives low in game text.

Retry hint lists the misspelled words so the model can correct them on
retranslation.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(0873e67)

  • Add NexusMods BBCode description (no markdown separators)

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(7ee671e)

  • Add config directory override in Settings → Storage

Reads from BSE_CONFIG_DIR env var or a bootstrap file at the fixed
default location, so the redirect is discoverable before the config
dir is known. UI shows the active path, browse/reset buttons, and a
restart-required warning when the value changes.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(fe63557)

  • Add cache directory override in Settings → Storage

Same bootstrap-file pattern as the config dir override. Priority:
BSE_CACHE_DIR env var → ~/.config/BethesdaModTools/.cache_dir_override
→ /mnt/ssd when mounted → config dir fallback.

UI shows the active cache path, browse/reset buttons, and restart
warning in the Storage group.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(6e2b8ff)

  • Add word checkers for DE/ES/FR/IT/PL/PT-BR languages

  • gui/_word_checker_base.py: shared WordChecker class (lazy load, thread-safe,
    handles both plain and "word count" frequency-list formats)

  • gui/{de,es,fr,it,pl,ptbr}_word_checker.py: thin wrappers per language

  • data/{german,spanish,french,italian,polish,portuguese}_words.txt: 50k-word
    frequency lists from hermitdave/FrequencyWords (MIT)

  • scripts/download_lang_dicts.py: download/refresh all 6 word lists

  • quality_checker: add _check_latin_coverage() — fires LOW_TARGET_COVERAGE
    when <20% of tokens match the expected target language; wired into check()

  • ollama_worker: preload all 6 dictionaries on worker init

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(b089218)

  • Add file association support for all supported file types

  • main.py: open a file path passed as argv[1] via QTimer.singleShot so
    double-clicking a .strings/.esp/.ba2 in a file manager launches the app
    and loads the file immediately

  • aur/bethesda-strings-editor.desktop: add %f to Exec= and declare
    MimeType= for the three new custom MIME types

  • aur/bethesda-strings-editor-mime.xml: freedesktop MIME definitions for
    application/x-bethesda-strings (.strings/.dlstrings/.ilstrings),
    application/x-bethesda-plugin (.esp/.esm/.esl, magic TES4), and
    application/x-bethesda-archive (.ba2, magic BTDX)

  • aur/PKGBUILD: install MIME XML to /usr/share/mime/packages/ so the
    shared-mime-info pacman hook runs update-mime-database automatically

  • scripts/install_file_associations.sh: helper for source-checkout users
    (xdg-mime + desktop-file-install for per-user ~/.local install)

  • Modelfile: update FROM to use blob SHA256 reference

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(11e68ba)

  • Add Modelfile and config for gemma4-opus48-st

Modelfile.gemma4-opus48: Ollama model definition for the Gemma 4 12B IT
fine-tuned on Claude Opus 4.6/4.7/4.8 reasoning distillation (GGUF at
/mnt/ssd/models/gguf/gemma4-opus48-Q4_K_M.gguf). Uses Gemma 4 chat
template, think=disabled, top_k 64 / top_p 0.95 per author recommendation.

MODEL_CONFIGS["gemma4-opus48-st"]: matching runtime config in OllamaWorker
so the app sends correct parameters when this model is selected.

Also adds gemma4-opus48-st to the default model picker suggestions.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(96a1a36)

  • Add rule 3e: translate multi-sentence bracket content (publisher notes, etc.)

The four existing categories only covered single-word tokens. Long bracket
content like publisher notes and editorial remarks had no matching rule, so
the model defaulted to preserving them in English. New category (e) explicitly
instructs: translate any bracket text that is multiple words or full sentences,
keeping the [ ] delimiters, with a concrete example from the failing book string.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(a9d2398)

  • Add skip-string-types setting (Book, Note, Dialogue, etc.)

Adds a row of checkboxes in Settings → Translation for each content
type (Book, Note, Terminal, Dialogue, Quest, UI, System). Checked
types are skipped during AI batch translation — strings are left
untranslated and marked as pending.

Implementation:

  • AppSettings.skip_string_types: list (default [], CONFIG_VERSION 24)
  • OllamaWorker / ClaudeTranslationWorker: skipped_types attribute;
    classify() called per-string; returns SKIP_SIGNAL when matched.
    Paragraph sub-requests (string_id==-1) are exempt from the check.
  • main_window.py passes skip_string_types to both workers on init.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(f6677f9)

  • Add Find & Replace to Advanced Search dialog

Adds a "Replace (Translated text only)" group below the search criteria:

  • "Replace with:" field (supports regex back-references when regex is on)
  • "Replace All" button — substitutes in all rows matching the current
    search criteria; asks for confirmation when >20 rows are affected
  • Shows "Replaced N occurrences in M rows" feedback inline

Replace uses the same pattern/flags as search (regex, case, whole-word).
Only the Translated column is modified; Original text is never touched.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(2e4e217)

  • Add error-code filter to Quality Check Results dialog

Adds an "Error code:" dropdown next to the existing severity filter.
It is populated dynamically from every issue code present in the current
reports (sorted alphabetically), so only codes that actually appear are
listed. Selecting a code hides all rows that don't contain that code.

Both filters (severity + code) are ANDed: e.g. "Errors" + "ENGLISH_LEAK"
shows only error-severity rows that have an ENGLISH_LEAK issue.

The code combo is rebuilt after Auto-Fix / retranslation refresh so
cleared codes disappear and the previous selection is preserved when
possible.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com([71bd17d](https://github.com/0xra0/bethesda-strings-editor/commit/...

Read more

v0.2.1

Choose a tag to compare

@github-actions github-actions released this 02 Jun 11:12

[v0.2.1] — 2026-06-02

Added

  • Add NexusMods automatic upload to release workflow

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(65d3b97)

  • Add NexusMods upload API support (v3 multipart flow)

  • gui/nexusmods_uploader.py: 6-step upload client (initiate → chunk PUT
    → S3 XML complete → finalise → poll → create version)

  • gui/nexusmods_upload_dialog.py: upload dialog with API key field,
    file/group-ID pickers, metadata form, progress bar, QThread worker

  • app_settings.py: nexusmods_api_key + nexusmods_file_group_id fields,
    config v20 → v21 migration

  • main_window.py: File → Upload to NexusMods… menu item

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(031177c)

  • Add NexusMods page auto-update script and updated description

  • scripts/update_nexusmods_page.py: Playwright automation that logs in,
    updates short + full description via TinyMCE API, saves, and persists
    cookies for future runs

  • resources/nexusmods_description.html: updated copy for all 11 languages,
    BA2 support, newline restoration, NexusMods upload feature

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(65cbd1b)

  • Add content-type icons to string table (Phosphor Icons, theme-aware)

  • gui/string_type_detector.py: classify strings into 7 types (Dialogue,
    Quest, Book, Note, Terminal, UI, System) using regex heuristics on
    text content + file extension; render Phosphor SVG icons on-demand via
    QSvgRenderer, cached per (type, dark_mode); colors adapt to palette

  • gui/string_table.py: insert "Kind" column (28 px) after ID; returns
    themed QIcon in DecorationRole and label in ToolTipRole; type cache
    cleared and icons re-tinted on theme change

  • main_window.py: call invalidate_type_cache() after manual and
    auto (OS) theme changes

Icon set: Phosphor Icons (MIT), phosphoricons.com
Colours: blue=dialogue, amber=quest, violet=book, slate=note,
green=terminal, orange=UI, slate=system

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(3f1f155)

  • Add NexusMods badge and link to README

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(8417fc6)

  • Add Batch Translate Folder dialog for bulk AI retranslation

New Translation → Batch Translate Folder… menu item opens a dialog that:

  • Scans a folder of binary string files (_uk.strings / .dlstrings / .ilstrings)
  • Loads matching _ru source files for source-language context
  • Auto-fixes mechanical issues (Russian chars, missing tags, whitespace)
  • AI-translates untranslated strings via Ollama (parallel workers)
  • AI-retranslates strings with Russian word leakage
  • Saves fixed binary files in place
  • Shows real-time progress and log

Designed for bulk post-fix of the 4,737 untranslated + 5,936 Russian-leak
strings remaining after the scripted fix pass.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(24547f8)

  • Add Claude AI integration: translation backend, chat assistant, quality review

Three new Claude-powered features (requires Anthropic API key):

  1. Claude translation backend

    • Select claude-haiku-4-5 / claude-sonnet-4-6 / claude-opus-4-7 in Preferences
    • ClaudeTranslationWorker mirrors OllamaWorker signals exactly; all existing
      progress, caching, term-protection, and glossary plumbing works unchanged
    • Parallel API calls via ThreadPoolExecutor (default 5 workers)
  2. Claude AI Assistant chat panel (Ctrl+Shift+A)

    • Dockable panel (right side); auto-updates context when a string is selected
    • Full multi-turn conversation with Claude about the current string
    • "Review Translation" — structured quality review with issues + rating
    • "Suggest Translation" — asks Claude to translate the source string
    • "Use as Translation" — applies Claude's last code-block suggestion to the table
    • API key stored in existing SecretStore (encrypted, per-machine)
    • Ctrl+Enter to send; model selector (Haiku / Sonnet / Opus)
  3. Claude menu

    • Claude AI → Show AI Assistant (Ctrl+Shift+A)
    • Claude AI → Review Current Translation (Ctrl+Shift+R)
    • Claude AI → Suggest Translation (Ctrl+Shift+T)

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(780e694)

  • Add QC training dataset generator for Gemma fine-tuning

scripts/create_qc_dataset.py — scans EN→UK Starfield string pairs,
runs QualityChecker on each, and emits ShareGPT JSONL with structured
quality assessment examples (VERDICT/CODES/SEVERITY/DETAILS/ACTION format).

Eleven synthetic injectors (missing_tag, extra_tag, russian_leak,
ai_artifact, repetition, empty, untranslated, truncated,
suspiciously_short, case_mismatch, missing_newlines) ensure full coverage
of all QC issue codes even when real data has few examples of a code.

Generated qc_dataset_sharegpt.jsonl: 14,928 examples
GOOD: 5,000 | ISSUES_FOUND: 9,928 across 16+ issue codes.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(0547e80)

  • Add icons to all menu actions and extend toolbar

Sets QIcon.fromTheme() icons on every menu action (File, Edit,
Translation, Glossary, Claude AI, Settings, Help) so menus are no
longer bare text. Also adds Approve, Reject, Next Untranslated,
Edit Glossary, and Show AI Assistant buttons to the toolbar so the
most-used review actions are one click away.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(b979d57)

  • Add Gemma 4 4B Modelfile and register in OllamaWorker

Modelfile.gemma4 points to the Unsloth Studio Q4_K_M GGUF export of
gemma-4-e4b-it. Adds "translategemma4-st" to OllamaWorker.MODEL_CONFIGS
with the same parameters as translategemma3-st so the app can use it
immediately after: ollama create translategemma4-st -f Modelfile.gemma4

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(5ebcd5a)

Changed

  • Update UI translations: de/fr/es/pl/cs complete, uk partial

de_DE, fr_FR, es_ES, pl_PL, cs_CZ: 844 strings fully translated.
uk_UA: 422 strings translated, 422 new strings pending (added by lupdate).

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(25f5a8a)

Fixed

  • Fix secrets context error in NexusMods upload job

secrets context is not available in job-level if conditions; move the
API key check to step level using an env var instead.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(9e224b4)

  • Fix Ctrl+Shift+A shortcut conflict breaking both actions

Translate All and Claude panel both claimed Ctrl+Shift+A, silencing
both shortcuts. Reassigned Claude panel to Ctrl+Shift+C.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(f5da782)

  • Fix encoding detection: English UTF-8 files no longer mis-detected as windows-1252

Two changes:

  • Count 3-byte UTF-8 sequences (0xE0-0xEF lead) in addition to 2-byte ones, so
    English smart quotes / em-dashes (U+201x, U+2014 etc.) raise UTF-8 confidence
    instead of leaving confirmed_pairs at zero.
  • Add step 5: if UTF-8 strict decode fails but < 1% of decoded chars are
    replacement chars, the content is overwhelmingly ASCII — return UTF-8 rather
    than falling through to the CP1252 fallback. This handles files that are
    effectively UTF-8 but contain a handful of stray CP1252 bytes.
    Russian/Ukrainian UTF-8 detection is unaffected (Cyrillic pair check in step 2).

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(ebfaa63)

  • Fix ruff lint errors: remove unused imports and local re-imports

  • scripts/update_translations.py: remove unused import json

  • gui/claude_translation_worker.py: remove unused Optional from typing import

  • gui/claude_chat_panel.py: remove unused top-level QApplication import;
    remove local from PySide6.QtGui import QTextCursor re-imports from
    _begin_claude_stream / _on_token / _on_reply (F811 redefinition of the
    top-level import on line 19)

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(0e47470)

  • Fix NexusMods upload steps being skipped due to env secret check

GitHub Actions does not reliably expose job-level env vars set from secrets
to step if: expressions — env.NEXUSMODS_API_KEY != '' always evaluated false,
silently skipping both upload steps. Remove the per-step guards; the job-level
condition (non-prerelease tag) is sufficient.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(a72fc2d)

  • Fix NexusMods workflow: use vars. context instead of secrets.

The API key and file group IDs were stored as repository Variables
(Settings → Secrets and variables → Variables tab), not as...

Read more

v0.2.0 — Multi-language support & BA2 archives

Choose a tag to compare

@0xra0 0xra0 released this 27 May 09:45

[v0.2.0] — 2026-05-27

Added

  • Add BA2 archive support and fix English→Ukrainian translation skip bug

BA2 archive support (Fallout 4 / Starfield):

  • bethesda_strings/ba2_handler.py: pure-Python BA2File class for GNRL
    archives; zlib compression; FO4 v1 + Starfield v2 header variants;
    safe in-place save via temp-file-then-move
  • gui/ba2_picker_dialog.py: picker dialog when archive has multiple
    strings files
  • core.py: add BethesdaStringFile.get_bytes() for in-memory serialization
  • main_window.py: .ba2 added to drop zones and file filters; open/save
    BA2 with full repack; BA2 file handle closed on new open and exit

Fix: English→Ukrainian translation skipped all strings (ESM/BA2)

  • _INPUT_NOTRANS_RE included '^[^Ѐ-ӿ]+$' (no Cyrillic = skip), which
    matched every English string and blocked EN→UK translation entirely
  • Moved that pattern to _INPUT_NOTRANS_NOCYRILLIC_RE and apply it only
    when source_lang != 'English' at both call sites in OllamaWorker
  • Also fixed path regex (was ^\w*[\/]\w, only matched 3-char strings)
    to ^\w.[/\]..\w+$ which correctly fullmatches long paths

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(415a399)

  • Add all 9 official Starfield languages to source/target selectors

Starfield ships with: English, German, Spanish, French, Italian, Japanese,
Polish, Portuguese (Brazil), Chinese (Simplified) — plus Russian and Ukrainian
for xTranslator workflows.

Changes:

  • main_window.py: SUPPORTED_LANGUAGES is now a list of (display_name, locale_code)
    tuples; combo boxes store the locale code as item data. Minimum width bumped to
    145 px to fit "Portuguese (Brazil)" / "Chinese (Simplified)". All hard-coded
    language comparisons updated from display names to locale codes (en/ru/uk/…).
  • encoding.py: ENCODING_PAIRS expanded with Starfield locale codes as aliases
    (de, es, fr, it, ja, pl, ptbr, zhhans, ru, uk). get_encodings_for_locale()
    now handles short codes, BCP-47 variants, and full display names.
  • app_settings.py: CONFIG_VERSION → 20; defaults changed to locale codes (ru/uk);
    v20 migration converts stored display names to locale codes.
  • ollama_worker.py / quality_checker.py: language comparisons updated to locale
    codes; Cyrillic-script guard accepts both "uk"/"ru" and legacy display names.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(484f9f6)

  • Add language-specific Ollama prompts for all 11 supported languages

Previously to_system_prompt() only had two hard-coded branches:
English→Ukrainian and a generic Russian→Ukrainian fallback.

Now it is fully data-driven:

_LANG_DISPLAY – locale code → display name for the "To {Language}:" prompt
_TARGET_STYLE – per-target-language style / register rules (rule #1)
_SOURCE_EXTRA – source-language notes (Russian: "don't transliterate")
_PAIR_EXTRA – extra rules for specific pairs (ru→uk Cyrillic mapping)
_LANG_EXAMPLES – one or two example translations per (src, tgt) pair
covering en→de/es/fr/it/ja/pl/ptbr/zhhans + ru→uk

to_prompt() now maps locale codes back to full display names so the
TranslateGemma English-Anchor fine-tuning format ("To Ukrainian:\n…") is
preserved for every language combination.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(356c472)

Changed

  • Update app icon to reflect multi-language support

Old icon showed "Ru → Ук" (Russian→Ukrainian only).
New icon shows "EN →" with a 2×2 grid of language chips (DE/FR/УК/JA)
hinting at the full set of 11 supported languages, against the same
NASApunk starfield background. All three formats regenerated:
app_icon.png (512 px), app_icon_64.png, app_icon.ico (multi-size).

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(e478481)

  • Update NexusMods header to reflect multi-language support

Replaces old "Russian → Ukrainian" subtitle with an "EN →" row of
coloured language chips: DE ES FR IT JA PL PT-BR ZH RU UK.
Feature tag row updated: added .ilstrings and BA2.
Background keeps the NASApunk starfield + hex-grid aesthetic.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(ab031a8)

  • Update README and GitHub description for multi-language support

  • Drop "Russian → Ukrainian only" framing throughout

  • Add supported-languages table (all 11 codes with display names)

  • Document language-pair prompts, newline/spacing restoration,
    mixed-script repair, BA2 support, and ShareGPT dataset script

  • Add nexusmods_header.png banner at the top

  • Update encoding section (CP1250 Polish, GBK Chinese)

  • Update project structure (ba2_handler.py, extract_sharegpt_dataset.py,
    app_settings v20)

  • GitHub description and topics updated via API

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(4d47325)

Fixed

  • Fix token leak, mixed-script repair, and quality checker tag detection

  • term_protector: _normalize_tokens now handles uppercase hex hash,
    space-as-underscore separator, and Cyrillic Т/К homoglyphs in token
    prefix; adds hash+index fallback key for fully garbled prefixes

  • term_protector: _merge_whitespace no longer falls back to source-language
    text when translated slot is empty (was injecting English into Ukrainian)

  • term_protector: restore_text skips template approach when model drops all
    tokens, avoiding mixed-language output; MISSING_TAG QC flags the issue

  • ollama_worker: add _fix_mixed_script() to convert stray Latin letters
    inside predominantly-Cyrillic words (e.g. "dослідницький" → "дослідницький")

  • quality_checker: fix double-counting via span deduplication;
    add [M]/[F]/[N] single-char bracket tags and [tk_...] token detection

  • scripts: add extract_sharegpt_dataset.py for EN→UK ShareGPT JSONL export

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(70fa99d)

Other

  • Restore dropped newlines and per-line leading spaces after translation

translategemma3-st silently drops [[STRUCT_BREAK_SGL_N]] / [[STRUCT_BREAK_DBL_N]]
tokens, collapsing multi-line strings into a single flat paragraph.

Add _restore_line_structure(): after restore_text() detects the newline
count is lower than the original, the function:

  1. Splits the original on (\n\n+|\n) to capture the exact delimiter sequence.
  2. Flattens the translated text and proportionally splits it into N segments
    using right-first word-boundary snapping (ensures cut lands after the
    segment's last word, not before the first word of the next segment).
  3. Copies per-line leading whitespace from the corresponding original line.
  4. Rejoins with the original \n / \n\n delimiters.

Empty trailing segments (strings ending with \n) are preserved unchanged.
The function is a no-op when the translated text already has the correct
newline count or the original has no newlines at all.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(0369ac1)

  • Release v0.2.0: multi-language support, BA2, newline restoration

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(0065239)

v0.1.1

Choose a tag to compare

@github-actions github-actions released this 20 May 10:26

[v0.1.1] — 2026-05-20

Added

  • Add ruff/Pyright static analysis; fix all lint errors

Tooling:

  • pyproject.toml: ruff (E/W/F rules, E741/E731/E501 ignored) + mypy config
  • .pre-commit-config.yaml: ruff linter + formatter on every commit
  • .github/workflows/lint.yml: ruff + Pyright on every PR and main push

Fixes applied:

  • ruff --fix: 44 auto-fixed issues (33 unused imports, 9 empty f-strings,
    2 redefined-while-unused across the codebase)
  • ruff --unsafe-fixes: 4 W293 trailing whitespace in docstrings
    (bethesda_strings/encoding.py)
  • F841 dead code: removed unused qs, has_errors, translated_count
    variables in app_settings.py and main_window.py
  • Pyright reportOptionalMemberAccess: narrowed QApplication.instance()
    result before calling .styleHints() in main_window.py; refactored
    _auto_fix_selected in quality_dialog.py to use local variables so
    Pyright can track the not-None guarantee from the early-return guard

Result: ruff clean (0 errors), Pyright clean (0 errors, 0 warnings).

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(d262449)

  • Add test CI, structured changelog, version injection, AUR package

Test workflow (.github/workflows/test.yml):

  • Runs pytest on ubuntu-22.04 and windows-latest on every PR and main push
  • Sets QT_QPA_PLATFORM=offscreen on Linux for headless Qt tests

Changelog (release workflow + .cliff.toml):

  • Uses git-cliff to generate a structured changelog from commit messages
  • Commit prefixes (Add/Fix/Remove/Update/Refactor) map to changelog sections
  • Release body is now the cliff-generated notes instead of GitHub's auto-notes

Version injection:

  • _version.py: dev placeholder committed to repo
  • Release workflow overwrites _version.py with the tag version before
    PyInstaller bundles the app, so the frozen binary shows the correct version
  • main.py: reads version from _version.py with ImportError fallback to "dev"

AUR package (aur/PKGBUILD):

  • bethesda-strings-editor-bin: installs the pre-built Linux zip from releases
  • Wrapper at /usr/bin/bethesda-strings-editor, desktop entry, icon
  • Self-contained (PyInstaller bundle needs no Python/Qt runtime)
  • Update sha256sums with updpkgsums before publishing to AUR

Windows code signing:

  • Placeholder in release.yml (commented out) showing the signtool invocation
  • Requires DigiCert/Sectigo EV certificate stored in GitHub Secrets

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(29caa98)

  • Add Sphinx documentation and GitHub Pages deployment

  • docs/: Sphinx project with RTD theme, autodoc for bethesda_strings/

  • docs/format-spec.rst: binary layout tables for .strings/.dlstrings/.ilstrings and SST XML

  • docs/architecture.rst: component diagram, translation pipeline, quality checks table

  • docs/contributing.rst + CONTRIBUTING.md: dev setup, release process, commit conventions

  • .github/workflows/docs.yml: build on every push; deploy to Pages on main

  • Fix RST formatting in xml_handler.py module docstring (clean sphinx-build -W)

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(41084fb)

  • Add DeepWiki badge to README

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(458d82a)

  • Add CHANGELOG.md

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(48fc995)

  • Add multi-language UI support (de, es, fr, pl, cs + RTL infrastructure)

  • Skeleton .ts files for German, Spanish, French, Polish, Czech

  • RTL layout auto-enabled for Arabic/Hebrew/Farsi/Urdu locales

  • Language selector in Settings shows all locales with native names

  • Restart-required notice appears inline on language change

  • ui_language migrated from display names to BCP-47 locale codes (v17)

  • Generic translator loader in main.py replaces Ukrainian-only branch

  • compile_translations.sh compiles all *.ts files, not just uk_UA

  • PyInstaller spec globs all *.qm at build time

  • TRANSLATING.md contributor guide + .weblate/component.yml

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(e16c30a)

  • Add accessibility: High Contrast theme, focus indicators, font size, color-blind mode

  • High Contrast theme: WCAG AAA black/white/cyan with yellow focus rings

  • Focus indicator QSS mixin applied to every theme (buttons, checkboxes,
    tabs, list/table views) — previously only QLineEdit/QComboBox/QSpinBox

  • Qt.AccessibleTextRole in StringTableModel: screen readers get
    "Translated — quality error" instead of raw symbols

  • Font size setting (0=OS default, 8-24pt); applied via QApplication.setFont()

  • Color-blind mode: blue/orange replaces green/red in status column;
    symbols (✓/⚠/✗) convey state independently of color

  • Config version 17 → 18

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(1108316)

  • Add encryption, audit logging, and security settings (v0.1.1)

  • AES-256-GCM at-rest encryption for translation cache (opt-in)

  • SecretStore: system keyring with PBKDF2/machine-key fallback

  • Append-only JSON-lines security audit log with 5 MB rotation

  • Settings → Security section: encrypt cache + audit log toggles

  • Accessibility: High Contrast theme, focus indicators, font size, color-blind mode

  • Multi-language UI: de/es/fr/pl/cs skeletons, RTL support, Weblate config

  • Glossary editor O(N²) freeze fix

  • cryptography>=43.0 + keyring>=25.0 dependencies

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(622c891)

Fixed

  • Fix CI: pyright path install, libEGL on Linux, Windows UTF-8 in tests

  • lint.yml: install pyright via pip so version:path finds it

  • test.yml: apt-get libegl1 on ubuntu-22.04 (PySide6 needs libEGL.so.1)

  • test_glossary.py: specify encoding='utf-8' for write_text/read_text
    (Windows defaults to cp1252 which can't encode Cyrillic)

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(f279d5f)

  • Fix lint: exclude scripts/ from Pyright (PIL not installed in CI)

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(62f38d2)

  • Fix glossary editor freeze: rebuild search index once, not per-entry

_clone_glossary called add_entry() N times, each triggering a full
_rebuild_search_index() — O(N²) for large glossaries. Add _rebuild=False
flag to add_entry and call _rebuild_search_index() once after bulk insert.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(3a94a58)

  • Fix git-cliff template: remote.link -> remote.github.link (v2.x)

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(9572e60)

  • Fix git-cliff template: use hardcoded repo URL instead of remote.link

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com(c4bae28)

v0.1.0

Choose a tag to compare

@github-actions github-actions released this 20 May 05:33