From 5a3f827a01d1902a0dadc5debb4200138d4af885 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Sat, 15 Aug 2026 12:37:41 +0200 Subject: feat(i18n): wire translations and ship an Italian one (item 22) Nothing loaded a translation before this: no QTranslator, no .ts file and no build rule, so every string was English whatever the locale said. The language now comes from the environment, LANG=it_IT.UTF-8, and any other locale runs in English as before. The audit found that the tr() discipline was largely holding, and found eight strings that could never be translated into any language. kFields[] in tagrulesdialog.cpp declared the rule-builder field labels with QT_TR_NOOP inside an anonymous namespace, where lupdate reports "tr() cannot be called without context" and extracts nothing, while the use site calls TagRulesDialog::tr() on them at runtime. From, To, Cc, Subject, Tag, Folder, Attachment and Date: the whole vocabulary of the rule builder, absent from every translation file that could ever exist. The source compiles and reads correctly; only lupdate reveals it. Q_DECLARE_TR_FUNCTIONS is not the fix for that case, though it is the fix for a free function calling tr(). Measured against lupdate: a class carrying the macro beside the array still extracts 0 strings, because the context must be attached to the literal itself. QT_TRANSLATE_NOOP names it explicitly and matches the tr() that already reads them, so the use site needed no change. Twenty configuration and keybinding warnings were not translatable either. They are user-facing, reaching the status label and the "Configuration problems" dialog. Config already had the tr() macro; KeyMap needed it. Translating the filter labels then broke startup_query, found in hand testing: a filter's name is a translated label, so `startup_query = Inbox` matched nothing where the filter shows as "In arrivo". The application opened a different view and reported the user's own working config as invalid. Resolution matches the generator as well now, which is stored in queries.json and identical in every locale; the translated name still works. The regression test installs a real QTranslator rather than a stub, since the bug lives in the gap between the stored string and the displayed one, and it writes a queries.json because the warning it asserts on is guarded by a non-empty saved-query list: without one the branch never runs and the test passes against a broken check. main.cpp's --help and --version stay bare printf, as they run before QApplication exists and no translator could serve them. Verified per the backlog's own standard, that lupdate output is the evidence rather than reading: 355 strings extracted with zero context warnings, where before there were 327 with eight; lrelease reporting 355 finished and 0 unfinished; the built .qm loaded in a standalone probe printing "From -> Da" and both Italian plural forms; and the install rule placing it where main.cpp looks. test_translations guards it and was mutation checked, failing on an emptied translation and naming the defect when QT_TRANSLATE_NOOP is reverted. Co-Authored-By: Claude Opus 5 --- docs/superpowers/specs/2026-08-15-i18n-design.md | 161 +++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-15-i18n-design.md (limited to 'docs/superpowers/specs') 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 -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. -- cgit v1.2.3