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 --- .../2026-08-03-post-0.1.0-usability-closed.md | 94 ++++++++++++++++++++++ .../plans/2026-08-03-post-0.1.0-usability.md | 44 +--------- 2 files changed, 95 insertions(+), 43 deletions(-) (limited to 'docs/superpowers/plans') diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md index e6c7840..1704a28 100644 --- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md +++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md @@ -5196,3 +5196,97 @@ The test asserts the Run item exists, that it is first, that triggering it reaches the query, and that the edit actions survived beside it. Restoring the old wiring fails it on the first of those and names the Qt behaviour rather than reporting a wrong query string. + +## 22. Translatability audit and i18n wiring + +**Observed (user, 2026-08-04):** "a full check of the codebase and wiring up of +the i18n system." + +**This is a debt `CLAUDE.md` already records.** The rule that every user-facing +string must be wrapped in `tr()` was added while building query completion, and +that file states plainly that "pre-existing code has not been audited against +this rule". This item is that audit, plus the loading machinery which does not +exist at all. + +**Two halves, and they are different sizes.** + +- *The audit.* Every user-visible string in `src/` checked for `tr()`, with the + translation context correct: a string in a free function needs + `Q_DECLARE_TR_FUNCTIONS`, since calling `QObject::tr()` compiles but files it + under the wrong context. `lupdate` output is the evidence here, not reading. +- *The wiring.* Nothing loads a `.qm` file today: there is no `QTranslator` in + `main.cpp`, no `.ts` files in the tree, and no CMake rule to build or install + them. Until that exists, a translated string has nowhere to come from. + +**Constraint:** query syntax is not user-facing text. notmuch keywords such as +`tag:` and `date:` are wire format and must never be translated, only the prose +describing them. The completion vocabulary is exactly this trap: the values are +literal, the descriptions are prose. + +**Verification:** run `lupdate` and read the generated `.ts`. A string that +does not appear there is not translatable, whatever the source looks like. + +**No longer on demand.** The entry said to do this when a defect needs it, not +before (user, 2026-08-04). Item 66 needs it now, so the deliverable is a RED +reproduction of that defect, not fixture wiring on its own. Fixing 66 is +deliberately excluded: it has never been isolated, and designing a fix beside a +hypothesis is how a wrong one gets locked in. + +**Smaller than this entry has read since 2026-08-04.** No hook has to be added +to `MainWindow`. `wireWorker()` (`src/mainwindow.cpp:1440`) already builds the +worker from `m_config.notmuchConfig()`, an ordinary config key, so a test that +writes a `qtmaildir.conf` pointing at the fixture gets a real worker through the +shipping path with nothing in `src/` changed. Mind the `[general]` prefix trap +when writing that file. + +**Done 2026-08-15, unreleased.** Both halves shipped: the audit and the wiring, +plus an Italian translation of all 355 strings. See +`specs/2026-08-15-i18n-design.md`. + +**What the audit found, and it was not what this entry predicted.** The `tr()` +discipline was largely holding: `lupdate` extracted 327 strings across 13 +contexts before any change. The real find was eight strings that could never be +translated in any language. `kFields[]` in `src/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 entire vocabulary of the rule +builder, invisible to every translation file that could ever exist. The source +compiles, reads correctly, and only `lupdate` reveals it, exactly as this +entry's verification note said. + +**The fix this entry recommended is the wrong one, measured.** The entry says a +string in a free function needs `Q_DECLARE_TR_FUNCTIONS`, which is true for the +case `querycompleter.cpp` solves and false here: a class carrying that macro +beside the array still extracts 0 strings, because `lupdate` needs the context +attached to the LITERAL, not to a neighbouring class. `QT_TRANSLATE_NOOP("TagRulesDialog", ...)` +is what works, verified extracting 8 of 8, and it names the context the reading +`tr()` already resolves against so the use site needed no change. + +**Twenty warnings were not translatable either**, seventeen in `config.cpp` and +three in `keymap.cpp`, all user-facing through the status label and the +"Configuration problems" modal. `Config` already had the macro; `KeyMap` needed +it. + +**`main.cpp`'s `--help` and `--version` stay bare `printf` deliberately.** They +run before `QApplication` exists, so no translator could serve them. + +**Wiring.** A `QTranslator` on `main`'s stack, which is load-bearing: one scoped +to a helper unloads on return and silently reverts every string. Installed +before `Config` loads, since config warnings are generated at load time and are +among the translated strings. `QLocale()` reads the environment, so +`LANG=it_IT.UTF-8` selects the file with no config key of this project's own, +and a missing `.qm` returns false and runs English. `LinguistTools` is optional +in CMake: a build without it is English-only rather than broken. + +**Verified**, per this entry's own standard that `lupdate` output is the +evidence: 355 strings extracted with zero context warnings (was 327 with eight), +`lrelease` reporting 355 finished and 0 unfinished, and the built `.qm` loaded +in a standalone probe printing `From -> Da`, `&Archive -> &Archivia`, and both +Italian plural forms. `tests/test_translations.cpp` guards it and was mutation +checked twice: it fails on an emptied translation, and it fails naming the +defect when `QT_TRANSLATE_NOOP` is reverted to `QT_TR_NOOP`. + +**Not done:** no language-selection UI, and no second language. `QLocale()` +reading the environment is how a Linux desktop already chooses; add a selector +when there is something to choose between. diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md index c02de31..e921e89 100644 --- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md +++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md @@ -86,7 +86,7 @@ taking that too literally. | 19 | No prompt to sync on exit when edits are pending | behavior | S | **done** | | 20 | Thread view does not match the user's mental model | presentation | L | **done** 2026-08-10, as the card list; see 53 | | 21 | Default shortcuts are not sensible enough | discoverability | S | open | -| 22 | Translatability audit and i18n wiring | correctness | M | open | +| 22 | Translatability audit and i18n wiring | correctness | M | **done** 2026-08-15, unreleased; see `specs/2026-08-15-i18n-design.md`. Found eight rule-builder labels that could never be translated in any language, and twenty untranslatable warnings. Ships an Italian translation of all 355 strings | | 23 | No way to save a search query from the UI | workflow | M | **done** 2026-08-13, shipped in 0.18.0; see `specs/2026-08-13-saved-queries-design.md` | | 24 | No right-click actions on the thread list | discoverability | S | **done** | | 25 | No select-all, and bulk actions are undiscoverable | workflow | S | **done** | @@ -194,48 +194,6 @@ that appeared to be bound. See `KeyMap::defaultBindings()` and the query bar claims it back while focused, so a proposal that moves it must not resurrect that bug. -## 22. Translatability audit and i18n wiring - -**Observed (user, 2026-08-04):** "a full check of the codebase and wiring up of -the i18n system." - -**This is a debt `CLAUDE.md` already records.** The rule that every user-facing -string must be wrapped in `tr()` was added while building query completion, and -that file states plainly that "pre-existing code has not been audited against -this rule". This item is that audit, plus the loading machinery which does not -exist at all. - -**Two halves, and they are different sizes.** - -- *The audit.* Every user-visible string in `src/` checked for `tr()`, with the - translation context correct: a string in a free function needs - `Q_DECLARE_TR_FUNCTIONS`, since calling `QObject::tr()` compiles but files it - under the wrong context. `lupdate` output is the evidence here, not reading. -- *The wiring.* Nothing loads a `.qm` file today: there is no `QTranslator` in - `main.cpp`, no `.ts` files in the tree, and no CMake rule to build or install - them. Until that exists, a translated string has nowhere to come from. - -**Constraint:** query syntax is not user-facing text. notmuch keywords such as -`tag:` and `date:` are wire format and must never be translated, only the prose -describing them. The completion vocabulary is exactly this trap: the values are -literal, the descriptions are prose. - -**Verification:** run `lupdate` and read the generated `.ts`. A string that -does not appear there is not translatable, whatever the source looks like. - -**No longer on demand.** The entry said to do this when a defect needs it, not -before (user, 2026-08-04). Item 66 needs it now, so the deliverable is a RED -reproduction of that defect, not fixture wiring on its own. Fixing 66 is -deliberately excluded: it has never been isolated, and designing a fix beside a -hypothesis is how a wrong one gets locked in. - -**Smaller than this entry has read since 2026-08-04.** No hook has to be added -to `MainWindow`. `wireWorker()` (`src/mainwindow.cpp:1440`) already builds the -worker from `m_config.notmuchConfig()`, an ordinary config key, so a test that -writes a `qtmaildir.conf` pointing at the fixture gets a real worker through the -shipping path with nothing in `src/` changed. Mind the `[general]` prefix trap -when writing that file. - ## 40. No live filter over the current view **Observed (user, 2026-08-05):** "search in current view", spelled out as two -- cgit v1.2.3