diff --git a/src/main.cpp b/src/main.cpp index 5e725c13..a89ceb94 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -94,17 +94,26 @@ int showErrorUpdatingTheDB() void loadTranslations(QApplication &app, QTranslator &myappTranslator) { - // Detect UI language. - // On macOS, QLocale::system().name() returns the regional-format locale - // (e.g. "en_US") which may differ from the display language set in - // System Preferences. uiLanguages() reads AppleLanguages and is - // reliable on all platforms. - QString language; - QStringList uiLangs = QLocale::system().uiLanguages(); - if (!uiLangs.isEmpty()) - language = uiLangs.first().left(2).toLower(); - else - language = QLocale::system().name().left(2).toLower(); + Utilities util(Q_FUNC_INFO); + + // The user may have selected a fixed language in the Language menu. + // "auto" (the default) follows the operating system language. + QSettings settings(util.getCfgFile(), QSettings::IniFormat); + QString language = settings.value("Language", "auto").toString().toLower(); + + if ((language == "auto") || language.isEmpty()) + { + // Detect UI language. + // On macOS, QLocale::system().name() returns the regional-format locale + // (e.g. "en_US") which may differ from the display language set in + // System Preferences. uiLanguages() reads AppleLanguages and is + // reliable on all platforms. + QStringList uiLangs = QLocale::system().uiLanguages(); + if (!uiLangs.isEmpty()) + language = uiLangs.first().left(2).toLower(); + else + language = QLocale::system().name().left(2).toLower(); + } //qDebug() << Q_FUNC_INFO << "Language:" << language; @@ -112,24 +121,7 @@ void loadTranslations(QApplication &app, QTranslator &myappTranslator) return; // English is built-in; no translation file needed. QString fileName = "klog_" + language + ".qm"; - QStringList searchPaths; - -#if defined(Q_OS_MACOS) - // .app bundle: KLog.app/Contents/Resources/translations/ - // (CMakeLists: MACOSX_PACKAGE_LOCATION Resources/translations) - searchPaths << QCoreApplication::applicationDirPath() + "/../Resources/translations"; -#elif defined(Q_OS_WIN) - // Alongside the .exe (CMakeLists: DESTINATION translations) - searchPaths << QCoreApplication::applicationDirPath() + "/translations"; -#else - // Linux FHS (CMakeLists: DESTINATION ${CMAKE_INSTALL_DATADIR}/klog/translations) - searchPaths << QCoreApplication::applicationDirPath() + "/../share/klog/translations"; - searchPaths << "/usr/share/klog/translations"; - searchPaths << "/usr/local/share/klog/translations"; -#endif - // Fallback for development builds on every platform - searchPaths << QCoreApplication::applicationDirPath() + "/translations"; - searchPaths << QCoreApplication::applicationDirPath() + "/../src/translations"; + const QStringList searchPaths = util.getTranslationSearchPaths(); for (const QString &dir : searchPaths) { diff --git a/src/setuppages/setuppageuserdata.cpp b/src/setuppages/setuppageuserdata.cpp index 28727065..dd324557 100644 --- a/src/setuppages/setuppageuserdata.cpp +++ b/src/setuppages/setuppageuserdata.cpp @@ -67,7 +67,22 @@ SetupPageUserDataPage::SetupPageUserDataPage(DataProxy_SQLite *dp, World *inject provinceLineEdit = new QLineEdit; countryLineEdit = new QLineEdit; + languageComboBox = new QComboBox; + languageComboBox->addItem(tr("System default"), "auto"); + const QStringList languages = util->getAvailableLanguages(); + for (const QString &code : languages) + { + QLocale langLocale(code); + QString langName = langLocale.nativeLanguageName(); + if (langName.isEmpty()) + langName = code; + else + langName[0] = langName.at(0).toUpper(); + languageComboBox->addItem(langName, code); + } + nameLineEdit->setToolTip(tr("Enter your name.")); + languageComboBox->setToolTip(tr("Select the language of the KLog user interface. 'System default' uses the language of the operating system.")); address1LineEdit->setToolTip(tr("Enter your address - 1st line.")); address2LineEdit->setToolTip(tr("Enter your address - 2nd line.")); address3LineEdit->setToolTip(tr("Enter your address - 3rd line.")); @@ -78,6 +93,7 @@ SetupPageUserDataPage::SetupPageUserDataPage(DataProxy_SQLite *dp, World *inject countryLineEdit->setToolTip(tr("Enter your country.")); QLabel *nameLabel = new QLabel(tr("&Name")); + QLabel *languageLabel = new QLabel(tr("Lang&uage")); QLabel *addressLabel = new QLabel(tr("&Address")); QLabel *cityLabel = new QLabel(tr("Cit&y")); QLabel *zipLabel = new QLabel(tr("&Zip Code")); @@ -85,6 +101,7 @@ SetupPageUserDataPage::SetupPageUserDataPage(DataProxy_SQLite *dp, World *inject QLabel *countryLabel = new QLabel(tr("Countr&y")); nameLabel->setBuddy(nameLineEdit); + languageLabel->setBuddy(languageComboBox); addressLabel->setBuddy(address1LineEdit); cityLabel->setBuddy(cityLineEdit); zipLabel->setBuddy(zipLineEdit); @@ -98,6 +115,8 @@ SetupPageUserDataPage::SetupPageUserDataPage(DataProxy_SQLite *dp, World *inject personalLayout->addWidget(nameLabel, 0, 0); personalLayout->addWidget(nameLineEdit, 1, 0); + personalLayout->addWidget(languageLabel, 0, 2); + personalLayout->addWidget(languageComboBox, 1, 2); personalLayout->addWidget(addressLabel, 2, 0); personalLayout->addWidget(address1LineEdit, 3, 0, 1, 2); personalLayout->addWidget(address2LineEdit, 4, 0, 1, 2); @@ -717,6 +736,15 @@ void SetupPageUserDataPage::saveSettings() settings.setValue ("Antenna3",getAntenna3()); settings.setValue ("Power", getPower ()); settings.endGroup (); + + // The language is a top level setting as it is read in main.cpp before the UI exists. + QString newLanguage = languageComboBox->currentData().toString(); + if (settings.value("Language", "auto").toString().toLower() != newLanguage) + { + settings.setValue("Language", newLanguage); + QMessageBox::information(this, tr("KLog - Language"), + tr("The language change will take effect the next time you start KLog.")); + } //qDebug() << Q_FUNC_INFO << " - END"; } @@ -748,4 +776,8 @@ void SetupPageUserDataPage::loadSettings() ant3LineEdit->setText (settings.value ("Antenna3").toString ()); myPowerSpinBox->setValue(settings.value ("Power").toDouble ()); settings.endGroup (); + + // The language is a top level setting as it is read in main.cpp before the UI exists. + int langIndex = languageComboBox->findData(settings.value("Language", "auto").toString().toLower()); + languageComboBox->setCurrentIndex(qMax(0, langIndex)); } diff --git a/src/setuppages/setuppageuserdata.h b/src/setuppages/setuppageuserdata.h index f252c307..49950f02 100644 --- a/src/setuppages/setuppageuserdata.h +++ b/src/setuppages/setuppageuserdata.h @@ -132,6 +132,7 @@ private slots: //Personal Tab QLineEdit *nameLineEdit; + QComboBox *languageComboBox; QTextEdit *addressTextEdit; QLineEdit *address1LineEdit; QLineEdit *address2LineEdit; diff --git a/src/utilities.cpp b/src/utilities.cpp index d79f3484..364254ba 100644 --- a/src/utilities.cpp +++ b/src/utilities.cpp @@ -26,6 +26,9 @@ #include "utilities.h" #include "callsign.h" #include +// Qt headers are not in cppcheck's include path on the CI; silence the false positive. +// cppcheck-suppress missingIncludeSystem +#include //bool c; Utilities::Utilities(const QString &_parentName) { @@ -583,6 +586,49 @@ QString Utilities::getCfgFile() #endif } +QStringList Utilities::getTranslationSearchPaths() +{ + QStringList searchPaths; +#if defined(Q_OS_MACOS) + // .app bundle: KLog.app/Contents/Resources/translations/ + // (CMakeLists: MACOSX_PACKAGE_LOCATION Resources/translations) + searchPaths << QCoreApplication::applicationDirPath() + "/../Resources/translations"; +#elif defined(Q_OS_WIN) + // Alongside the .exe (CMakeLists: DESTINATION translations) + searchPaths << QCoreApplication::applicationDirPath() + "/translations"; +#else + // Linux FHS (CMakeLists: DESTINATION ${CMAKE_INSTALL_DATADIR}/klog/translations) + searchPaths << QCoreApplication::applicationDirPath() + "/../share/klog/translations"; + searchPaths << "/usr/share/klog/translations"; + searchPaths << "/usr/local/share/klog/translations"; +#endif + // Fallback for development builds on every platform + searchPaths << QCoreApplication::applicationDirPath() + "/translations"; + searchPaths << QCoreApplication::applicationDirPath() + "/../src/translations"; + // qt_add_translations generates the .qm files in /src in development builds + searchPaths << QCoreApplication::applicationDirPath() + "/../src"; + return searchPaths; +} + +QStringList Utilities::getAvailableLanguages() +{ + QStringList languages; + languages << "en"; // English is built-in; no translation file needed. + const QStringList paths = getTranslationSearchPaths(); + for (const QString &dir : paths) + { + const QStringList files = QDir(dir).entryList(QStringList("klog_*.qm"), QDir::Files); + for (const QString &file : files) + { // klog_.qm + QString code = file.mid(5, file.length() - 8).toLower(); + if (!code.isEmpty() && !languages.contains(code)) + languages << code; + } + } + languages.sort(); + return languages; +} + QString Utilities::getDebugLogFile() { #if defined(Q_OS_WIN) diff --git a/src/utilities.h b/src/utilities.h index ebb32b6f..1fe24e0b 100644 --- a/src/utilities.h +++ b/src/utilities.h @@ -86,6 +86,8 @@ class Utilities : public QObject { QString getTQSLsPath(); // Depending on the OS where are usually installed the executables QString getHomeDir(); QString getCfgFile(); + QStringList getTranslationSearchPaths(); // Folders where the klog_*.qm files may be installed + QStringList getAvailableLanguages(); // 2-letter codes of the languages with a translation installed (plus built-in English) QString getCTYFile(); QString getDebugLogFile(); QString getSaveSpotsLogFile();