# Translatability audit and i18n wiring (item 22) Date: 2026-08-15 Item: 22, backlog `2026-08-03-post-0.1.0-usability.md` ## What this is Item 22 is two halves of different sizes: an **audit** of every user-facing string for `tr()` with the correct context, and the **wiring** that does not exist at all. Nothing loads a `.qm` file today: no `QTranslator` in `main.cpp`, no `.ts` in the tree, no CMake rule to build or install one. Until that exists a translated string has nowhere to come from. The user's decision, 2026-08-15: **Italian only, fully translated.** Not machinery-with-empty-strings, and not a representative slice. One language, all of it, so the machinery is proven by a translation that actually renders. ## Audit findings Measured with `lupdate-qt6 src/ -ts -no-obsolete`, not by reading. **327 source strings across 13 contexts.** The `tr()` discipline `CLAUDE.md` records is largely holding; this is an audit with three specific defects, not a rewrite. ### Defect 1: eight labels are unreachable (the real find) `src/tagrulesdialog.cpp:66-73` declares the rule-builder field labels in an anonymous namespace: ```cpp struct FieldEntry { RuleTerm::Field field; const char *label; }; const FieldEntry kFields[] = { {RuleTerm::From, QT_TR_NOOP("From")}, ... }; ``` lupdate reports `tr() cannot be called without context` eight times and **files nothing**. The use site at `:940` is `row.field->addItem(tr(entry.label), ...)`, inside `TagRulesDialog`, so at runtime Qt looks up context `TagRulesDialog` for a string no `.ts` file ever contained. The eight labels are From, To, Cc, Subject, Tag, Folder, Attachment and Date: the entire vocabulary of the rule builder. This is exactly the trap the backlog entry predicted, and it is invisible without running `lupdate`. The source looks correct. **The fix is NOT `Q_DECLARE_TR_FUNCTIONS`**, which is what `querycompleter.cpp` uses for its own namespace problem and what this design assumed first. Verified against `lupdate-qt6` in a standalone file: a class carrying `Q_DECLARE_TR_FUNCTIONS` beside the array still extracts **0 strings**, because lupdate needs the context attached to the literal itself, not to a nearby class. The mechanism that works, verified extracting 2 of 2 under context `TagRulesDialog`: ```cpp {RuleTerm::From, QT_TRANSLATE_NOOP("TagRulesDialog", "From")}, ``` It names the context explicitly and **matches the existing `tr(entry.label)` call at `:940` without changing it**, since `TagRulesDialog::tr` resolves in that same context. ### Defect 2: twenty untranslated warnings Seventeen in `src/config.cpp` (`addWarning`/`addProblem` with a bare `QStringLiteral`) and three in `src/keymap.cpp:268,274,282`. All are user-facing: `MainWindow` at `:1730` and `:1746` joins `m_config.warnings() + m_keyMap.warnings()` into the status label and the "Configuration problems" modal that `main.cpp:106` raises. `Config` already has `Q_DECLARE_TR_FUNCTIONS(Config)` at `config.h:162`, so those seventeen become `tr()` with no other change. `KeyMap` needs the macro adding. ### Defect 3: nothing, deliberately `main.cpp`'s `--help` and `--version` text stays bare `printf`. It runs before `QApplication` is constructed, by design, so no translator could be installed to serve it. Translating it would be a lie about what can happen. ### Confirmed clean, and must stay untranslated Wire format, per the constraint in `CLAUDE.md` and the backlog entry: - `keymap.cpp`'s 85 key-sequence literals (`Ctrl+G`, `Alt+Up`). - `htmlbuilder.cpp`'s markup and `@PLACEHOLDER@` tokens. - Config keys (`auto_sync_delay_ms`), notmuch query syntax (`tag:`, `from:`). - `querycompleter.cpp`'s completion **values** are literal; only the descriptions beside them are prose, and those already use `VocabularyStrings::tr`. ## The wiring ### Load path, verified Confirmed in a standalone program against Qt 6.11, printing `load=1` and `From -> Da`: ```cpp QTranslator translator; if (translator.load(QLocale(), QStringLiteral("qtmaildir"), QStringLiteral("_"), translationsDir)) app.installTranslator(&translator); ``` Placed in `main.cpp` **after** `QApplication` is constructed and **before** `Config` is loaded, since config warnings are generated at load time and are among the strings being translated. `QLocale()` default-constructs to the system locale, so `LANG=it_IT.UTF-8` selects the file with no config key. A missing `.qm` returns false and the app runs in English, which is the correct failure. **The translator must outlive `app.exec()`.** A stack `QTranslator` in `main` does; one in a helper function does not, and the strings silently revert. ### Where the .qm lives Installed to `share/qtmaildir/translations/`. `main.cpp` looks there via `QStandardPaths::AppDataLocation`, falling back to a path beside the binary so a build tree works without installing. ### CMake `find_package(Qt6 REQUIRED COMPONENTS LinguistTools)` — present on this machine as `/usr/lib64/cmake/Qt6LinguistTools`. `qt_add_translations` handles both `lupdate` and `lrelease`; the `.ts` file is tracked in git under `translations/`, the `.qm` is generated and is not. The install rule is part of this, not a follow-up. A `.qm` that is built and never installed reproduces exactly the state item 22 describes. ## Verification Per the backlog entry: **`lupdate` output is the evidence, not reading.** 1. `lupdate` runs clean. The eight `tr() cannot be called without context` warnings are gone, and the probe count rises from 327 by the twenty newly wrapped warnings. 2. The `.ts` contains context `TagRulesDialog` with the eight field labels. This is the assertion that would have failed before the fix, and the one a reading of the source cannot make. 3. `lrelease` reports 0 unfinished for `it_IT`. 4. A test asserts the shipped `.ts` has no empty `` and no `type="unfinished"`, so a later string added without a translation is caught by the suite rather than by seeing English in an Italian UI. 5. Hand test, and it belongs to the user: `LANG=it_IT.UTF-8 qtmaildir`, with the rule-builder field dropdown open, since those eight labels are the strings this item exists to fix. ## What this does not do - No language-selection UI or config key. `QLocale()` reads the environment, which is how a Linux desktop already chooses. Add one when a second language exists and there is something to choose between. - No second language. The `.ts` machinery makes one a copy of a file and a translation pass, with no code change. - No audit of `tests/`. Test strings are not user-facing.