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 --- tests/test_config.cpp | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) (limited to 'tests/test_config.cpp') diff --git a/tests/test_config.cpp b/tests/test_config.cpp index e2dffb0..00e8d89 100644 --- a/tests/test_config.cpp +++ b/tests/test_config.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -99,6 +100,7 @@ private slots: void theStartupAccountIsReadAndValidated(); void theStartupAccountTakesTheKeyNotTheSyncChannel(); void theStartupQueryCanNameABuiltinFilter(); + void theStartupQuerySurvivesATranslatedFilterName(); void theStartupQueryPrefersASavedQueryOverAFilterOfTheSameName(); void anUnmatchedStartupQueryFallsBackToAFilterNotAStrayQuery(); void theFlaggedFilterIsCalledImportant(); @@ -1175,6 +1177,91 @@ void TestConfig::theStartupQueryCanNameABuiltinFilter() QStringLiteral("tag:inbox")); } +void TestConfig::theStartupQuerySurvivesATranslatedFilterName() +{ + // Reported by the user running the 0.23.0 Italian translation: the app + // started on the wrong view and said + // + // La ricerca iniziale 'Inbox' non è una ricerca salvata; verrà aperta + // 'Non letti'. + // + // A filter's NAME is a translated label, so `startup_query = Inbox` matched + // nothing once the Inbox filter was called "In arrivo": a config file that + // had always worked broke because the UI language changed, and the warning + // named the user's own correct config as the fault. + // + // The fix matches the GENERATOR too, which is stored in queries.json and + // identical in every locale. Uses a real QTranslator rather than a stub, + // because the bug lives in the gap between the stored string and the + // displayed one, and only an actual translation opens that gap. + QTranslator translator; + const QString qm = QStringLiteral(TRANSLATIONS_QM); + QVERIFY2(QFile::exists(qm), + qPrintable(QStringLiteral("no compiled translation at %1").arg(qm))); + QVERIFY2(translator.load(qm), "the Italian translation failed to load"); + QVERIFY(qApp->installTranslator(&translator)); + + // Proves the translator is actually in effect. Without this the test passes + // when the translation silently fails to load, asserting nothing: the names + // stay English and every comparison below succeeds for the wrong reason. + const SavedQuery inbox = Config::builtinFilter(QStringLiteral("inbox")); + QCOMPARE(inbox.name, QStringLiteral("In arrivo")); + + QTemporaryDir dir; + // A saved query has to exist for the warning to be reachable at all: the + // check is guarded by !m_savedQueries.isEmpty(). Without this file the + // branch never runs, and an assertion that no problem was reported passes + // against a broken check by never reaching it. Measured: with no + // queries.json, reverting the fix left this test green. + { + QFile queries(dir.filePath(QStringLiteral("queries.json"))); + QVERIFY(queries.open(QIODevice::WriteOnly)); + queries.write(QStringLiteral(R"({ + "version": 1, + "queries": [ { "name": "Mine", "query": "tag:mine" } ] + })").toUtf8()); + } + + Config config; + config.load(writeIni(dir, QStringLiteral( + "[general]\n" + "startup_query=Inbox\n" + "\n" + "[account.work]\n" + "maildir=work\n" + "sent=Sent\n"))); + QVERIFY2(!config.savedQueries().isEmpty(), + "queries.json did not load, so the warning path is unreachable"); + + const SavedQuery startup = config.startupSavedQuery(); + QCOMPARE(startup.generated, QStringLiteral("inbox")); + QCOMPARE(config.resolvedQuery(startup, QString()), + QStringLiteral("tag:inbox")); + + // And it must not warn about a config that is working. The user saw the + // warning as well as the wrong view, and a warning they cannot act on is + // its own defect. + QVERIFY2(config.problems().isEmpty(), + qPrintable(QStringLiteral("unexpected problem: %1") + .arg(config.problems().join(QLatin1Char(' '))))); + + // The translated name still works, since that is what a user reading their + // own Italian UI would naturally write. + Config byLabel; + byLabel.load(writeIni(dir, QStringLiteral( + "[general]\n" + "startup_query=In arrivo\n" + "\n" + "[account.work]\n" + "maildir=work\n" + "sent=Sent\n"))); + QVERIFY(!byLabel.savedQueries().isEmpty()); + QCOMPARE(byLabel.startupSavedQuery().generated, QStringLiteral("inbox")); + QVERIFY(byLabel.problems().isEmpty()); + + qApp->removeTranslator(&translator); +} + void TestConfig::theFlaggedFilterIsCalledImportant() { // Item 57 decided this and item 93 contradicted it. The `flag` ACTION has -- cgit v1.2.3