summaryrefslogtreecommitdiffstats
path: root/CLAUDE.md
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-15 12:37:41 +0200
committerDanilo M. <danix@danix.xyz>2026-08-15 12:37:41 +0200
commit5a3f827a01d1902a0dadc5debb4200138d4af885 (patch)
tree5b00207aefccf625a54ac4b928a05561d1a0fff1 /CLAUDE.md
parent8e7208dc771bc9344f583442ad58cdd5b4178233 (diff)
downloadqtmaildir-5a3f827a01d1902a0dadc5debb4200138d4af885.tar.gz
qtmaildir-5a3f827a01d1902a0dadc5debb4200138d4af885.zip
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 <noreply@anthropic.com>
Diffstat (limited to 'CLAUDE.md')
-rw-r--r--CLAUDE.md49
1 files changed, 48 insertions, 1 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index 030083d..249b39b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -241,7 +241,54 @@ and quit on a sync that never happened.
that are only ever shown in passing: status bar messages, tooltips, dialog prose,
completion descriptions. Query syntax itself is not user-facing text — notmuch keywords
like `tag:` and `date:` are wire format and must never be translated, only the prose
-describing them. Pre-existing code has not been audited against this rule.
+describing them. The tree was audited against this rule by item 22 on
+2026-08-15, and an Italian translation ships, so a new string that misses
+`tr()` is now a regression rather than pre-existing debt.
+
+**`tr()` alone does not make a string translatable, and the source cannot tell
+you which.** A literal in an ARRAY or any other place with no enclosing class
+needs the context named on the literal itself:
+`QT_TRANSLATE_NOOP("TheClass", "Text")`. `QT_TR_NOOP` there compiles, reads
+correctly, and extracts NOTHING — `lupdate` prints "tr() cannot be called
+without context" and skips it, while the use site's `tr()` looks it up at
+runtime under a context no `.ts` file contains. That shipped for the eight
+rule-builder field labels in `tagrulesdialog.cpp` and made every one of them
+permanently untranslatable in any language.
+
+`Q_DECLARE_TR_FUNCTIONS` is NOT the fix for that case, though it is the fix for
+a free FUNCTION calling `tr()` (which is what `querycompleter.cpp` uses it
+for). Measured: a class carrying the macro beside the array still extracts 0.
+The context must be on the literal.
+
+**`lupdate` output is the evidence, never reading**, and `ctest -R
+translations` encodes it: it fails on a string with no translation and on one
+`lupdate` cannot see. Refresh with `lupdate-qt6 src/ -ts
+translations/qtmaildir_it_IT.ts -no-obsolete -locations none` after changing
+any user-facing string; a clean run reports zero context warnings, and
+`lrelease` must report 0 unfinished, since it silently DROPS an unfinished
+string and ships it as English inside an otherwise Italian UI.
+
+A `QTranslator` must live on `main`'s stack: one scoped to a helper function
+unloads on return and every string reverts to English with nothing to see.
+
+**Translating a string that something MATCHES on breaks config in a language
+the author never runs.** The built-in filters' names are labels and are
+translated; `startup_query` resolved by comparing the config's text against
+those names, so `startup_query = Inbox` matched nothing under `LANG=it_IT`
+where the filter is called "In arrivo". The application opened the wrong view
+AND warned that the user's own working config was invalid. It resolves on the
+GENERATOR as well now, which is stored in queries.json and identical in every
+locale. Before wrapping a string in `tr()`, ask whether anything compares
+against it; if so, match on the wire-format identifier and treat the
+translated name as an additional convenience, never as the identity. The
+regression test installs a real `QTranslator` rather than a stub, because the
+bug lives in the gap between the stored string and the displayed one and only
+a real translation opens it.
+
+A second trap sits under that test and cost a wrong green: the warning it
+asserts on is guarded by `!m_savedQueries.isEmpty()`, so a test with no
+`queries.json` never reaches the branch and passes against a broken check. It
+writes one, and asserts the file loaded before asserting on what it produced.
**This application has a sibling, and one file couples them.** `mailctl`
(`../mailctl`) is a narrow, agent-safe CLI over the same notmuch index. The two