File indexing completed on 2024-04-28 15:25:16

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