File indexing completed on 2024-05-19 04:45:52

0001 // SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
0002 // SPDX-License-Identifier: BSD-2-Clause OR MIT
0003 
0004 // Own headers
0005 // First the interface, which forces the header to be self-contained.
0006 #include "initializetranslation.h"
0007 
0008 #include "initializelibraryresources.h"
0009 #include <qcoreapplication.h>
0010 #include <qdebug.h>
0011 #include <qglobal.h>
0012 #include <qlocale.h>
0013 #include <qmutex.h>
0014 #include <qpointer.h>
0015 #include <qstring.h>
0016 #include <qstringliteral.h>
0017 #include <qthread.h>
0018 #include <qtranslator.h>
0019 
0020 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
0021 #include <qlist.h>
0022 #else
0023 #include <qstringlist.h>
0024 #endif
0025 
0026 /** @internal @file
0027  *
0028  * Provides the @ref PerceptualColor::initializeTranslation() function. */
0029 
0030 namespace PerceptualColor
0031 {
0032 
0033 /** @internal
0034  *
0035  * @brief Set the translation for the whole library.
0036  *
0037  * After calling this function, all objects of this library that are created
0038  * from now on are translated according to translation that was set.
0039  *
0040  * Objects that were yet existing when calling are <em>not always</em>
0041  * automatically updated: When calling this function, Qt sends
0042  * a <tt>QEvent::LanguageChange</tt> event only to top-level widgets,
0043  * and these will get updated then. You can send the event yourself
0044  * to non-top-level widgets to update those widgets also. Note that
0045  * also @ref RgbColorSpaceFactory generates objects that might have
0046  * localized properties; these objects do not support translation
0047  * updates.
0048  *
0049  * If you create objects that use translations <em>before</em> a translation
0050  * has been set explicitly, than automatically an environment-dependant
0051  * translation is loaded.
0052  *
0053  * It is safe to call this function multiple times.
0054  *
0055  * @pre There exists exactly <em>one</em> instance of <tt>QCoreApplication</tt>
0056  * to which the parameter points. This function is called from the same thread
0057  * in which the <tt>QCoreApplication</tt> instance lives.
0058  *
0059  * @param instance A pointer to the <tt>QCoreApplication</tt> instance for
0060  *        which the initialization will be done.
0061  *
0062  * @param newUiLanguages List of translations, ordered by priority, most
0063  *        important ones first, like in <tt>QLocale::uiLanguages()</tt>. If
0064  *        <tt>std::optional::has_value()</tt> than the translation is
0065  *        initialized for this value, and previously loaded translations are
0066  *        removed. If <tt>std::optional::has_value()</tt> is <tt>false</tt>,
0067  *        than it depends: If no translation has been initialized so far, than
0068  *        the translation is initialized to an environment-dependent default
0069  *        value; otherwise there last initialization is simply repeated.
0070  *
0071  * @post The translation is initialized, even if a previous initialization
0072  * had been destroyed by deleting the previous QCoreApplication object. */
0073 void initializeTranslation(QCoreApplication *instance, std::optional<QStringList> newUiLanguages)
0074 {
0075     // Mutex protection
0076     static QMutex mutex;
0077 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
0078     QMutexLocker<QMutex> mutexLocker(&mutex);
0079 #else
0080     QMutexLocker mutexLocker(&mutex);
0081 #endif
0082 
0083     // Check of pre-conditions
0084     // The mutex lowers the risk when using QCoreApplication::instance()
0085     // and QThread::currentThread(), which are not explicitly documented
0086     // as thread-safe.
0087     if (instance == nullptr) {
0088         qWarning() //
0089             << __func__ //
0090             << "must not be called without a QCoreApplication object.";
0091         throw 0;
0092     }
0093     if (QThread::currentThread() != QCoreApplication::instance()->thread()) {
0094         qWarning() //
0095             << __func__ //
0096             << "must not be called by any other thread "
0097                "except the QCoreApplication thread.";
0098         throw 0;
0099     }
0100 
0101     // Static variables
0102     static QTranslator translator;
0103     // The last UI language list that was loaded:
0104     static std::optional<QStringList> translatorUiLanguages;
0105     // A guarded pointer to the QCoreApplication object for which
0106     // the translation is initialized. In the (strange) use case
0107     // that a library user deletes his QCoreApplication (and maybe
0108     // create a new one), this guarded pointer is set to nullptr.
0109     // We provide support for this use case because Q_COREAPP_STARTUP_FUNCTION
0110     // also does, and we want to provide a full-featured alternative
0111     // to Q_COREAPP_STARTUP_FUNCTION.
0112     static QPointer<QCoreApplication> instanceWhereTranslationIsInstalled;
0113 
0114     // Actual function implementation…
0115 
0116     if (!newUiLanguages.has_value()) {
0117         if (translatorUiLanguages.has_value()) {
0118             newUiLanguages = translatorUiLanguages;
0119         } else {
0120             newUiLanguages = //
0121                 QLocale().uiLanguages() + QLocale::system().uiLanguages();
0122         }
0123     }
0124 
0125     // NOTE Currently, this library does not use any plural forms in the
0126     // original English user-visible strings of the form "%1 color(s)".
0127     // If it becomes necessary to have these strings later, we have to
0128     // adapt this function: It has to install unconditionally first a
0129     // QTranslator for English, which resolves to "1 color" or "2 colors"
0130     // and so on. Then, a further QTranslator is installed for the target
0131     // language (but only if the target language is not English?). A
0132     // QTranslator that is installed later has higher priority. This makes
0133     // sure that, if a string is not translated to the target language,
0134     // the user does not see something like "1 color(s)" in the English
0135     // fallback, but instead "1 color".
0136 
0137     // QTranslator::load() will generate a QEvent::LanguageChange event
0138     // even if it loads the same translation file that was loaded anyway.
0139     // To avoid unnecessary events, we check if the new locale is really
0140     // different from the old locale: only than, we try to load the new
0141     // locale.
0142     //
0143     // Still, this will generate unnecessary events if in the previous call
0144     // of this function, we had tried to load a non-existing translation, so
0145     // that the QTranslator has an empty file path now. If we try to load now
0146     // another non-existing translation, we get an unnecessary event. But
0147     // to try to filter this out would be overkill…
0148     if (translatorUiLanguages != newUiLanguages) {
0149         // Resources can be loaded, and they can also be unloaded. Therefore,
0150         // here we make sure that our resources are actually currently loaded.
0151         // It is safe to call this function various times, and the overhead
0152         // should not be big.
0153         initializeLibraryResources();
0154 
0155         if (newUiLanguages.value().count() <= 0) {
0156             // QTranslator::load() will always delete the currently loaded
0157             // translation. After that, it will try to load the new one.
0158             // With this trick, we can delete the existing translation:
0159             Q_UNUSED( // We expect load() to fail, so we discard return value.
0160                 translator.load(
0161                     // Filename of the binary translation file (.qm):
0162                     QStringLiteral("nonexistingfilename"),
0163                     // Directory within which filename is searched
0164                     // if filename is a relative path:
0165                     QStringLiteral(":/PerceptualColor/i18n")));
0166         } else {
0167             bool loaded = false;
0168             int i = 0;
0169             // NOTE We will load the first translation among the translation
0170             // list for which we can find an actual translation file (qm file).
0171             // Example: The list is "fr", "es", "de". The qm file for "fr" does
0172             // not exist, but the "qm" filed for "es" and "de" exist. Only the
0173             // "es" translation is loaded. If a specific string is missing in
0174             // "es", but exists in "de", than the system will nevertheless
0175             // fallback to the original source code language ("en"). Of course,
0176             // it would be better to fallback to "de", but this would require
0177             // to load various QTranslator and this might be a overkill. While
0178             // KDE’s internationalization library explicitly supports this use
0179             // case, Qt doesn’t. And we do not have too many strings to
0180             // translate anyway. If we find out later that we have many
0181             // incomplete translations, we can still implement this feature…
0182             while (!loaded && i < newUiLanguages.value().count()) {
0183                 loaded = translator.load(
0184                     // The locale. From this locale are generated BCP47 codes.
0185                     // Various versions (upper-case and lower-case) are tried
0186                     // to load. If for more specific codes like "en-US" it
0187                     // does not succeed, than less specific variants like
0188                     // "en" are also tried.
0189                     QLocale(newUiLanguages.value().value(i)),
0190                     // First part of the filename
0191                     QStringLiteral("localization"),
0192                     // Separator after the first part of the filename
0193                     // (Intentionally NOT "_" or "-" to avoid confusion
0194                     // with the separators in BCP47 codes
0195                     QStringLiteral("."),
0196                     // Directory within which filename is searched
0197                     // if filename is a relative path:
0198                     QStringLiteral(":/PerceptualColor/i18n"));
0199                 ++i;
0200             }
0201         }
0202         translatorUiLanguages = newUiLanguages;
0203     }
0204 
0205     // Make sure that the translator is installed into the current
0206     // QCoreApplication instance. The library user cannot uninstall it
0207     // because this is only possible when you have a pointer to the
0208     // QTranslator object, but the pointer is kept a private information
0209     // of this function. So we can be confident that, if once installed
0210     // on a QCoreApplication object, the QTranslation object will stay
0211     // available. However, the library user could delete the existing
0212     // QCoreApplication object and create a new one. In this case,
0213     // our guarded pointer instanceWhereTranslationIsInstalled will
0214     // be set to nullptr, so we can detect this case:
0215     if (instanceWhereTranslationIsInstalled != instance) {
0216         if (instance->installTranslator(&translator)) {
0217             instanceWhereTranslationIsInstalled = instance;
0218         } else {
0219             instanceWhereTranslationIsInstalled = nullptr;
0220         }
0221     }
0222 }
0223 
0224 } // namespace PerceptualColor