Skip to content

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

Closed

Description

@smb

Summary

MatteLoop has no internationalisation at all. There is not a single tr(),
QCoreApplication.translate() or QTranslator in src/ — every user-visible
string is a bare literal at its point of use. The interface is English only
because most of the literals happen to be English.

That is not a theoretical problem. #47 had to translate six German strings that
had been sitting in the execution-provider feature: a form label, an accessible
name, a state-invalidation reason shown to the user, a provider suffix, and a
CPU-fallback notice. None of them were caught by a test, a linter or a review.
They were found 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.

Goal

English as the primary language, German as the second. A user should be able to
run the application in either, and a contributor should not be able to add a
user-visible string without it being obvious that it needs a translation.

Recommended approach: Qt's own translation system

Use tr() / QCoreApplication.translate() with .ts sources and compiled
.qm catalogues, loaded through QTranslator at startup.

It costs no new dependency. PySide6 already ships the whole toolchain — the
installed package contains lupdate, lrelease and Linguist.app. That
matters in this repository, where every third-party component has to be tracked
through THIRD_PARTY_NOTICES.md and the packaging pipeline.

It is also the system the framework expects: context-aware lookups, plural
forms, and disambiguation are built in, and .qm files load with no
application code beyond installing the translator.

Alternative considered: the standard library's gettext, with Babel for
extraction. Rejected — it duplicates machinery Qt already provides, needs a new
dependency for extraction, and integrates worse with Qt widgets.

Scope

  1. Wrap user-visible strings. Every literal that reaches a user: labels,
    buttons, dialog text, tooltips, accessible names, status and error copy.
    Strings that are not user-visible — log lines, error codes, telemetry
    reasons — stay untranslated and it should be clear which is which. Note that
    core/parameters.py currently passes a user-visible label as a plain
    invalidation reason string, so the boundary is not currently drawn anywhere.
  2. Toolchain. A documented command to regenerate .ts files with
    lupdate, and to compile .qm with lrelease. Both checked into the
    repository so a build does not depend on a translator being run.
  3. Packaging. The .qm catalogues have to ship inside the native bundles.
    The packaging tests assert on what lands in a build, so this needs to be
    wired in deliberately rather than discovered later.
  4. Language selection. A preference, defaulting to the system language when
    a catalogue exists for it and to English otherwise.
  5. A German catalogue that is actually complete, not a stub.

Number and date formatting needs a decision at the same time

Independently of the strings, the application formats numbers with QLocale's
system default while the interface is English. On a German system the inspector
reads 6,042 s, 0,000 MiB and 0,00 % inside otherwise English labels. The
README screenshots pin the locale to English to avoid showing this.

Decide the policy as part of this work: either formatting follows the selected
interface language, or it follows the system locale independently. Either is
defensible; the current state is neither, it is an accident.

CLAUDE.md has to change with it

A translation layer decays the moment a contributor adds an English string and
stops. CLAUDE.md is the authority document for how work is done here, and it
needs a rule stating that a change adding or altering user-visible copy also
updates the translation catalogues, and that a string reaching a user without
going through the translation function is an incomplete change — the same way a
fix: without a Trigger: line is.

This should land in the same pull request as the mechanism, not afterwards. A
rule pointing at machinery that does not exist yet is worse than no rule.

Acceptance criteria

  1. No user-visible string in src/ reaches a widget without going through the
    translation function.
  2. Running with the language set to German shows a fully German interface, with
    no English fallback in normal use.
  3. Running with the language set to English shows a fully English interface.
  4. .ts and .qm files are checked in, and the command to regenerate them is
    documented.
  5. The .qm catalogues are present inside a native build.
  6. CLAUDE.md carries the rule described above.
  7. The number-formatting policy is decided, documented and implemented
    consistently.
  8. uv run ruff check . && uv run mypy src && QT_QPA_PLATFORM=offscreen uv run pytest -q still green.

Not in scope

  • Any language beyond English and German.
  • Translating docs/, the README, or THIRD_PARTY_NOTICES.md.
  • Changing what any string says, beyond what a correct translation requires.

Activity

  1. smb commented on Sep 5, 2026

    @smb
    CollaboratorAuthor

    The language switch needs a surface, and the application currently has none —
    no menu bar, no toolbar, no QAction anywhere in src/matteloop/ui/. Filed as
    #50, which introduces the menu bar and a Preferences dialog placed natively
    through PreferencesRole.

    This issue depends on #50 for the language selector in acceptance criterion 4.
    The number-formatting policy decided here also belongs in that dialog if it
    becomes a user-facing choice rather than a fixed rule.

    Refs #50.

  2. self-assigned this
    on Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions