File indexing completed on 2024-04-21 03:54:20

0001 /*  This file is part of the KDE libraries
0002     SPDX-FileCopyrightText: 2001 Hans Petter Bieker <bieker@kde.org>
0003     SPDX-FileCopyrightText: 2012, 2013 Chusslove Illich <caslav.ilic@gmx.net>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "config.h"
0009 
0010 #include <kcatalog_p.h>
0011 
0012 #include "ki18n_logging.h"
0013 
0014 #include <QByteArray>
0015 #include <QCoreApplication>
0016 #include <QDebug>
0017 #include <QDir>
0018 #include <QFile>
0019 #include <QFileInfo>
0020 #include <QMutexLocker>
0021 #include <QSet>
0022 #include <QStandardPaths>
0023 #include <QStringList>
0024 
0025 #ifdef Q_OS_ANDROID
0026 #include <QCoreApplication>
0027 #include <QJniEnvironment>
0028 #include <QJniObject>
0029 
0030 #include <android/asset_manager.h>
0031 #include <android/asset_manager_jni.h>
0032 
0033 #if __ANDROID_API__ < 23
0034 #include <dlfcn.h>
0035 #endif
0036 #endif
0037 
0038 #include <cerrno>
0039 #include <cstdio>
0040 #include <cstring>
0041 #include <locale.h>
0042 #include <stdlib.h>
0043 
0044 #include "gettext.h" // Must be included after <stdlib.h>
0045 
0046 // not defined on win32 :(
0047 #ifdef _WIN32
0048 #ifndef LC_MESSAGES
0049 #define LC_MESSAGES 42
0050 #endif
0051 #endif
0052 
0053 #if HAVE_NL_MSG_CAT_CNTR
0054 extern "C" int Q_DECL_IMPORT _nl_msg_cat_cntr;
0055 #endif
0056 
0057 static char *s_langenv = nullptr;
0058 static const int s_langenvMaxlen = 64;
0059 // = "LANGUAGE=" + 54 chars for language code + terminating null \0 character
0060 
0061 static void copyToLangArr(const QByteArray &lang)
0062 {
0063     const int bytes = std::snprintf(s_langenv, s_langenvMaxlen, "LANGUAGE=%s", lang.constData());
0064     if (bytes < 0) {
0065         qCWarning(KI18N) << "There was an error while writing LANGUAGE environment variable:" << std::strerror(errno);
0066     } else if (bytes > (s_langenvMaxlen - 1)) { // -1 for the \0 character
0067         qCWarning(KI18N) << "The value of the LANGUAGE environment variable:" << lang << "( size:" << lang.size() << "),\n"
0068                          << "was longer than (and consequently truncated to) the max. length of:" << (s_langenvMaxlen - strlen("LANGUAGE=") - 1);
0069     }
0070 }
0071 
0072 class KCatalogStaticData
0073 {
0074 public:
0075     KCatalogStaticData()
0076     {
0077 #ifdef Q_OS_ANDROID
0078         QJniEnvironment env;
0079         QJniObject context = QNativeInterface::QAndroidApplication::context();
0080         m_assets = context.callObjectMethod("getAssets", "()Landroid/content/res/AssetManager;");
0081         m_assetMgr = AAssetManager_fromJava(env.jniEnv(), m_assets.object());
0082 
0083 #if __ANDROID_API__ < 23
0084         fmemopenFunc = reinterpret_cast<decltype(fmemopenFunc)>(dlsym(RTLD_DEFAULT, "fmemopen"));
0085 #endif
0086 #endif
0087     }
0088 
0089     QHash<QByteArray /*domain*/, QString /*directory*/> customCatalogDirs;
0090     QMutex mutex;
0091 
0092 #ifdef Q_OS_ANDROID
0093     QJniObject m_assets;
0094     AAssetManager *m_assetMgr = nullptr;
0095 #if __ANDROID_API__ < 23
0096     FILE *(*fmemopenFunc)(void *, size_t, const char *);
0097 #endif
0098 #endif
0099 };
0100 
0101 Q_GLOBAL_STATIC(KCatalogStaticData, catalogStaticData)
0102 
0103 class KCatalogPrivate
0104 {
0105 public:
0106     KCatalogPrivate();
0107 
0108     QByteArray domain;
0109     QByteArray language;
0110     QByteArray localeDir;
0111 
0112     QByteArray systemLanguage;
0113     bool bindDone;
0114 
0115     static QByteArray currentLanguage;
0116 
0117     void setupGettextEnv();
0118     void resetSystemLanguage();
0119 };
0120 
0121 KCatalogPrivate::KCatalogPrivate()
0122     : bindDone(false)
0123 {
0124 }
0125 
0126 QByteArray KCatalogPrivate::currentLanguage;
0127 
0128 KCatalog::KCatalog(const QByteArray &domain, const QString &language_)
0129     : d(new KCatalogPrivate)
0130 {
0131     d->domain = domain;
0132     d->language = QFile::encodeName(language_);
0133     d->localeDir = QFile::encodeName(catalogLocaleDir(domain, language_));
0134 
0135     if (!d->localeDir.isEmpty()) {
0136         // Always get translations in UTF-8, regardless of user's environment.
0137         bind_textdomain_codeset(d->domain, "UTF-8");
0138 
0139         // Invalidate current language, to trigger binding at next translate call.
0140         KCatalogPrivate::currentLanguage.clear();
0141 
0142         if (!s_langenv) {
0143             // Call putenv only here, to initialize LANGUAGE variable.
0144             // Later only change s_langenv to what is currently needed.
0145             // This doesn't work on Windows though, so there we need putenv calls on every change
0146             s_langenv = new char[s_langenvMaxlen];
0147             copyToLangArr(qgetenv("LANGUAGE"));
0148             putenv(s_langenv);
0149         }
0150     }
0151 }
0152 
0153 KCatalog::~KCatalog() = default;
0154 
0155 #if defined(Q_OS_ANDROID) && __ANDROID_API__ < 23
0156 static QString androidUnpackCatalog(const QString &relpath)
0157 {
0158     // the catalog files are no longer extracted to the local file system
0159     // by androiddeployqt starting with Qt 5.14, libintl however needs
0160     // local files rather than qrc: or asset: URLs, so we unpack the .mo
0161     // files on demand to the local cache folder
0162 
0163     const QString cachePath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/org.kde.ki18n/") + relpath;
0164     QFileInfo cacheFile(cachePath);
0165     if (cacheFile.exists()) {
0166         return cachePath;
0167     }
0168 
0169     const QString assetPath = QLatin1String("assets:/share/locale/") + relpath;
0170     if (!QFileInfo::exists(assetPath)) {
0171         return {};
0172     }
0173 
0174     QDir().mkpath(cacheFile.absolutePath());
0175     QFile f(assetPath);
0176     if (!f.copy(cachePath)) {
0177         qCWarning(KI18N) << "Failed to copy catalog:" << f.errorString() << assetPath << cachePath;
0178         return {};
0179     }
0180     return cachePath;
0181 }
0182 #endif
0183 
0184 QString KCatalog::catalogLocaleDir(const QByteArray &domain, const QString &language)
0185 {
0186     QString relpath = QStringLiteral("%1/LC_MESSAGES/%2.mo").arg(language, QFile::decodeName(domain));
0187 
0188     {
0189         QMutexLocker lock(&catalogStaticData->mutex);
0190         const QString customLocaleDir = catalogStaticData->customCatalogDirs.value(domain);
0191         const QString filename = customLocaleDir + QLatin1Char('/') + relpath;
0192         if (!customLocaleDir.isEmpty() && QFileInfo::exists(filename)) {
0193 #if defined(Q_OS_ANDROID)
0194             // The exact file name must be returned on Android because libintl-lite loads a catalog by filename with bindtextdomain()
0195             return filename;
0196 #else
0197             return customLocaleDir;
0198 #endif
0199         }
0200     }
0201 
0202 #if defined(Q_OS_ANDROID)
0203 #if __ANDROID_API__ < 23
0204     // fall back to copying the catalog to the file system on old systems
0205     // without fmemopen()
0206     if (!catalogStaticData->fmemopenFunc) {
0207         return androidUnpackCatalog(relpath);
0208     }
0209 #endif
0210     const QString assetPath = QLatin1String("assets:/share/locale/") + relpath;
0211     if (!QFileInfo::exists(assetPath)) {
0212         return {};
0213     }
0214     return assetPath;
0215 
0216 #else
0217     QString file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("locale/") + relpath);
0218 #ifdef Q_OS_WIN
0219     // QStandardPaths fails on Windows for executables that aren't properly deployed yet, such as unit tests
0220     if (file.isEmpty()) {
0221         const QString p = QLatin1String(INSTALLED_LOCALE_PREFIX) + QLatin1String("/bin/data/locale/") + relpath;
0222         if (QFile::exists(p)) {
0223             file = p;
0224         }
0225     }
0226 #endif
0227 
0228     QString localeDir;
0229     if (!file.isEmpty()) {
0230         // Path of the locale/ directory must be returned.
0231         localeDir = QFileInfo(file.left(file.size() - relpath.size())).absolutePath();
0232     }
0233     return localeDir;
0234 #endif
0235 }
0236 
0237 QSet<QString> KCatalog::availableCatalogLanguages(const QByteArray &domain_)
0238 {
0239     QString domain = QFile::decodeName(domain_);
0240     QStringList localeDirPaths = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("locale"), QStandardPaths::LocateDirectory);
0241 #ifdef Q_OS_WIN
0242     // QStandardPaths fails on Windows for executables that aren't properly deployed yet, such as unit tests
0243     localeDirPaths += QLatin1String(INSTALLED_LOCALE_PREFIX) + QLatin1String("/bin/data/locale/");
0244 #endif
0245 
0246     {
0247         QMutexLocker lock(&catalogStaticData->mutex);
0248         auto it = catalogStaticData->customCatalogDirs.constFind(domain_);
0249         if (it != catalogStaticData->customCatalogDirs.constEnd()) {
0250             localeDirPaths.prepend(*it);
0251         }
0252     }
0253 
0254     QSet<QString> availableLanguages;
0255     for (const QString &localDirPath : std::as_const(localeDirPaths)) {
0256         QDir localeDir(localDirPath);
0257         const QStringList languages = localeDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot);
0258         for (const QString &language : languages) {
0259             QString relPath = QStringLiteral("%1/LC_MESSAGES/%2.mo").arg(language, domain);
0260             if (localeDir.exists(relPath)) {
0261                 availableLanguages.insert(language);
0262             }
0263         }
0264     }
0265     return availableLanguages;
0266 }
0267 
0268 #ifdef Q_OS_ANDROID
0269 static void androidAssetBindtextdomain(const QByteArray &domain, const QByteArray &localeDir)
0270 {
0271     AAsset *asset = AAssetManager_open(catalogStaticData->m_assetMgr, localeDir.mid(8).constData(), AASSET_MODE_UNKNOWN);
0272     if (!asset) {
0273         qWarning() << "unable to load asset" << localeDir;
0274         return;
0275     }
0276 
0277     off64_t size = AAsset_getLength64(asset);
0278     const void *buffer = AAsset_getBuffer(asset);
0279 #if __ANDROID_API__ >= 23
0280     FILE *moFile = fmemopen(const_cast<void *>(buffer), size, "r");
0281 #else
0282     FILE *moFile = catalogStaticData->fmemopenFunc(const_cast<void *>(buffer), size, "r");
0283 #endif
0284     loadMessageCatalogFile(domain, moFile);
0285     fclose(moFile);
0286     AAsset_close(asset);
0287 }
0288 #endif
0289 
0290 void KCatalogPrivate::setupGettextEnv()
0291 {
0292     // Point Gettext to current language, recording system value for recovery.
0293     systemLanguage = qgetenv("LANGUAGE");
0294     if (systemLanguage != language) {
0295         // putenv has been called in the constructor,
0296         // it is enough to change the string set there.
0297         copyToLangArr(language);
0298 #ifdef Q_OS_WINDOWS
0299         putenv(s_langenv);
0300 #endif
0301     }
0302 
0303     // Rebind text domain if language actually changed from the last time,
0304     // as locale directories may differ for different languages of same catalog.
0305     if (language != currentLanguage || !bindDone) {
0306         Q_ASSERT_X(QCoreApplication::instance(), "KCatalogPrivate::setupGettextEnv", "You need to instantiate a Q*Application before using KCatalog");
0307         if (!QCoreApplication::instance()) {
0308             qCWarning(KI18N) << "KCatalog being used without a Q*Application instance. Some translations won't work";
0309         }
0310 
0311         currentLanguage = language;
0312         bindDone = true;
0313 
0314         // qDebug() << "bindtextdomain" << domain << localeDir;
0315 #ifdef Q_OS_ANDROID
0316         if (localeDir.startsWith("assets:/")) {
0317             androidAssetBindtextdomain(domain, localeDir);
0318         } else {
0319             bindtextdomain(domain, localeDir);
0320         }
0321 #else
0322         bindtextdomain(domain, localeDir);
0323 #endif
0324 
0325 #if HAVE_NL_MSG_CAT_CNTR
0326         // Magic to make sure GNU Gettext doesn't use stale cached translation
0327         // from previous language.
0328         ++_nl_msg_cat_cntr;
0329 #endif
0330     }
0331 }
0332 
0333 void KCatalogPrivate::resetSystemLanguage()
0334 {
0335     if (language != systemLanguage) {
0336         copyToLangArr(systemLanguage);
0337 #ifdef Q_OS_WINDOWS
0338         putenv(s_langenv);
0339 #endif
0340     }
0341 }
0342 
0343 QString KCatalog::translate(const QByteArray &msgid) const
0344 {
0345     if (!d->localeDir.isEmpty()) {
0346         QMutexLocker locker(&catalogStaticData()->mutex);
0347         d->setupGettextEnv();
0348         const char *msgid_char = msgid.constData();
0349         const char *msgstr = dgettext(d->domain.constData(), msgid_char);
0350         d->resetSystemLanguage();
0351         return msgstr != msgid_char // Yes we want pointer comparison
0352             ? QString::fromUtf8(msgstr)
0353             : QString();
0354     } else {
0355         return QString();
0356     }
0357 }
0358 
0359 QString KCatalog::translate(const QByteArray &msgctxt, const QByteArray &msgid) const
0360 {
0361     if (!d->localeDir.isEmpty()) {
0362         QMutexLocker locker(&catalogStaticData()->mutex);
0363         d->setupGettextEnv();
0364         const char *msgid_char = msgid.constData();
0365         const char *msgstr = dpgettext_expr(d->domain.constData(), msgctxt.constData(), msgid_char);
0366         d->resetSystemLanguage();
0367         return msgstr != msgid_char // Yes we want pointer comparison
0368             ? QString::fromUtf8(msgstr)
0369             : QString();
0370     } else {
0371         return QString();
0372     }
0373 }
0374 
0375 QString KCatalog::translate(const QByteArray &msgid, const QByteArray &msgid_plural, qulonglong n) const
0376 {
0377     if (!d->localeDir.isEmpty()) {
0378         QMutexLocker locker(&catalogStaticData()->mutex);
0379         d->setupGettextEnv();
0380         const char *msgid_char = msgid.constData();
0381         const char *msgid_plural_char = msgid_plural.constData();
0382         const char *msgstr = dngettext(d->domain.constData(), msgid_char, msgid_plural_char, n);
0383         d->resetSystemLanguage();
0384         // If original and translation are same, dngettext will return
0385         // the original pointer, which is generally fine, except in
0386         // the corner cases where e.g. msgstr[1] is same as msgid.
0387         // Therefore check for pointer difference only with msgid or
0388         // only with msgid_plural, and not with both.
0389         return (n == 1 && msgstr != msgid_char) || (n != 1 && msgstr != msgid_plural_char) ? QString::fromUtf8(msgstr) : QString();
0390     } else {
0391         return QString();
0392     }
0393 }
0394 
0395 QString KCatalog::translate(const QByteArray &msgctxt, const QByteArray &msgid, const QByteArray &msgid_plural, qulonglong n) const
0396 {
0397     if (!d->localeDir.isEmpty()) {
0398         QMutexLocker locker(&catalogStaticData()->mutex);
0399         d->setupGettextEnv();
0400         const char *msgid_char = msgid.constData();
0401         const char *msgid_plural_char = msgid_plural.constData();
0402         const char *msgstr = dnpgettext_expr(d->domain.constData(), msgctxt.constData(), msgid_char, msgid_plural_char, n);
0403         d->resetSystemLanguage();
0404         return (n == 1 && msgstr != msgid_char) || (n != 1 && msgstr != msgid_plural_char) ? QString::fromUtf8(msgstr) : QString();
0405     } else {
0406         return QString();
0407     }
0408 }
0409 
0410 void KCatalog::addDomainLocaleDir(const QByteArray &domain, const QString &path)
0411 {
0412     QMutexLocker locker(&catalogStaticData()->mutex);
0413     catalogStaticData()->customCatalogDirs.insert(domain, path);
0414 }