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 }