diff options
Diffstat (limited to 'docs/superpowers/specs')
| -rw-r--r-- | docs/superpowers/specs/2026-08-15-i18n-design.md | 161 |
1 files changed, 161 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-08-15-i18n-design.md b/docs/superpowers/specs/2026-08-15-i18n-design.md new file mode 100644 index 0000000..105dca8 --- /dev/null +++ b/docs/superpowers/specs/2026-08-15-i18n-design.md @@ -0,0 +1,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. |
