summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/config.cpp24
-rw-r--r--src/config.h12
-rw-r--r--src/main.cpp28
-rw-r--r--tests/test_config.cpp81
4 files changed, 136 insertions, 9 deletions
diff --git a/src/config.cpp b/src/config.cpp
index 266ed39..600c558 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -196,6 +196,30 @@ void Config::load(const QString &path)
m_startupAccount =
settings.value(QStringLiteral("startup_account")).toString().trimmed();
+ // Interface language. "system" is spelled out so the default can be written
+ // down rather than only expressed by deleting the key.
+ //
+ // Validated here rather than left to whether a translation loads, because
+ // those are different questions and only one of them is an error. QLocale
+ // accepts anything and degrades an unrecognised name to C, so `language =
+ // itallian` would load no translation and look exactly like asking for
+ // English. Meanwhile `language = en_US` legitimately loads nothing, since
+ // English is the source language and ships no .qm. Checking the NAME
+ // separates the typo from the deliberate choice.
+ const QString language =
+ settings.value(QStringLiteral("language")).toString().trimmed();
+ if (!language.isEmpty()
+ && language.compare(QStringLiteral("system"), Qt::CaseInsensitive) != 0) {
+ if (QLocale(language).language() == QLocale::C) {
+ addProblem(tr("Language '%1' is not a locale name; using the "
+ "system language. Expected something like 'it' or "
+ "'it_IT'.")
+ .arg(language));
+ } else {
+ m_language = language;
+ }
+ }
+
const QVariant zoom = settings.value(QStringLiteral("message_zoom"));
if (zoom.isValid()) {
bool ok = false;
diff --git a/src/config.h b/src/config.h
index d1f8e65..70f7181 100644
--- a/src/config.h
+++ b/src/config.h
@@ -292,6 +292,17 @@ public:
/// the same fixed string on every card rather than failing visibly.
QString dateFormat() const { return m_dateFormat; }
+ /// Interface language, or empty to follow the environment.
+ ///
+ /// A locale name, short ("it") or full ("it_IT"); Qt resolves the short
+ /// form to a country. `system` reads as empty, so a user can write the
+ /// default down rather than having to delete the key to get it back.
+ ///
+ /// Validated at load, because an unrecognised name does NOT fail: QLocale
+ /// degrades it to C, which then loads no translation and is indistinguishable
+ /// from asking for English on purpose. A typo would otherwise be silent.
+ QString language() const { return m_language; }
+
/// The saved query to open at startup, by name. Falls back to "Unread"
/// when unset, and to the first saved query when no query by that name
/// exists: [queries] is read through childKeys(), which sorts
@@ -404,6 +415,7 @@ private:
int m_toolbarIconSize = 24;
QString m_notmuchConfig;
QString m_dateFormat;
+ QString m_language;
qreal m_messageZoom = 1.0;
bool m_completionOnFocus = false;
int m_markReadDelayMs = 2000;
diff --git a/src/main.cpp b/src/main.cpp
index 59f39be..2ed8057 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -89,14 +89,27 @@ int main(int argc, char *argv[])
app.setWindowIcon(QIcon(QStringLiteral(":/icons/qtmaildir.svg")));
app.setDesktopFileName(QStringLiteral("qtmaildir"));
+ Config config;
+ config.load(Config::defaultPath());
+
// On main's stack deliberately: a QTranslator must outlive exec(), and one
// scoped to a helper function unloads on return, silently reverting every
- // string to English. Installed before Config is loaded, because config
- // warnings are generated at load time and are themselves translated.
+ // string to English.
+ //
+ // Loaded AFTER Config, because [general] language overrides the
+ // environment. The cost is that config warnings are generated before the
+ // translator exists, so they are built in English; retranslating them would
+ // mean re-running load(), and a warning about the config file is the one
+ // string a user can still act on in either language.
//
- // QLocale() reads the system locale, so LANG=it_IT.UTF-8 selects the file
- // with no config key of our own. A missing .qm returns false and the app
- // runs in English, which is the correct failure rather than a fatal one.
+ // An empty language() means follow the environment, which is what QLocale()
+ // default-constructs to. A missing .qm returns false and the app runs in
+ // English: that is the correct outcome both for an unsupported language and
+ // for `language = en_US`, since English is the source and ships no .qm.
+ const QLocale locale = config.language().isEmpty()
+ ? QLocale()
+ : QLocale(config.language());
+
QTranslator translator;
QStringList translationDirs;
// Beside the binary first, so a build tree works without installing.
@@ -108,7 +121,7 @@ int main(int argc, char *argv[])
translationDirs << dir + QStringLiteral("/translations");
for (const QString &dir : std::as_const(translationDirs)) {
- if (translator.load(QLocale(), QStringLiteral("qtmaildir"),
+ if (translator.load(locale, QStringLiteral("qtmaildir"),
QStringLiteral("_"), dir)) {
app.installTranslator(&translator);
break;
@@ -122,9 +135,6 @@ int main(int argc, char *argv[])
return 1;
}
- Config config;
- config.load(Config::defaultPath());
-
MainWindow window(config);
window.show();
diff --git a/tests/test_config.cpp b/tests/test_config.cpp
index 00e8d89..0dfda86 100644
--- a/tests/test_config.cpp
+++ b/tests/test_config.cpp
@@ -101,6 +101,8 @@ private slots:
void theStartupAccountTakesTheKeyNotTheSyncChannel();
void theStartupQueryCanNameABuiltinFilter();
void theStartupQuerySurvivesATranslatedFilterName();
+ void theLanguageKeyOverridesTheEnvironment();
+ void theLanguageKeyRejectsWhatIsNotALocale();
void theStartupQueryPrefersASavedQueryOverAFilterOfTheSameName();
void anUnmatchedStartupQueryFallsBackToAFilterNotAStrayQuery();
void theFlaggedFilterIsCalledImportant();
@@ -1262,6 +1264,85 @@ void TestConfig::theStartupQuerySurvivesATranslatedFilterName()
qApp->removeTranslator(&translator);
}
+void TestConfig::theLanguageKeyOverridesTheEnvironment()
+{
+ // A directory PER CASE. writeIni() always writes qtmaildir.conf and
+ // QSettings caches by path, so five loads from one QTemporaryDir all see
+ // whichever file was written first: this test failed reporting "it_IT"
+ // where it had just written en_US.
+ QTemporaryDir dir, systemDir, shortDir, fullDir, englishDir;
+
+ // Unset means follow the environment, which is what an empty value tells
+ // main.cpp to do by default-constructing a QLocale.
+ Config unset;
+ unset.load(writeIni(dir, QStringLiteral("[general]\n")));
+ QVERIFY(unset.language().isEmpty());
+ QVERIFY(unset.problems().isEmpty());
+
+ // "system" is the default written down. It must read as unset rather than
+ // being passed to QLocale, which would resolve it to C and force English.
+ Config system;
+ system.load(writeIni(systemDir, QStringLiteral(
+ "[general]\n"
+ "language = system\n")));
+ QVERIFY2(system.language().isEmpty(),
+ "'system' must read as unset, not as a locale name");
+ QVERIFY(system.problems().isEmpty());
+
+ // A short code is accepted as written. Qt resolves "it" to it_IT when the
+ // QLocale is built, and QTranslator::load falls back from qtmaildir_it_IT
+ // to qtmaildir_it, so the short form needs no expansion here.
+ Config shortCode;
+ shortCode.load(writeIni(shortDir, QStringLiteral(
+ "[general]\n"
+ "language = it\n")));
+ QCOMPARE(shortCode.language(), QStringLiteral("it"));
+ QVERIFY(shortCode.problems().isEmpty());
+ QCOMPARE(QLocale(shortCode.language()).name(), QStringLiteral("it_IT"));
+
+ Config full;
+ full.load(writeIni(fullDir, QStringLiteral(
+ "[general]\n"
+ "language = it_IT\n")));
+ QCOMPARE(full.language(), QStringLiteral("it_IT"));
+ QVERIFY(full.problems().isEmpty());
+
+ // Forcing English is legitimate and must NOT be reported as a problem, even
+ // though it loads no .qm: English is the source language and ships none.
+ // This is the case that separates "no translation" from "bad value".
+ Config english;
+ english.load(writeIni(englishDir, QStringLiteral(
+ "[general]\n"
+ "language = en_US\n")));
+ QCOMPARE(english.language(), QStringLiteral("en_US"));
+ QVERIFY2(english.problems().isEmpty(),
+ "forcing English is a valid choice, not a configuration error");
+}
+
+void TestConfig::theLanguageKeyRejectsWhatIsNotALocale()
+{
+ // The trap this guards. QLocale accepts any string and degrades an
+ // unrecognised one to C rather than failing, so `language = itallian`
+ // would load no translation and be indistinguishable from asking for
+ // English on purpose: the user's typo would be silent forever. Verified
+ // against QLocale directly first, so the test rests on measured behaviour
+ // rather than on the assumption that a bad name is rejected somewhere.
+ QCOMPARE(QLocale(QStringLiteral("itallian")).language(), QLocale::C);
+
+ QTemporaryDir dir;
+ Config config;
+ config.load(writeIni(dir, QStringLiteral(
+ "[general]\n"
+ "language = itallian\n")));
+
+ QCOMPARE(config.problems().size(), 1);
+ QVERIFY2(config.problems().first().contains(QStringLiteral("itallian")),
+ "the warning must name the value the user wrote");
+ // Cleared, so the caller falls back to the environment rather than being
+ // handed a name that resolves to C and forces English.
+ QVERIFY(config.language().isEmpty());
+}
+
void TestConfig::theFlaggedFilterIsCalledImportant()
{
// Item 57 decided this and item 93 contradicted it. The `flag` ACTION has