From 4c11e7c26f625068135fa77cb3285ca51b41cdf8 Mon Sep 17 00:00:00 2001 From: ea5wa Date: Fri, 12 Jun 2026 12:48:22 +0200 Subject: [PATCH 1/3] Add Language menu to select the UI language. Closes #9 KLog used to follow the operating system language only. A new Language menu (between Tools and Help) lists the installed translations by their native name, plus a System default entry that keeps the previous behaviour. The selection is stored in the config file (Language key) and applied on the next start. Co-Authored-By: Claude Fable 5 --- src/main.cpp | 50 ++++++++++++++++--------------------- src/mainwindow.cpp | 62 +++++++++++++++++++++++++++++++++++++++++++++- src/mainwindow.h | 3 +++ src/utilities.cpp | 44 ++++++++++++++++++++++++++++++++ src/utilities.h | 2 ++ 5 files changed, 131 insertions(+), 30 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 5e725c13d..a89ceb943 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/mainwindow.cpp b/src/mainwindow.cpp index b75a19b0f..0a3032305 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2701,7 +2701,7 @@ void MainWindow::createMenusCommon() //setupMenu = menuBar()->addMenu(tr("Setup")); - + createLanguageMenu(); //TODO: To be added once the help dialog has been implemented helpMenu = menuBar()->addMenu(tr("&Help")); @@ -2744,6 +2744,66 @@ void MainWindow::createMenusCommon() connect(updateAct, SIGNAL(triggered()), this, SLOT(slotHelpCheckUpdatesAction())); } +void MainWindow::createLanguageMenu() +{ + logEvent(Q_FUNC_INFO, "Start", Devel); + QSettings settings(util->getCfgFile(), QSettings::IniFormat); + QString currentLanguage = settings.value("Language", "auto").toString().toLower(); + + languageMenu = menuBar()->addMenu(tr("&Language")); + QActionGroup *languageActionGroup = new QActionGroup(this); + languageActionGroup->setExclusive(true); + + QAction *autoAct = new QAction(tr("System default"), this); + autoAct->setCheckable(true); + autoAct->setData("auto"); + autoAct->setToolTip(tr("Use the language of the operating system.")); + languageActionGroup->addAction(autoAct); + languageMenu->addAction(autoAct); + languageMenu->addSeparator(); + + const QStringList languages = util->getAvailableLanguages(); + for (const QString &code : languages) + { + QLocale locale(code); + QString name = locale.nativeLanguageName(); + if (name.isEmpty()) + name = code; + else + name[0] = name.at(0).toUpper(); + QAction *languageAct = new QAction(name, this); + languageAct->setCheckable(true); + languageAct->setData(code); + languageActionGroup->addAction(languageAct); + languageMenu->addAction(languageAct); + if (code == currentLanguage) + languageAct->setChecked(true); + } + // If the configured language is not installed (or it is "auto"), fall back to System default. + if (!languageActionGroup->checkedAction()) + autoAct->setChecked(true); + + connect(languageActionGroup, SIGNAL(triggered(QAction*)), this, SLOT(slotLanguageActionTriggered(QAction*))); + logEvent(Q_FUNC_INFO, "END", Debug); +} + +void MainWindow::slotLanguageActionTriggered(QAction *action) +{ + logEvent(Q_FUNC_INFO, "Start", Devel); + if (action == nullptr) + return; + QSettings settings(util->getCfgFile(), QSettings::IniFormat); + QString newLanguage = action->data().toString(); + QString currentLanguage = settings.value("Language", "auto").toString().toLower(); + if (newLanguage != currentLanguage) + { + settings.setValue("Language", newLanguage); + QMessageBox::information(this, tr("KLog - Language"), + tr("The language change will take effect the next time you start KLog.")); + } + logEvent(Q_FUNC_INFO, "END", Debug); +} + void MainWindow::slotDebugAction() { logEvent(Q_FUNC_INFO, "Start", Devel); diff --git a/src/mainwindow.h b/src/mainwindow.h index 2496465b8..08d392550 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -250,6 +250,7 @@ private slots: void slotAboutQt(); void slotTipsAction(); void slotDebugAction(); + void slotLanguageActionTriggered(QAction *action); // MainQSOEntryWidget void slotShowInfoLabel(const QString &_m); @@ -410,6 +411,7 @@ private slots: void clearBandLabels(); void createMenusCommon(); + void createLanguageMenu(); void createActionsCommon(); void connectDebugLogActions(); // Connects the log actions from other classes @@ -530,6 +532,7 @@ private slots: // qMenu *lotwMarkAllInThisLogAsQueuedMenu; QMenu *viewMenu; // qMenu *setupMenu; + QMenu *languageMenu; QMenu *helpMenu; // qAction *TestAct; // Action for testing purposes only diff --git a/src/utilities.cpp b/src/utilities.cpp index d79f3484b..bd9af4cca 100644 --- a/src/utilities.cpp +++ b/src/utilities.cpp @@ -26,6 +26,7 @@ #include "utilities.h" #include "callsign.h" #include +#include //bool c; Utilities::Utilities(const QString &_parentName) { @@ -583,6 +584,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 ebb32b6fb..1fe24e0b5 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(); From 5f88e6a5a681494fe1aa32092944eb5717e4a3e8 Mon Sep 17 00:00:00 2001 From: ea5wa Date: Fri, 12 Jun 2026 22:51:07 +0200 Subject: [PATCH 2/3] Move the language selector to Settings - User data - Personal data Replace the Language menu in the menu bar with a Language combo box next to the Name field, as requested in the issue #9 review. The selection is still stored in the top level Language key of the config file, read in main.cpp before the UI exists. Co-Authored-By: Claude Fable 5 --- src/mainwindow.cpp | 62 +--------------------------- src/mainwindow.h | 3 -- src/setuppages/setuppageuserdata.cpp | 32 ++++++++++++++ src/setuppages/setuppageuserdata.h | 1 + 4 files changed, 34 insertions(+), 64 deletions(-) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 0a3032305..b75a19b0f 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2701,7 +2701,7 @@ void MainWindow::createMenusCommon() //setupMenu = menuBar()->addMenu(tr("Setup")); - createLanguageMenu(); + //TODO: To be added once the help dialog has been implemented helpMenu = menuBar()->addMenu(tr("&Help")); @@ -2744,66 +2744,6 @@ void MainWindow::createMenusCommon() connect(updateAct, SIGNAL(triggered()), this, SLOT(slotHelpCheckUpdatesAction())); } -void MainWindow::createLanguageMenu() -{ - logEvent(Q_FUNC_INFO, "Start", Devel); - QSettings settings(util->getCfgFile(), QSettings::IniFormat); - QString currentLanguage = settings.value("Language", "auto").toString().toLower(); - - languageMenu = menuBar()->addMenu(tr("&Language")); - QActionGroup *languageActionGroup = new QActionGroup(this); - languageActionGroup->setExclusive(true); - - QAction *autoAct = new QAction(tr("System default"), this); - autoAct->setCheckable(true); - autoAct->setData("auto"); - autoAct->setToolTip(tr("Use the language of the operating system.")); - languageActionGroup->addAction(autoAct); - languageMenu->addAction(autoAct); - languageMenu->addSeparator(); - - const QStringList languages = util->getAvailableLanguages(); - for (const QString &code : languages) - { - QLocale locale(code); - QString name = locale.nativeLanguageName(); - if (name.isEmpty()) - name = code; - else - name[0] = name.at(0).toUpper(); - QAction *languageAct = new QAction(name, this); - languageAct->setCheckable(true); - languageAct->setData(code); - languageActionGroup->addAction(languageAct); - languageMenu->addAction(languageAct); - if (code == currentLanguage) - languageAct->setChecked(true); - } - // If the configured language is not installed (or it is "auto"), fall back to System default. - if (!languageActionGroup->checkedAction()) - autoAct->setChecked(true); - - connect(languageActionGroup, SIGNAL(triggered(QAction*)), this, SLOT(slotLanguageActionTriggered(QAction*))); - logEvent(Q_FUNC_INFO, "END", Debug); -} - -void MainWindow::slotLanguageActionTriggered(QAction *action) -{ - logEvent(Q_FUNC_INFO, "Start", Devel); - if (action == nullptr) - return; - QSettings settings(util->getCfgFile(), QSettings::IniFormat); - QString newLanguage = action->data().toString(); - QString currentLanguage = settings.value("Language", "auto").toString().toLower(); - if (newLanguage != currentLanguage) - { - settings.setValue("Language", newLanguage); - QMessageBox::information(this, tr("KLog - Language"), - tr("The language change will take effect the next time you start KLog.")); - } - logEvent(Q_FUNC_INFO, "END", Debug); -} - void MainWindow::slotDebugAction() { logEvent(Q_FUNC_INFO, "Start", Devel); diff --git a/src/mainwindow.h b/src/mainwindow.h index 08d392550..2496465b8 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -250,7 +250,6 @@ private slots: void slotAboutQt(); void slotTipsAction(); void slotDebugAction(); - void slotLanguageActionTriggered(QAction *action); // MainQSOEntryWidget void slotShowInfoLabel(const QString &_m); @@ -411,7 +410,6 @@ private slots: void clearBandLabels(); void createMenusCommon(); - void createLanguageMenu(); void createActionsCommon(); void connectDebugLogActions(); // Connects the log actions from other classes @@ -532,7 +530,6 @@ private slots: // qMenu *lotwMarkAllInThisLogAsQueuedMenu; QMenu *viewMenu; // qMenu *setupMenu; - QMenu *languageMenu; QMenu *helpMenu; // qAction *TestAct; // Action for testing purposes only diff --git a/src/setuppages/setuppageuserdata.cpp b/src/setuppages/setuppageuserdata.cpp index 287270654..dd3245575 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 f252c307c..49950f025 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; From aa9ac2b29e9ab8e0bf887167417de243374c63b0 Mon Sep 17 00:00:00 2001 From: ea5wa Date: Sat, 13 Jun 2026 17:28:28 +0200 Subject: [PATCH 3/3] Silence cppcheck missingIncludeSystem false positive for QCoreApplication Codacy's cppcheck has no Qt headers in its include path, so the new #include in utilities.cpp was reported as a new issue. The include is required (QCoreApplication::applicationDirPath is used by getTranslationSearchPaths). Add a cppcheck inline suppression for this known false positive. Co-Authored-By: Claude Fable 5 --- src/utilities.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utilities.cpp b/src/utilities.cpp index bd9af4cca..364254bae 100644 --- a/src/utilities.cpp +++ b/src/utilities.cpp @@ -26,6 +26,8 @@ #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)