summaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-08-15-i18n-design.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/superpowers/specs/2026-08-15-i18n-design.md')
-rw-r--r--docs/superpowers/specs/2026-08-15-i18n-design.md161
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.