# 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 <probe> -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 `<translation>` 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.
