File indexing completed on 2024-04-28 07:43:00
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 }