aboutsummaryrefslogtreecommitdiffstats
path: root/src/config.cpp
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 /src/config.cpp
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 'src/config.cpp')
-rw-r--r--src/config.cpp72
1 files changed, 49 insertions, 23 deletions
diff --git a/src/config.cpp b/src/config.cpp
index 9bed74a..266ed39 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -208,14 +208,14 @@ void Config::load(const QString &path)
// first. What was missing is the report. The value parses, so
// nothing ever said the 500 in the file is not what is on screen.
if (value < MessageView::kMinZoom || value > MessageView::kMaxZoom) {
- addProblem(QStringLiteral("Message zoom %1 is outside %2 to %3; "
+ addProblem(tr("Message zoom %1 is outside %2 to %3; "
"using the nearest allowed value.")
.arg(value)
.arg(MessageView::kMinZoom)
.arg(MessageView::kMaxZoom));
}
} else {
- addProblem(QStringLiteral("Message zoom '%1' is not a number; "
+ addProblem(tr("Message zoom '%1' is not a number; "
"using the default.")
.arg(zoom.toString()));
}
@@ -235,14 +235,14 @@ void Config::load(const QString &path)
bool ok = false;
const int value = iconSize.toString().trimmed().toInt(&ok);
if (!ok) {
- addProblem(QStringLiteral("Toolbar icon size '%1' is not a number; "
+ addProblem(tr("Toolbar icon size '%1' is not a number; "
"using %2.")
.arg(iconSize.toString())
.arg(m_toolbarIconSize));
} else if (value < kMinToolbarIconSize || value > kMaxToolbarIconSize) {
m_toolbarIconSize =
qBound(kMinToolbarIconSize, value, kMaxToolbarIconSize);
- addProblem(QStringLiteral("Toolbar icon size %1 is outside %2 to "
+ addProblem(tr("Toolbar icon size %1 is outside %2 to "
"%3; using %4.")
.arg(value)
.arg(kMinToolbarIconSize)
@@ -268,7 +268,7 @@ void Config::load(const QString &path)
const QDateTime b(QDate(2019, 1, 3), QTime(4, 5));
const QLocale locale = QLocale::system();
if (locale.toString(a, dateFormat) == locale.toString(b, dateFormat)) {
- addProblem(QStringLiteral("Date format '%1' contains no date or "
+ addProblem(tr("Date format '%1' contains no date or "
"time field; using the system format.")
.arg(dateFormat));
} else {
@@ -297,7 +297,7 @@ void Config::load(const QString &path)
} else {
// Naming the accepted values, since a typo here silently changes what
// happens to unsynced work at exit.
- addProblem(QStringLiteral("Unknown sync_on_exit '%1'; expected ask, "
+ addProblem(tr("Unknown sync_on_exit '%1'; expected ask, "
"always or never. Using ask.")
.arg(syncExit));
}
@@ -309,7 +309,7 @@ void Config::load(const QString &path)
if (ok) {
m_markReadDelayMs = value;
} else {
- addProblem(QStringLiteral("Mark-read delay '%1' is not a number; "
+ addProblem(tr("Mark-read delay '%1' is not a number; "
"using the default.")
.arg(markRead.toString()));
}
@@ -326,7 +326,7 @@ void Config::load(const QString &path)
if (ok) {
m_autoSyncDelayMs = value;
} else {
- addProblem(QStringLiteral("Auto-sync delay '%1' is not a number; "
+ addProblem(tr("Auto-sync delay '%1' is not a number; "
"using the default.")
.arg(autoSync.toString()));
}
@@ -352,7 +352,7 @@ void Config::load(const QString &path)
// Skip only the bad entry: one typo must not cost the user the rest
// of the list, and the built-ins are appended to regardless.
if (value.isEmpty()) {
- addProblem(QStringLiteral("[completion] extra_mimetypes: entry '%1' "
+ addProblem(tr("[completion] extra_mimetypes: entry '%1' "
"has no mimetype; ignoring it.")
.arg(entry));
continue;
@@ -453,18 +453,29 @@ void Config::load(const QString &path)
// entry for an account that does not exist and would sit on "All accounts"
// without saying why.
if (!m_startupAccount.isEmpty() && !account(m_startupAccount).isValid()) {
- addProblem(QStringLiteral("Startup account '%1' is not a configured "
+ addProblem(tr("Startup account '%1' is not a configured "
"account; starting on all accounts.")
.arg(m_startupAccount));
m_startupAccount.clear();
}
- if (m_startupQueryWasSet && !m_savedQueries.isEmpty()
- && startupSavedQuery().name.compare(m_startupQuery,
- Qt::CaseInsensitive) != 0) {
- addProblem(QStringLiteral("Startup query '%1' is not a saved query; "
- "opening '%2' instead.")
- .arg(m_startupQuery, startupSavedQuery().name));
+ // Asks whether the resolved query matched on EITHER a name or a generator,
+ // rather than comparing the name alone. Comparing names warned about a
+ // config that was working: `startup_query = Inbox` resolves through the
+ // generator under a translated UI, where the filter's name is "In arrivo",
+ // so the app opened the right view and reported it as wrong.
+ if (m_startupQueryWasSet && !m_savedQueries.isEmpty()) {
+ const SavedQuery resolved = startupSavedQuery();
+ const bool matched =
+ resolved.name.compare(m_startupQuery, Qt::CaseInsensitive) == 0
+ || (!resolved.generated.isEmpty()
+ && resolved.generated.compare(m_startupQuery,
+ Qt::CaseInsensitive) == 0);
+ if (!matched) {
+ addProblem(tr("Startup query '%1' is not a saved query; "
+ "opening '%2' instead.")
+ .arg(m_startupQuery, resolved.name));
+ }
}
}
@@ -513,14 +524,14 @@ void Config::loadSavedQueries(const QString &configPath, QSettings &settings)
// Order is alphabetical here because childKeys() is genuinely all the
// INI knows. The user reorders once and it sticks from then on.
if (!m_savedQueries.isEmpty() && !saveSavedQueries()) {
- addProblem(QStringLiteral("Could not write saved queries to %1.")
+ addProblem(tr("Could not write saved queries to %1.")
.arg(m_queriesPath));
}
return;
}
if (!file.open(QIODevice::ReadOnly)) {
- addProblem(QStringLiteral("Could not read %1: %2.")
+ addProblem(tr("Could not read %1: %2.")
.arg(m_queriesPath, file.errorString()));
m_queriesRefused = true;
return;
@@ -532,7 +543,7 @@ void Config::loadSavedQueries(const QString &configPath, QSettings &settings)
file.close();
if (error.error != QJsonParseError::NoError || !document.isObject()) {
- addProblem(QStringLiteral("%1 is not valid JSON: %2.")
+ addProblem(tr("%1 is not valid JSON: %2.")
.arg(m_queriesPath, error.errorString()));
m_queriesRefused = true;
return;
@@ -545,7 +556,7 @@ void Config::loadSavedQueries(const QString &configPath, QSettings &settings)
// Refused rather than guessed at, and the refusal blocks the save:
// rewriting a newer document with this build's reading of it would
// destroy whatever the newer build stored.
- addProblem(QStringLiteral("%1 has format version %2; this build "
+ addProblem(tr("%1 has format version %2; this build "
"understands %3. Saved queries were not "
"loaded.")
.arg(m_queriesPath)
@@ -585,13 +596,13 @@ void Config::loadSavedQueries(const QString &configPath, QSettings &settings)
// dropping the row here would delete it from the file on the next
// save, which is the same data loss the unknown-field handling
// exists to prevent.
- addProblem(QStringLiteral("Saved query '%1' uses an unknown "
+ addProblem(tr("Saved query '%1' uses an unknown "
"generator '%2' and will find nothing.")
.arg(query.name, query.generated));
}
if (query.name.isEmpty()) {
- addProblem(QStringLiteral("A saved query in %1 has no name and was "
+ addProblem(tr("A saved query in %1 has no name and was "
"skipped.").arg(m_queriesPath));
continue;
}
@@ -813,10 +824,25 @@ SavedQuery Config::startupSavedQuery() const
// matched nothing once item 93 shipped Inbox as a filter and the duplicated
// saved query was removed, and the app started on whatever query happened
// to be first in the file.
- for (const SavedQuery &filter : builtinFilters()) {
+ //
+ // Matched on the GENERATOR as well as the name, and the generator is what
+ // makes this survive translation. A filter's name is a translated label, so
+ // under LANG=it_IT the Inbox filter is called "In arrivo" and a config
+ // reading `startup_query = Inbox` matched nothing, warned, and fell back to
+ // another filter: the user's startup view changed because the UI language
+ // did. The generator is stored in queries.json and matched against a closed
+ // set, so it is wire format and identical in every locale. Names are still
+ // tried first, so a translated name a user copied out of their own UI keeps
+ // working.
+ const QList<SavedQuery> filters = builtinFilters();
+ for (const SavedQuery &filter : filters) {
if (filter.name.compare(m_startupQuery, Qt::CaseInsensitive) == 0)
return filter;
}
+ for (const SavedQuery &filter : filters) {
+ if (filter.generated.compare(m_startupQuery, Qt::CaseInsensitive) == 0)
+ return filter;
+ }
// Named nothing that exists. Falling back to m_savedQueries.first() is what
// this used to do and it is worse than it looks: after the duplicated