1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
|
# 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.
|