diff --git a/CHANGES.markdown b/CHANGES.markdown index 86d00ba4b..2c84bc5cc 100644 --- a/CHANGES.markdown +++ b/CHANGES.markdown @@ -25,9 +25,11 @@ Bug fixes and minor enhancements. * Drop down menu for Style and Equipment are so narrow, cannot see contents [879](https://github.com/Brewtarget/brewtarget/issues/879) * OG doesn't immediately change when adding malt to a recipe [880](https://github.com/Brewtarget/brewtarget/issues/880) * IBU doesn't update when adding hops [881](https://github.com/Brewtarget/brewtarget/issues/881) +* Errors when loading translations [889](https://github.com/Brewtarget/brewtarget/pull/889) +* Debian package depencies [890](https://github.com/Brewtarget/brewtarget/issues/890) ### Release Timestamp -Tue, 19 Nov 2024 04:00:11 +0100 +Fri, 22 Nov 2024 04:00:11 +0100 ## v4.0.10 Bug fixes and minor enhancements. diff --git a/packaging/linux/control.in b/packaging/linux/control.in index 6c240965e..9af4a154e 100644 --- a/packaging/linux/control.in +++ b/packaging/linux/control.in @@ -126,25 +126,31 @@ Architecture: amd64 # # Note that some libraries have a "t64" version which is used instead of the "base" one (eg libqt6network6t64 instead # of libqt6network6). This is to do with upgrades to 64-bit time (to fix the "year 2038 problem") -- per -# https://wiki.debian.org/ReleaseGoals/64bit-time. +# https://wiki.debian.org/ReleaseGoals/64bit-time. For the moment, we specify both the "t64" and "non-t64" versions +# to allow things to work on Ubuntu (mostly already migrated to t64) and Debian Stable (mostly not yet migrated to +# t64). +# +# Note too that qt6-translations-l10n is not required in terms of providing any functions that we call, but it does +# ensure the Qt framework's own translation files are installed. # Depends: \ - libc6 (>= 2.35 ), \ - libc6-dev (>= 2.35 ), \ - libc6-i386 (>= 2.35 ), \ - lib32gcc-s1 (>= 12.3.0), \ - lib32stdc++6 (>= 12.3.0), \ - libgcc-s1 (>= 12.3.0), \ - libstdc++6 (>= 12.3.0), \ - libqt6core6t64 (>= 6.2.4 ), \ - libqt6gui6t64 (>= 6.2.4 ), \ - libqt6multimedia6 (>= 6.2.4 ), \ - libqt6network6t64 (>= 6.2.4 ), \ - libqt6printsupport6t64 (>= 6.2.4 ), \ - libqt6sql6t64 (>= 6.2.4 ), \ - libqt6widgets6t64 (>= 6.2.4 ), \ - libxalan-c112t64 (>= 1.12 ), \ - libxerces-c3.2t64 (>= 3.2.3 ) + libc6 (>= 2.35 ), \ + libc6-dev (>= 2.35 ), \ + libc6-i386 (>= 2.35 ), \ + lib32gcc-s1 (>= 12.3.0), \ + lib32stdc++6 (>= 12.3.0), \ + libgcc-s1 (>= 12.3.0), \ + libstdc++6 (>= 12.3.0), \ + libqt6core6 | libqt6core6t64 (>= 6.2.4 ), \ + libqt6gui6 | libqt6gui6t64 (>= 6.2.4 ), \ + libqt6multimedia6 (>= 6.2.4 ), \ + libqt6network6 | libqt6network6t64 (>= 6.2.4 ), \ + libqt6printsupport6 | libqt6printsupport6t64 (>= 6.2.4 ), \ + libqt6sql6 | libqt6sql6t64 (>= 6.2.4 ), \ + libqt6widgets6 | libqt6widgets6t64 (>= 6.2.4 ), \ + libxalan-c112 | libxalan-c112t64 (>= 1.12 ), \ + libxerces-c3.2 | libxerces-c3.2t64 (>= 3.2.3 ), \ + qt6-translations-l10n (>= 6.2.4 ) # # Installed-Size (Optional) : an estimate of the total amount of disk space required to install the named package # The disk space is given as the integer value of the estimated installed size in bytes, divided by 1024 and rounded diff --git a/packaging/linux/rpm.spec.in b/packaging/linux/rpm.spec.in index 6eb2aa4cd..69f079bca 100644 --- a/packaging/linux/rpm.spec.in +++ b/packaging/linux/rpm.spec.in @@ -71,21 +71,22 @@ BuildArch : x86_64 # continuations are OK here! # Requires : \ - glibc >= 2.35 , \ - glibc-32bit >= 2.35 , \ - libgcc_s1-32bit >= 12.3.0, \ - libstdc++6-32bit >= 12.3.0, \ - libgcc_s1 >= 12.3.0, \ - libstdc++6 >= 12.3.0, \ - libQt6Core6 >= 6.2.4 , \ - libQt6Gui6 >= 6.2.4 , \ - libQt6Multimedia6 >= 6.2.4 , \ - libQt6Network6 >= 6.2.4 , \ - libQt6PrintSupport6 >= 6.2.4 , \ - libQt6Sql6 >= 6.2.4 , \ - libQt6Widgets6 >= 6.2.4 , \ - libxalan-c112 >= 1.12 , \ - libxerces-c-3_2 >= 3.2.3 + glibc >= 2.35 , \ + glibc-32bit >= 2.35 , \ + libgcc_s1-32bit >= 12.3.0, \ + libstdc++6-32bit >= 12.3.0, \ + libgcc_s1 >= 12.3.0, \ + libstdc++6 >= 12.3.0, \ + libQt6Core6 >= 6.2.4 , \ + libQt6Gui6 >= 6.2.4 , \ + libQt6Multimedia6 >= 6.2.4 , \ + libQt6Network6 >= 6.2.4 , \ + libQt6PrintSupport6 >= 6.2.4 , \ + libQt6Sql6 >= 6.2.4 , \ + libQt6Widgets6 >= 6.2.4 , \ + libxalan-c112 >= 1.12 , \ + libxerces-c-3_2 >= 3.2.3 , \ + qt6-translations-l10n >= 6.2.4 # Description is done in a different way, perhaps because it's a multi-line field %description diff --git a/scripts/buildTool.py b/scripts/buildTool.py index bb392dcab..c601179c3 100755 --- a/scripts/buildTool.py +++ b/scripts/buildTool.py @@ -429,6 +429,13 @@ def installDependencies(): # NOTE: For the moment at least, we are assuming you are on Ubuntu or another Debian-based Linux. For other # flavours of the OS you need to install libraries and frameworks manually. # + distroName = str( + btUtils.abortOnRunFail(subprocess.run(['lsb_release', '-is'], encoding = "utf-8", capture_output = True)).stdout + ).rstrip() + distroRelease = str( + btUtils.abortOnRunFail(subprocess.run(['lsb_release', '-rs'], encoding = "utf-8", capture_output = True)).stdout + ).rstrip() + log.debug('Linux distro: ' + distroName + ', release: ' + distroRelease) # # For almost everything apart form Boost (see below) we can rely on the distro packages. A few notes: @@ -439,7 +446,9 @@ def installDependencies(): # - We need python-dev to build parts of Boost -- though it may be we could do without this as we only use a # few parts of Boost and most Boost libraries are header-only, so do not require compilation. # - To keep us on our toes, some of the package name formats change between Qt5 and Qt6. Eg qtmultimedia5-dev - # becomes qt6-multimedia-dev. Also libqt5multimedia5-plugins has no direct successor in Qt6. + # becomes qt6-multimedia-dev, qtbase5-dev becomes qt6-base-dev. Also libqt5multimedia5-plugins has no + # direct successor in Qt6. + # - The package called 'libqt6svg6-dev' in Ubuntu 22.04, is renamed to 'qt6-svg-dev' from Ubuntu 24.04. # # I have struggled to find how to install a Qt6 version of lupdate. Compilation on Ubuntu 24.04 seems to work # fine with the 5.15.13 version of lupdate, so we'll make sure that's installed. Various other comments below @@ -447,34 +456,56 @@ def installDependencies(): # log.info('Ensuring libraries and frameworks are installed') btUtils.abortOnRunFail(subprocess.run(['sudo', 'apt-get', 'update'])) + + qt6svgDevPackage = 'qt6-svg-dev' + if ('Ubuntu' == distroName and Decimal(distroRelease) < Decimal('24.04')): + qt6svgDevPackage = 'libqt6svg6-dev' + btUtils.abortOnRunFail( subprocess.run( - ['sudo', 'apt', 'install', '-y', 'build-essential', - 'cmake', - 'coreutils', - 'debhelper', - 'git', - 'libqt6sql6-psql', - 'libqt6sql6-sqlite', - 'libqt6svg6-dev', - 'libssl-dev', # For OpenSSL headers - 'libxalan-c-dev', - 'libxerces-c-dev', - 'lintian', - 'meson', - 'ninja-build', - 'pandoc', - 'python3', - 'python3-dev', - 'qmake6', # Possibly needed for Qt6 lupdate - 'qtbase5-dev', - 'qt6-l10n-tools', # Needed for Qt6 lupdate? - 'qt6-multimedia-dev', - 'qt6-tools-dev', - 'qttools5-dev-tools', # For Qt5 version of lupdate, per comment above - 'qt6-tools-dev-tools', - 'rpm', - 'rpmlint'] + ['sudo', 'apt', 'install', '-y', + + 'build-essential', + 'cmake', + 'coreutils', + 'debhelper', + 'git', + # + # On Ubuntu 22.04, installing the packages for the Qt GUI module, does not automatically install all its + # dependencies. At compile-time we get an error "Qt6Gui could not be found because dependency + # WrapOpenGL could not be found". Various different posts suggest what packages are needed to satisfy + # this dependency. With a bit of trial-and-error, we have the following. + # + 'libgl1', + 'libglx-dev', + 'libgl1-mesa-dev', + # + 'libqt6gui6', # Qt GUI module -- needed for QColor (per https://doc.qt.io/qt-6.2/qtgui-module.html) + 'libqt6sql6-psql', + 'libqt6sql6-sqlite', + 'libqt6svg6', + 'libqt6svgwidgets6', + 'libssl-dev', # For OpenSSL headers + 'libxalan-c-dev', + 'libxerces-c-dev', + 'lintian', + 'meson', + 'ninja-build', + 'pandoc', + 'python3', + 'python3-dev', + 'qmake6', # Possibly needed for Qt6 lupdate + 'qt6-base-dev', + 'qt6-l10n-tools', # Needed for Qt6 lupdate? + 'qt6-multimedia-dev', + 'qt6-tools-dev', + 'qt6-translations-l10n', # Puts all the *.qm files in /usr/share/qt6/translations + qt6svgDevPackage, + 'qttools5-dev-tools', # For Qt5 version of lupdate, per comment above + 'qt6-tools-dev-tools', + 'rpm', + 'rpmlint' + ] ) ) @@ -715,24 +746,15 @@ def installDependencies(): # to run `sudo qtchooser -install qt6 $(which qmake6)`, so that's what we do here after sorting out the Meson # install. # - distroName = str( - btUtils.abortOnRunFail(subprocess.run(['lsb_release', '-is'], encoding = "utf-8", capture_output = True)).stdout - ).rstrip() - log.debug('Linux distro: ' + distroName) - if ('Ubuntu' == distroName): - ubuntuRelease = str( - btUtils.abortOnRunFail(subprocess.run(['lsb_release', '-rs'], encoding = "utf-8", capture_output = True)).stdout - ).rstrip() - log.debug('Ubuntu release: ' + ubuntuRelease) - if (Decimal(ubuntuRelease) < Decimal('24.04')): - log.info('Installing newer version of Meson the hard way') - btUtils.abortOnRunFail(subprocess.run(['sudo', 'apt', 'remove', '-y', 'meson'])) - btUtils.abortOnRunFail(subprocess.run(['sudo', 'pip3', 'install', 'meson'])) - # - # Now fix lupdate - # - fullPath_qmake6 = shutil.which('qmake6') - btUtils.abortOnRunFail(subprocess.run(['sudo', 'qtchooser', '-install', 'qt6', fullPath_qmake6])) + if ('Ubuntu' == distroName and Decimal(distroRelease) < Decimal('24.04')): + log.info('Installing newer version of Meson the hard way') + btUtils.abortOnRunFail(subprocess.run(['sudo', 'apt', 'remove', '-y', 'meson'])) + btUtils.abortOnRunFail(subprocess.run(['sudo', 'pip3', 'install', 'meson'])) + # + # Now fix lupdate + # + fullPath_qmake6 = shutil.which('qmake6') + btUtils.abortOnRunFail(subprocess.run(['sudo', 'qtchooser', '-install', 'qt6', fullPath_qmake6])) #----------------------------------------------------------------------------------------------------------------- #--------------------------------------------- Windows Dependencies ---------------------------------------------- @@ -1149,7 +1171,8 @@ def installDependencies(): 'pandoc', # 'xercesc3', # 'xalanc', - 'qt6' + 'qt6', + 'qt6-qttranslations' ] for packageToInstall in installListPort: log.debug('Installing ' + packageToInstall + ' via MacPorts') diff --git a/src/Localization.cpp b/src/Localization.cpp index 92a97299c..6a35bc4d3 100644 --- a/src/Localization.cpp +++ b/src/Localization.cpp @@ -18,6 +18,7 @@ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌*/ #include "Localization.h" +#include #include #include // For qApp @@ -40,12 +41,76 @@ namespace { Localization::NumericDateFormat dateFormat = Localization::YearMonthDay; - QString currentLanguage = "en"; + QString currentTwoLetterLanguageCode{""}; + QLocale currentLanguageLocale{}; - QTranslator defaultTrans; - QTranslator btTrans; + // + // It's not super-well explained in the Qt documentation (or the wiki article at + // https://wiki.qt.io/How_to_create_a_multi_language_application), but there are, typically, two sets of translation + // files: + // - The application's own *.qm translation files (in our case bt-*.qm) that are generated from its *.ts files and + // (should ideally) contain translations of all the application-specific text. + // - Qt's own qt_*.qm translation files that ship with the Qt framework and contain translations for Qt's buttons, + // dialog boxes, error messages and so on. + // + // So, we need one translator for each of these two sets of files. When we change languages, we have to reload each + // one. + // + // (I wish I could say huge thought and cleverness went into making these two variable names the same length, but it + // was more luck than judgement!) + // + QTranslator qtFrameworkTranslator; + QTranslator applicationTranslator; + + /** + * \brief This is basically a wrapper around (one of the overloads of) \c QTranslator::load. + * + * As explained at https://doc.qt.io/qt-6/qtranslator.html#load-1, the \c QTranslator::load function does a + * fair bit of logic to try to find the "best match" translation file. Eg, in a locale with UI languages + * "fr_CA" and "en", then, given prefix "qt_" and suffix ".qm", it will look for: + * qt_fr_CA.qm + * qt_fr_ca.qm + * qt_fr_CA + * qt_fr_ca + * qt_fr.qm + * qt_fr + * qt_en.qm + * qt_en + */ + void loadTranslator(QTranslator & translator, + QLocale const & locale, + QString const & filenamePrefix, + QDir const & directory) { + // We have to remove the old translator before we load the new one + if (!translator.isEmpty()) { + qApp->removeTranslator(&translator); + } + + // Qt logging output stream pads everything with spaces by default, so we do our own concatenation here where we + // don't want the extra spaces. + QString message{}; + QTextStream messageAsStream{&message}; + messageAsStream << + filenamePrefix << "*.qm translations for locale " << locale.name() << " with language list " << + locale.uiLanguages().join(", ") << " from " << directory.canonicalPath(); + + qInfo() << Q_FUNC_INFO << "Loading" << message; + if (translator.load(locale, filenamePrefix, "", directory.canonicalPath(), ".qm")) { + qInfo() << Q_FUNC_INFO << "Loaded" << translator.language() << "translations from" << translator.filePath(); + qApp->installTranslator(&translator); + } else { + qWarning() << Q_FUNC_INFO << "Unable to load" << message; + } + + return; + } QLocale initSystemLocale() { + auto systemLocale = QLocale::system(); + + QStringList uiLanguages = systemLocale.uiLanguages(); + qInfo() << Q_FUNC_INFO << "System locale" << systemLocale << "with UI languages" << uiLanguages; + // // At the moment, you need to manually edit the config file to set a forced locale (which is a step up from having // to hard-code something and recompile). Potentially in future we'll allow this to be set via the UI. @@ -68,19 +133,65 @@ namespace { // forcedLocale=fr_FR // if (PersistentSettings::contains(PersistentSettings::Names::forcedLocale)) { - QLocale forcedLocale = - QLocale(PersistentSettings::value(PersistentSettings::Names::forcedLocale, "").toString()); + QString forcedLocaleName = PersistentSettings::value(PersistentSettings::Names::forcedLocale, "").toString(); + QLocale forcedLocale{forcedLocaleName}; + qInfo() << + Q_FUNC_INFO << "Read config setting" << PersistentSettings::Names::forcedLocale << "=" << + forcedLocaleName << "so overriding system locale" << systemLocale << "with" << forcedLocale << + "with UI languages" << forcedLocale.uiLanguages(); // This probably isn't needed, but should force this locale into places where QLocale::system() is being called // instead of Localization::getLocale(). Note that QLocale::setDefault() is not reentrant, but that's OK as // we are guaranteed to be single-threaded here. QLocale::setDefault(forcedLocale); return forcedLocale; } - return QLocale::system(); + return systemLocale; } } +// +// NOTE that when we add a new language, we need to: +// - Update the list below +// - Create a new bt_*.ts file in the ../translations directory +// - Create a new flag svg in the ../images directory +// - Add a file alias for the new flag in ../resources.qrc +// - Add the name of the new bt_*.ts file to both ../CMakeLists.txt and ../meson.build +// +QVector Localization::languageInfo { + // + // The order here is alphabetical by language name in English (eg "German" rather then "Deutsch"). This is the same + // as the order of the QLocale::Language enum values (see https://doc.qt.io/qt-6/qlocale.html#Language-enum). + // + // TODO: one day it would be nice to sort this at run-time by the name in whatever the currently-set language is. + // + // For languages spoken in more than one country, we usually choose the flag for the country where that language has + // been spoken the longest (eg France for French) + // + {QLocale::Basque , "eu", QIcon{":/images/flagBasque.svg" }, "Basque" , QObject::tr("Basque" )}, + {QLocale::Catalan , "ca", QIcon{":/images/flagCatalonia.svg" }, "Catalan" , QObject::tr("Catalan" )}, + {QLocale::Chinese , "zh", QIcon{":/images/flagChina.svg" }, "Chinese" , QObject::tr("Chinese" )}, + {QLocale::Czech , "cs", QIcon{":/images/flagCzech.svg" }, "Czech" , QObject::tr("Czech" )}, + {QLocale::Danish , "da", QIcon{":/images/flagDenmark.svg" }, "Danish" , QObject::tr("Danish" )}, + {QLocale::Dutch , "nl", QIcon{":/images/flagNetherlands.svg"}, "Dutch" , QObject::tr("Dutch" )}, + {QLocale::English , "en", QIcon{":/images/flagUK.svg" }, "English" , QObject::tr("English" )}, + {QLocale::Estonian , "et", QIcon{":/images/flagEstonia.svg" }, "Estonian" , QObject::tr("Estonian" )}, + {QLocale::French , "fr", QIcon{":/images/flagFrance.svg" }, "French" , QObject::tr("French" )}, + {QLocale::Galician , "gl", QIcon{":/images/flagGalicia.svg" }, "Galician" , QObject::tr("Galician" )}, + {QLocale::German , "de", QIcon{":/images/flagGermany.svg" }, "German" , QObject::tr("German" )}, + {QLocale::Greek , "el", QIcon{":/images/flagGreece.svg" }, "Greek" , QObject::tr("Greek" )}, + {QLocale::Hungarian , "hu", QIcon{":/images/flagHungary.svg" }, "Hungarian" , QObject::tr("Hungarian" )}, + {QLocale::Italian , "it", QIcon{":/images/flagItaly.svg" }, "Italian" , QObject::tr("Italian" )}, + {QLocale::Latvian , "lv", QIcon{":/images/flagLatvia.svg" }, "Latvian" , QObject::tr("Latvian" )}, + {QLocale::NorwegianBokmal, "nb", QIcon{":/images/flagNorway.svg" }, "Norwegian Bokmål", QObject::tr("Norwegian Bokmål")}, + {QLocale::Polish , "pl", QIcon{":/images/flagPoland.svg" }, "Polish" , QObject::tr("Polish" )}, + {QLocale::Portuguese , "pt", QIcon{":/images/flagBrazil.svg" }, "Portuguese" , QObject::tr("Portuguese" )}, + {QLocale::Russian , "ru", QIcon{":/images/flagRussia.svg" }, "Russian" , QObject::tr("Russian" )}, + {QLocale::Serbian , "sr", QIcon{":/images/flagSerbia.svg" }, "Serbian" , QObject::tr("Serbian" )}, + {QLocale::Spanish , "es", QIcon{":/images/flagSpain.svg" }, "Spanish" , QObject::tr("Spanish" )}, + {QLocale::Swedish , "sv", QIcon{":/images/flagSweden.svg" }, "Swedish" , QObject::tr("Swedish" )}, + {QLocale::Turkish , "tr", QIcon{":/images/flagTurkey.svg" }, "Turkish" , QObject::tr("Turkish" )}, +}; QLocale const & Localization::getLocale() { // @@ -89,6 +200,7 @@ QLocale const & Localization::getLocale() { // small runtime overhead) means that the variable will not be initialised until the first call of this function // (which should be after PersistentSettings::initialise() has been called). // + Q_ASSERT(PersistentSettings::isInitialised()); static QLocale systemLocale = initSystemLocale(); return systemLocale; @@ -126,28 +238,116 @@ QString Localization::displayDateUserFormated(QDate const & date) { return date.toString(format); } -void Localization::setLanguage(QString twoLetterLanguage) { - currentLanguage = twoLetterLanguage; - qApp->removeTranslator(&btTrans); +[[nodiscard]] bool Localization::isSupportedLanguage(QString const & twoLetterLanguage) { + auto const match = std::find_if( + Localization::languageInfo.begin(), + Localization::languageInfo.end(), + [& twoLetterLanguage](auto const & record) { return twoLetterLanguage == record.iso639_1Code; } + ); + return match != Localization::languageInfo.end(); +} + + +void Localization::setLanguage(QLocale const & newLanguageLocale) { + // + // The wording here is a bit stilted, but it's because a locale can have more than one UI language + // + qInfo() << + Q_FUNC_INFO << "Changing language from that for locale" << currentLanguageLocale << "to that for locale" << + newLanguageLocale; + // + // On Linux, because there is a built-in package manager, we don't ship the Qt framework translation files. Provided + // qt6-translations-l10n is installed, the Qt translation files will be in a standard location (eg + // /usr/share/qt6/translations). + // + // On Windows and Mac, we have to package the Qt translation files with our application. However, this is done + // automatically for us by windeployqt and macdeployqt. Eg the former puts Qt's translations in the application's + // \bin\translations subdirectory. + // + // In all cases, our own translations live in a different folder. + // + static const QDir qtFrameworkTranslationsDir{QLibraryInfo::path(QLibraryInfo::TranslationsPath)}; + static const QDir applicationTranslationsDir{Application::getResourceDir().canonicalPath() + "/translations_qm"}; + loadTranslator(qtFrameworkTranslator, newLanguageLocale, "qt_", qtFrameworkTranslationsDir); + loadTranslator(applicationTranslator, newLanguageLocale, "bt_", applicationTranslationsDir); + currentLanguageLocale = newLanguageLocale; + // + // QTranslator::language() returns "the target language as stored in the translation file", so this _should_ give us + // back the value of language in the opening TS tag of the XML in our own bt_*.ts files, and this _should_ already be + // a two-letter language code. + // + // However, for reasons I didn't get to the bottom of, it can be that we actually get back empty string from + // QTranslator::language(). In which case, our best guess at the language that has just been set comes from the + // name of the locale. + // + currentTwoLetterLanguageCode = applicationTranslator.language(); + qDebug() << Q_FUNC_INFO << "QTranslator::language() returned" << currentTwoLetterLanguageCode; + if (currentTwoLetterLanguageCode.isEmpty()) { + auto const qtLanguageCode = newLanguageLocale.language(); + qDebug() << Q_FUNC_INFO << "QLocale::language() returned" << qtLanguageCode; + auto const match = std::find_if( + Localization::languageInfo.begin(), + Localization::languageInfo.end(), + [& qtLanguageCode](auto const & record) { return qtLanguageCode == record.qtLanguageCode; } + ); + if (match != Localization::languageInfo.end()) { + currentTwoLetterLanguageCode = match->iso639_1Code; + } else { + // + // If we get here, we're out of ideas for how to deduce the language that was set + // + qWarning() << + Q_FUNC_INFO << "Could not deduce language from locale" << newLanguageLocale << ". This may be a bug!"; + qDebug().noquote() << Q_FUNC_INFO << Logging::getStackTrace(); + } + } else { + // Doesn't hurt to force the length to be <=2 + currentTwoLetterLanguageCode.truncate(2); + } + qDebug() << Q_FUNC_INFO << "currentTwoLetterLanguageCode" << currentTwoLetterLanguageCode; + return; +} + +void Localization::setLanguage(QString const & twoLetterLanguage) { + if (!Localization::isSupportedLanguage(twoLetterLanguage)) { + // + // This could be a coding error or could be bad data in a config file. Either way, we can safely continue. + // If it's a coding error then it's useful to have the stack trace. + // + qWarning() << + Q_FUNC_INFO << "Ignoring request to set language to" << twoLetterLanguage << "as not in translations list"; + qDebug().noquote() << Q_FUNC_INFO << Logging::getStackTrace(); + return; + } - QString filename = QString("bt_%1").arg(twoLetterLanguage); - QDir translations = QDir(Application::getResourceDir().canonicalPath() + "/translations_qm"); + qInfo() << Q_FUNC_INFO << "Changing language from" << currentTwoLetterLanguageCode << "to" << twoLetterLanguage; + Localization::setLanguage(QLocale{twoLetterLanguage}); + // Normally the QLocale overload of Localization::setLanguage should be able to work out what language was set, but, + // if not, we can say at least what we are trying to set! + if (currentTwoLetterLanguageCode.isEmpty()) { + currentTwoLetterLanguageCode = twoLetterLanguage; + } - if (btTrans.load(filename, translations.canonicalPath())) { - qApp->installTranslator(&btTrans); + if (currentTwoLetterLanguageCode != twoLetterLanguage) { + qWarning() << + Q_FUNC_INFO << "Attempt to set language to" << twoLetterLanguage << "actually resulted in setting it to" << + currentTwoLetterLanguageCode; } + return; } -QString const & Localization::getCurrentLanguage() { - return currentLanguage; +QString Localization::getCurrentLanguage() { + return currentTwoLetterLanguageCode; } -QString const & Localization::getSystemLanguage() { +QString Localization::getSystemLanguage() { // QLocale::name() is of the form language_country, // where 'language' is a lowercase 2-letter ISO 639-1 language code, // and 'country' is an uppercase 2-letter ISO 3166 country code. - return Localization::getLocale().name().split("_")[0]; + QString localeName {Localization::getLocale().name()}; + qDebug() << Q_FUNC_INFO << "Locale name:" << localeName; + return localeName.split("_")[0]; } bool Localization::hasUnits(QString qstr) { @@ -217,39 +417,46 @@ double Localization::toDouble(QString text, char const * const caller) { void Localization::loadTranslations() { - if (qApp == nullptr) { + // TBD: Not sure if we really need this check here, but it's not hurting anything. + if (!qApp) { return; } - // Load translators. - bool succeeded = defaultTrans.load("qt_" + Localization::getLocale().name(), - QLibraryInfo::path(QLibraryInfo::TranslationsPath)); - if (!succeeded) { - qWarning() << Q_FUNC_INFO << "Error loading translations for" << Localization::getLocale().name(); - } - if (getCurrentLanguage().isEmpty()) { - setLanguage(getSystemLanguage()); + // If Localization::loadSettings() has already set the language, then we don't need to do anything here. Otherwise, + // by default, we'll try to use the language of the system locale. + if (currentTwoLetterLanguageCode.isEmpty()) { + QLocale systemLocale = Localization::getLocale(); + qInfo() << Q_FUNC_INFO << "Setting language based on system locale:" << systemLocale; + Localization::setLanguage(systemLocale); + // If that didn't work then we'll try getting the system language directly. + if (currentTwoLetterLanguageCode.isEmpty()) { + Localization::setLanguage(Localization::getSystemLanguage()); + } } - //btTrans.load("bt_" + getSystemLanguage()); - - // Install translators - qApp->installTranslator(&defaultTrans); - //qApp->installTranslator(btTrans); return; } void Localization::loadSettings() { + if (PersistentSettings::contains(PersistentSettings::Names::language)) { - Localization::setLanguage(PersistentSettings::value(PersistentSettings::Names::language,"").toString()); + // It can be that the config file contains an empty setting for language ("language="), in which case we should + // ignore it + QString savedLanguage = PersistentSettings::value(PersistentSettings::Names::language, "").toString(); + if (!savedLanguage.isEmpty()) { + qInfo() << Q_FUNC_INFO << "Config file requests language as" << savedLanguage; + Localization::setLanguage(savedLanguage); + } } - dateFormat = static_cast(PersistentSettings::value(PersistentSettings::Names::date_format, Localization::YearMonthDay).toInt()); + dateFormat = static_cast( + PersistentSettings::value(PersistentSettings::Names::date_format, Localization::YearMonthDay).toInt() + ); return; } void Localization::saveSettings() { - PersistentSettings::insert(PersistentSettings::Names::language, currentLanguage); + PersistentSettings::insert(PersistentSettings::Names::language , currentTwoLetterLanguageCode); PersistentSettings::insert(PersistentSettings::Names::date_format, dateFormat); return; } diff --git a/src/Localization.h b/src/Localization.h index d8e0d103c..2fc59a52e 100644 --- a/src/Localization.h +++ b/src/Localization.h @@ -1,5 +1,5 @@ /*╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ - * Localization.h is part of Brewtarget, and is copyright the following authors 2011-2023: + * Localization.h is part of Brewtarget, and is copyright the following authors 2011-2024: * • Greg Meess * • Matt Young * • Mik Firestone @@ -21,13 +21,29 @@ #pragma once #include +#include #include #include +#include class BtStringConst; class NamedEntity; namespace Localization { + struct LanguageInfo { + int qtLanguageCode; // From enum QLocale::Language + QString iso639_1Code; // The two-letter language code used by Localization::setLanguage() + QIcon countryFlag; // Yes, we know some languages are spoken in more than one country... + char const * nameInEnglish; + QString nameInCurrentLang; // Don't strictly need to store this, but having the hard-coded tr() calls + // in the initialisation flag up what language names need translating + }; + + /** + * \brief This list holds info about the languages we support + */ + extern QVector languageInfo; + /** * \brief Returns the locale to use for formatting numbers etc. Usually this is the same as \c QLocale::system(), * but can be overridden for testing purposes. @@ -78,6 +94,13 @@ namespace Localization { */ QString displayDateUserFormated(QDate const & date); + /** + * \return \c true if the supplied language code is one we support, \c false otherwise + */ + [[nodiscard]] bool isSupportedLanguage(QString const & twoLetterLanguage); + + void setLanguage(QLocale const & newLanguageLocale); + /** * \brief Loads the translator with two letter ISO 639-1 code. * @@ -86,19 +109,19 @@ namespace Localization { * * \param twoLetterLanguage two letter ISO 639-1 code */ - void setLanguage(QString twoLetterLanguage); + void setLanguage(QString const & twoLetterLanguage); /** * \brief Gets the 2-letter ISO 639-1 language code we are currently using. * \returns current 2-letter ISO 639-1 language code. */ - QString const & getCurrentLanguage(); + QString getCurrentLanguage(); /** * \brief Gets the ISO 639-1 language code for the system. * \returns current 2-letter ISO 639-1 system language code */ - QString const & getSystemLanguage(); + QString getSystemLanguage(); /** * \brief Convert a \c QString to a \c double, if possible using the default locale and if not using the C locale. diff --git a/src/OptionDialog.cpp b/src/OptionDialog.cpp index ad6117d8d..d0b34a395 100644 --- a/src/OptionDialog.cpp +++ b/src/OptionDialog.cpp @@ -64,14 +64,6 @@ namespace { TEST_PASSED }; - struct LanguageInfo { - QString iso639_1Code; // What we need to pass to Localization::setLanguage() - QIcon countryFlag; // Yes, we know some languages are spoken in more than one country... - char const * nameInEnglish; - QString nameInCurrentLang; // Don't strictly need to store this, but having the hard-coded tr() calls - // in the initialisation flag up what language names need translating - }; - /** * \brief For a given QComboBox, save the UnitSystem it has selected * @@ -136,38 +128,7 @@ class OptionDialog::impl { label_numBackups {optionDialog.groupBox_dbConfig}, spinBox_numBackups {optionDialog.groupBox_dbConfig}, label_frequency {optionDialog.groupBox_dbConfig}, - spinBox_frequency {optionDialog.groupBox_dbConfig}, - languageInfo { - // - // See also CmakeLists.txt for list of translation source files (in ../translations directory) - // - // The order here is alphabetical by language name in English (eg "German" rather then "Deutsch"). Of course, - // one day it would be nice to sort this at run-time by the name in whatever the currently-set language is. - // - {"eu", QIcon(":images/flagBasque.svg" ), "Basque" , tr("Basque" )}, - {"ca", QIcon(":images/flagCatalonia.svg" ), "Catalan" , tr("Catalan" )}, - {"zh", QIcon(":images/flagChina.svg" ), "Chinese" , tr("Chinese" )}, - {"cs", QIcon(":images/flagCzech.svg" ), "Czech" , tr("Czech" )}, - {"da", QIcon(":images/flagDenmark.svg" ), "Danish" , tr("Danish" )}, - {"nl", QIcon(":images/flagNetherlands.svg"), "Dutch" , tr("Dutch" )}, - {"de", QIcon(":images/flagGermany.svg" ), "German" , tr("German" )}, - {"el", QIcon(":images/flagGreece.svg" ), "Greek" , tr("Greek" )}, - {"en", QIcon(":images/flagUK.svg" ), "English" , tr("English" )}, - {"et", QIcon(":images/flagEstonia.svg" ), "Estonian" , tr("Estonian" )}, - {"fr", QIcon(":images/flagFrance.svg" ), "French" , tr("French" )}, - {"gl", QIcon(":images/flagGalicia.svg" ), "Galician" , tr("Galician" )}, - {"hu", QIcon(":images/flagHungary.svg" ), "Hungarian" , tr("Hungarian" )}, - {"it", QIcon(":images/flagItaly.svg" ), "Italian" , tr("Italian" )}, - {"lv", QIcon(":images/flagLatvia.svg" ), "Latvian" , tr("Latvian" )}, - {"nb", QIcon(":images/flagNorway.svg" ), "Norwegian Bokmål", tr("Norwegian Bokmål")}, - {"pl", QIcon(":images/flagPoland.svg" ), "Polish" , tr("Polish" )}, - {"pt", QIcon(":images/flagBrazil.svg" ), "Portuguese" , tr("Portuguese" )}, - {"ru", QIcon(":images/flagRussia.svg" ), "Russian" , tr("Russian" )}, - {"sr", QIcon(":images/flagSerbia.svg" ), "Serbian" , tr("Serbian" )}, - {"es", QIcon(":images/flagSpain.svg" ), "Spanish" , tr("Spanish" )}, - {"sv", QIcon(":images/flagSweden.svg" ), "Swedish" , tr("Swedish" )}, - {"tr", QIcon(":images/flagTurkey.svg" ), "Turkish" , tr("Turkish" )}, - } { + spinBox_frequency {optionDialog.groupBox_dbConfig} { // // Optimise the select file dialog to select directories // @@ -219,7 +180,7 @@ class OptionDialog::impl { void initLangs(OptionDialog & optionDialog) { - for (auto langInfo : this->languageInfo) { + for (auto langInfo : Localization::languageInfo) { optionDialog.comboBox_lang->addItem(langInfo.countryFlag, langInfo.nameInCurrentLang, langInfo.iso639_1Code); } @@ -393,9 +354,9 @@ class OptionDialog::impl { this->retranslateDbDialog(); // Retranslate the language combobox. - for (int ii = 0; ii < this->languageInfo.size(); ++ii) { - this->languageInfo[ii].nameInCurrentLang = tr(this->languageInfo[ii].nameInEnglish); - optionDialog.comboBox_lang->setItemText(ii, this->languageInfo[ii].nameInCurrentLang); + for (int ii = 0; ii < Localization::languageInfo.size(); ++ii) { + Localization::languageInfo[ii].nameInCurrentLang = tr(Localization::languageInfo[ii].nameInEnglish); + optionDialog.comboBox_lang->setItemText(ii, Localization::languageInfo[ii].nameInCurrentLang); } return; } @@ -579,8 +540,6 @@ class OptionDialog::impl { DbConnectionTestStates dbConnectionTestState; - QVector languageInfo; - }; OptionDialog::OptionDialog(QWidget * parent) : QDialog{}, diff --git a/src/PersistentSettings.cpp b/src/PersistentSettings.cpp index 9e906b606..75c01f60b 100644 --- a/src/PersistentSettings.cpp +++ b/src/PersistentSettings.cpp @@ -171,6 +171,10 @@ void PersistentSettings::initialise(QString customUserDataDir) { return; } +[[nodiscard]] bool PersistentSettings::isInitialised() { + return initialised; +} + QDir PersistentSettings::getConfigDir() { Q_ASSERT(initialised); // Note that it can be valid for canonicalPath() to return empty string -- if config dir is current dir diff --git a/src/PersistentSettings.h b/src/PersistentSettings.h index 247fb585d..d8d31c078 100644 --- a/src/PersistentSettings.h +++ b/src/PersistentSettings.h @@ -35,7 +35,7 @@ #include "utils/BtStringConst.h" -//╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ +//====================================================================================================================== //========================================== Start of setting NAME constants =========================================== //===== (Note that we only need to add here names that have no section or are used in multiple places in the code) ===== //===== (Note too that property names are often used as setting names and, in such cases, are not redefined here) ====== @@ -88,8 +88,8 @@ AddSettingName(versioning) AddSettingName(windowState) #undef AddSettingName //=========================================== End of setting NAME constants ============================================ -//╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ -//╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ +//====================================================================================================================== +//====================================================================================================================== //======================================== Start of setting SECTION constants ========================================== // .:TODO:. I think most of these are no longer used and can be deleted #define AddSettingSection(section) namespace PersistentSettings::Sections { BtStringConst const section{#section}; } @@ -106,7 +106,7 @@ AddSettingSection(yeastTable) AddSettingSection(yeastTableModel) #undef AddSettingName //========================================= End of setting SECTION constants =========================================== -//╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ +//====================================================================================================================== /** @@ -135,6 +135,8 @@ namespace PersistentSettings { */ void initialise(QString customUserDataDir = ""); + [[nodiscard]] bool isInitialised(); + /** * \return the config directory */