Skip to content

Translate the interface, with German as the second language - #84

Merged
smb merged 2 commits into
mainfrom
feat/issue-49-translations-v2
Sep 6, 2026
Merged

smb merged 2 commits into
mainfrom
feat/issue-49-translations-v2

Conversation

@smb

@smb smb commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Closes #49.

MatteLoop was English by accident, not by design: not one tr() call anywhere. #47 had to translate six German strings that had been sitting in the execution-provider feature — caught not by a test or a review, but because a screenshot was rendered for the README and the German was visible in the picture.

The interface in German

What this does

Qt's own system — tr() / QCoreApplication.translate(), .ts sources, compiled .qm loaded through QTranslator. No new dependency; PySide6 ships lupdate and lrelease. The language is chosen in Preferences, persists immediately, and applies at the next start.

Number and date formatting follows the interface language, which is the decision the issue left open. A German machine with an English interface used to read 6,042 s inside English labels; the two can no longer disagree, because the locale changes with the language at startup and never in between.

Log lines, error codes, AppError.technical_detail and ProgressEvent payloads stay canonical English. They are data, not copy — one of them is serialised as exception text.

The catalogue is generated, never written

tests/test_translations.py runs the documented lupdate command into a temporary directory and compares the extracted messages — sources, contexts and numerus flags — against the checked catalogues. A hand-edited entry, or a plural lupdate cannot see, fails CI.

That guard is not decorative. Two earlier drafts of this change were rejected in review for exactly that: 191 hand-seeded entries the documented command could not reproduce, and a plural rendering 1 frames. Both drafts looked complete and passed their own tests.

453 messages, none unfinished, 11 plurals as Qt numerus forms.

Rendering it was a review

The German window showed Compute acceleration clipped to Berechnungsbeschleun, so the German label is now Beschleunigung. The screenshot above is the corrected render.

Guardrails

inspector.py and timeline.py rise to 897 and 842 lines. The growth is mechanical — tr() wrapping reflows long literals — and the maintainer approved these two specifically before the work began. No function crossed the 60-line budget; main_window.py stayed inside its own by extracting its render helpers into main_window_render.py.

Verification

uv run python scripts/check_guardrails.py && uv run ruff check . && uv run mypy src && QT_QPA_PLATFORM=offscreen uv run pytest -q
Guardrail check passed (24 module(s) over budget).
All checks passed!
Success: no issues found in 112 source files
1796 passed, 4 skipped, 15 warnings in 44.44s

The English screenshots are unchanged, byte for byte.

🤖 Generated with Claude Code

MatteLoop was English by accident rather than by design: not one tr() call
anywhere, every visible string a bare literal at its point of use. #47 had to
translate six German strings that had been sitting in the execution-provider
feature — found not by a test or a review but because a screenshot of the
running window was rendered for the README and the German was simply visible in
the picture. Without a translation layer there is no mechanism that can catch
the next one.

This wraps every user-visible string with Qt's own system: tr() and
QCoreApplication.translate(), .ts sources, compiled .qm catalogues, loaded
through QTranslator at startup. No new dependency — PySide6 ships lupdate and
lrelease. Log lines, error codes, AppError.technical_detail and ProgressEvent
payloads stay canonical English: they are data, not copy, and one of them is
serialised as exception text.

Number and date formatting follows the selected interface language rather than
the system locale, which is the decision the issue left open. On a German
machine an English interface used to read "6,042 s" inside otherwise English
labels; now the two never disagree. The language is chosen in Preferences,
persists immediately, and applies at the next start — including the locale, so
words and numbers always change together.

The catalogue is generated, never written. tests/test_translations.py runs the
documented lupdate command into a temporary directory and compares the
extracted messages — sources, contexts and numerus flags — against the checked
catalogues, so a hand-edited entry or a plural that lupdate cannot see fails
CI. That guard is not decorative: two earlier drafts of this change shipped
hand-seeded entries the documented command could not reproduce, and a plural
form that rendered "1 frames".

Rendering the German interface was itself a review, as it was in #43: it showed
"Compute acceleration" clipped to "Berechnungsbeschleun", so the German label is
now "Beschleunigung".

Also makes the language test independent of the host locale. Without a stored
value the dialog opens on the system language, so on a German machine switching
to German asserted on a no-op — it could only ever have passed on an English
runner.

The guardrail baselines for inspector.py and timeline.py rise to 897 and 842
lines. The growth is mechanical: tr() wrapping reflows long literals across
several lines. The maintainer approved these two before the work started; no
function crossed the 60-line budget, and main_window.py stayed inside its own by
extracting its render helpers into main_window_render.py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread src/matteloop/ui/i18n.py Outdated
Comment thread tests/test_translations.py
Trigger: the Gitar review on the pull request. install_translators read
QLibraryInfo.LibraryPath.Translations, which does not exist — the member is
TranslationsPath — so app.py raised AttributeError before the window appeared.
Every launch, on both platforms.

The whole suite was green while the application could not start, because
nothing called install_translators: it is reached only from _run_gui, and no
test runs the real startup path. A wrong enum member is exactly the kind of
mistake a type checker cannot see through getattr() and a reading review skims
past.

So the fix is one name, and the coverage is the point: one test installs the
translators for real and asserts a German string comes back, and a second
compiles both .ts files with lrelease and compares the result byte for byte
against the checked-in .qm. The catalogue tests until now proved the sources
were extractable and complete, but never that the compiled files anyone
actually ships match them — a stale .qm would have shipped the wrong words
while every review of the .ts looked correct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gitar-bot

gitar-bot Bot commented Sep 6, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 2 resolved / 2 findings

Adds comprehensive translation support with German as the second language using Qt's tr() / QTranslator system. Resolved two findings: install_translators now uses the correct QLibraryInfo enum member, and compiled .qm catalogues are verified against .ts sources by tests/test_translations.py to prevent hand-edited entries and undetectable plural errors. No issues remain.

✅ 2 resolved
✅ Bug: install_translators uses nonexistent QLibraryInfo enum member

📄 src/matteloop/ui/i18n.py:117-118 📄 src/matteloop/app.py:60
install_translators calls getattr(QLibraryInfo.LibraryPath, "Translations") with no default, but PySide6's QLibraryInfo.LibraryPath enum has no Translations member — the member is TranslationsPath (verified in the installed PySide6 6.10 stubs: only TranslationsPath = 0xa exists). The getattr therefore raises AttributeError every time it runs. Since _run_gui in app.py calls install_translators(application, language) unconditionally at startup and no test exercises this function, the GUI crashes on launch. Replace the lookup with the real enum member.

✅ Quality: Compiled .qm catalogues are never verified against .ts sources

📄 tests/test_translations.py:52-66 📄 tests/test_resources.py:54-63
The runtime loads the compiled .qm files via QTranslator, but test_translations.py only validates the .ts XML sources (against lupdate extraction and placeholder/completeness rules), and test_resources.py only checks that a .qm loads — never that its contents match the .ts. A developer who edits a translation and forgets to run pyside6-lrelease (or hand-edits a .qm) ships stale/incorrect translations that CI cannot catch, which is precisely the class of regression this PR set out to guard against. Consider adding a test that recompiles the .ts to a temp .qm and byte-compares (or message-compares) against the checked-in .qm.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@sonarqubecloud

sonarqubecloud Bot commented Sep 6, 2026

Copy link
Copy Markdown

@smb
smb merged commit fac900d into main Sep 6, 2026
10 checks passed
@smb
smb deleted the feat/issue-49-translations-v2 branch September 6, 2026 22:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The UI is English by accident, not by design: introduce translations with German as the second language

1 participant