KI18n

kcatalog.cpp
1 /* This file is part of the KDE libraries
2  SPDX-FileCopyrightText: 2001 Hans Petter Bieker <[email protected]>
3  SPDX-FileCopyrightText: 2012, 2013 Chusslove Illich <[email protected]>
4 
5  SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7 
8 #include "config.h"
9 
10 #include <kcatalog_p.h>
11 
12 #include "ki18n_logging.h"
13 
14 #include <QByteArray>
15 #include <QCoreApplication>
16 #include <QDebug>
17 #include <QDir>
18 #include <QFile>
19 #include <QFileInfo>
20 #include <QMutexLocker>
21 #include <QSet>
22 #include <QStandardPaths>
23 #include <QStringList>
24 
25 #ifdef Q_OS_ANDROID
26 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
27 #include <QAndroidJniEnvironment>
28 #include <QAndroidJniObject>
29 #include <QtAndroid>
30 #else
31 #include <QCoreApplication>
32 #include <QJniEnvironment>
33 #include <QJniObject>
34 using QAndroidJniObject = QJniObject;
35 #endif
36 
37 #include <android/asset_manager.h>
38 #include <android/asset_manager_jni.h>
39 
40 #if __ANDROID_API__ < 23
41 #include <dlfcn.h>
42 #endif
43 #endif
44 
45 #include <locale.h>
46 #include <stdlib.h>
47 
48 #include "gettext.h" // Must be included after <stdlib.h>
49 
50 // not defined on win32 :(
51 #ifdef _WIN32
52 #ifndef LC_MESSAGES
53 #define LC_MESSAGES 42
54 #endif
55 #endif
56 
57 #if HAVE_NL_MSG_CAT_CNTR
58 extern "C" int Q_DECL_IMPORT _nl_msg_cat_cntr;
59 #endif
60 
61 static char *langenv = nullptr;
62 static const int langenvMaxlen = 42;
63 // = "LANGUAGE=" + 32 chars for language code + terminating zero
64 
65 class KCatalogStaticData
66 {
67 public:
68  KCatalogStaticData()
69  {
70 #ifdef Q_OS_ANDROID
71 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
72  QAndroidJniEnvironment env;
73  QAndroidJniObject context = QtAndroid::androidContext();
74  m_assets = context.callObjectMethod("getAssets", "()Landroid/content/res/AssetManager;");
75  m_assetMgr = AAssetManager_fromJava(env, m_assets.object());
76 #else
77  QJniEnvironment env;
78  QJniObject context = QNativeInterface::QAndroidApplication::context();
79  m_assets = context.callObjectMethod("getAssets", "()Landroid/content/res/AssetManager;");
80  m_assetMgr = AAssetManager_fromJava(env.jniEnv(), m_assets.object());
81 #endif
82 
83 #if __ANDROID_API__ < 23
84  fmemopenFunc = reinterpret_cast<decltype(fmemopenFunc)>(dlsym(RTLD_DEFAULT, "fmemopen"));
85 #endif
86 #endif
87  }
88 
89  QHash<QByteArray /*domain*/, QString /*directory*/> customCatalogDirs;
90  QMutex mutex;
91 
92 #ifdef Q_OS_ANDROID
93  QAndroidJniObject m_assets;
94  AAssetManager *m_assetMgr = nullptr;
95 #if __ANDROID_API__ < 23
96  FILE *(*fmemopenFunc)(void *, size_t, const char *);
97 #endif
98 #endif
99 };
100 
101 Q_GLOBAL_STATIC(KCatalogStaticData, catalogStaticData)
102 
103 class KCatalogPrivate
104 {
105 public:
106  KCatalogPrivate();
107 
108  QByteArray domain;
109  QByteArray language;
110  QByteArray localeDir;
111 
112  QByteArray systemLanguage;
113  bool bindDone;
114 
115  static QByteArray currentLanguage;
116 
117  void setupGettextEnv();
118  void resetSystemLanguage();
119 };
120 
121 KCatalogPrivate::KCatalogPrivate()
122  : bindDone(false)
123 {
124 }
125 
126 QByteArray KCatalogPrivate::currentLanguage;
127 
128 KCatalog::KCatalog(const QByteArray &domain, const QString &language_)
129  : d(new KCatalogPrivate)
130 {
131  d->domain = domain;
132  d->language = QFile::encodeName(language_);
133  d->localeDir = QFile::encodeName(catalogLocaleDir(domain, language_));
134 
135  if (!d->localeDir.isEmpty()) {
136  // Always get translations in UTF-8, regardless of user's environment.
137  bind_textdomain_codeset(d->domain, "UTF-8");
138 
139  // Invalidate current language, to trigger binding at next translate call.
140  KCatalogPrivate::currentLanguage.clear();
141 
142  if (!langenv) {
143  // Call putenv only here, to initialize LANGUAGE variable.
144  // Later only change langenv to what is currently needed.
145  // This doesn't work on Windows though, so there we need putenv calls on every change
146  langenv = new char[langenvMaxlen];
147  QByteArray baselang = qgetenv("LANGUAGE");
148  qsnprintf(langenv, langenvMaxlen, "LANGUAGE=%s", baselang.constData());
149  putenv(langenv);
150  }
151  }
152 }
153 
154 KCatalog::~KCatalog() = default;
155 
156 #if defined(Q_OS_ANDROID) && __ANDROID_API__ < 23
157 static QString androidUnpackCatalog(const QString &relpath)
158 {
159  // the catalog files are no longer extracted to the local file system
160  // by androiddeployqt starting with Qt 5.14, libintl however needs
161  // local files rather than qrc: or asset: URLs, so we unpack the .mo
162  // files on demand to the local cache folder
163 
164  const QString cachePath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/org.kde.ki18n/") + relpath;
165  QFileInfo cacheFile(cachePath);
166  if (cacheFile.exists()) {
167  return cachePath;
168  }
169 
170  const QString assetPath = QLatin1String("assets:/share/locale/") + relpath;
171  if (!QFileInfo::exists(assetPath)) {
172  return {};
173  }
174 
175  QDir().mkpath(cacheFile.absolutePath());
176  QFile f(assetPath);
177  if (!f.copy(cachePath)) {
178  qCWarning(KI18N) << "Failed to copy catalog:" << f.errorString() << assetPath << cachePath;
179  return {};
180  }
181  return cachePath;
182 }
183 #endif
184 
185 QString KCatalog::catalogLocaleDir(const QByteArray &domain, const QString &language)
186 {
187  QString relpath = QStringLiteral("%1/LC_MESSAGES/%2.mo").arg(language, QFile::decodeName(domain));
188 
189  {
190  QMutexLocker lock(&catalogStaticData->mutex);
191  const QString customLocaleDir = catalogStaticData->customCatalogDirs.value(domain);
192  const QString filename = customLocaleDir + QLatin1Char('/') + relpath;
193  if (!customLocaleDir.isEmpty() && QFileInfo::exists(filename)) {
194 #if defined(Q_OS_ANDROID)
195  // The exact file name must be returned on Android because libintl-lite loads a catalog by filename with bindtextdomain()
196  return filename;
197 #else
198  return customLocaleDir;
199 #endif
200  }
201  }
202 
203 #if defined(Q_OS_ANDROID)
204 #if __ANDROID_API__ < 23
205  // fall back to copying the catalog to the file system on old systems
206  // without fmemopen()
207  if (!catalogStaticData->fmemopenFunc) {
208  return androidUnpackCatalog(relpath);
209  }
210 #endif
211  const QString assetPath = QLatin1String("assets:/share/locale/") + relpath;
212  if (!QFileInfo::exists(assetPath)) {
213  return {};
214  }
215  return assetPath;
216 
217 #else
218  QString file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("locale/") + relpath);
219 #ifdef Q_OS_WIN
220  // QStandardPaths fails on Windows for executables that aren't properly deployed yet, such as unit tests
221  if (file.isEmpty()) {
222  const QString p = QLatin1String(INSTALLED_LOCALE_PREFIX) + QLatin1String("/bin/data/locale/") + relpath;
223  if (QFile::exists(p)) {
224  file = p;
225  }
226  }
227 #endif
228 
229  QString localeDir;
230  if (!file.isEmpty()) {
231  // Path of the locale/ directory must be returned.
232  localeDir = QFileInfo(file.left(file.size() - relpath.size())).absolutePath();
233  }
234  return localeDir;
235 #endif
236 }
237 
238 QSet<QString> KCatalog::availableCatalogLanguages(const QByteArray &domain_)
239 {
240  QString domain = QFile::decodeName(domain_);
242 #ifdef Q_OS_WIN
243  // QStandardPaths fails on Windows for executables that aren't properly deployed yet, such as unit tests
244  localeDirPaths += QLatin1String(INSTALLED_LOCALE_PREFIX) + QLatin1String("/bin/data/locale/");
245 #endif
246 
247  {
248  QMutexLocker lock(&catalogStaticData->mutex);
249  auto it = catalogStaticData->customCatalogDirs.constFind(domain_);
250  if (it != catalogStaticData->customCatalogDirs.constEnd()) {
251  localeDirPaths.prepend(*it);
252  }
253  }
254 
255  QSet<QString> availableLanguages;
256  for (const QString &localDirPath : std::as_const(localeDirPaths)) {
257  QDir localeDir(localDirPath);
258  const QStringList languages = localeDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot);
259  for (const QString &language : languages) {
260  QString relPath = QStringLiteral("%1/LC_MESSAGES/%2.mo").arg(language, domain);
261  if (localeDir.exists(relPath)) {
262  availableLanguages.insert(language);
263  }
264  }
265  }
266  return availableLanguages;
267 }
268 
269 #ifdef Q_OS_ANDROID
270 static void androidAssetBindtextdomain(const QByteArray &domain, const QByteArray &localeDir)
271 {
272  AAsset *asset = AAssetManager_open(catalogStaticData->m_assetMgr, localeDir.mid(8).constData(), AASSET_MODE_UNKNOWN);
273  if (!asset) {
274  qWarning() << "unable to load asset" << localeDir;
275  return;
276  }
277 
278  off64_t size = AAsset_getLength64(asset);
279  const void *buffer = AAsset_getBuffer(asset);
280 #if __ANDROID_API__ >= 23
281  FILE *moFile = fmemopen(const_cast<void *>(buffer), size, "r");
282 #else
283  FILE *moFile = catalogStaticData->fmemopenFunc(const_cast<void *>(buffer), size, "r");
284 #endif
285  loadMessageCatalogFile(domain, moFile);
286  fclose(moFile);
287  AAsset_close(asset);
288 }
289 #endif
290 
291 void KCatalogPrivate::setupGettextEnv()
292 {
293  // Point Gettext to current language, recording system value for recovery.
294  systemLanguage = qgetenv("LANGUAGE");
295  if (systemLanguage != language) {
296  // putenv has been called in the constructor,
297  // it is enough to change the string set there.
298  qsnprintf(langenv, langenvMaxlen, "LANGUAGE=%s", language.constData());
299 #ifdef Q_OS_WINDOWS
300  putenv(langenv);
301 #endif
302  }
303 
304  // Rebind text domain if language actually changed from the last time,
305  // as locale directories may differ for different languages of same catalog.
306  if (language != currentLanguage || !bindDone) {
307  Q_ASSERT_X(QCoreApplication::instance(), "KCatalogPrivate::setupGettextEnv", "You need to instantiate a Q*Application before using KCatalog");
309  qCWarning(KI18N) << "KCatalog being used without a Q*Application instance. Some translations won't work";
310  }
311 
312  currentLanguage = language;
313  bindDone = true;
314 
315  // qDebug() << "bindtextdomain" << domain << localeDir;
316 #ifdef Q_OS_ANDROID
317  if (localeDir.startsWith("assets:/")) {
318  androidAssetBindtextdomain(domain, localeDir);
319  } else {
320  bindtextdomain(domain, localeDir);
321  }
322 #else
323  bindtextdomain(domain, localeDir);
324 #endif
325 
326 #if HAVE_NL_MSG_CAT_CNTR
327  // Magic to make sure GNU Gettext doesn't use stale cached translation
328  // from previous language.
329  ++_nl_msg_cat_cntr;
330 #endif
331  }
332 }
333 
334 void KCatalogPrivate::resetSystemLanguage()
335 {
336  if (language != systemLanguage) {
337  qsnprintf(langenv, langenvMaxlen, "LANGUAGE=%s", systemLanguage.constData());
338 #ifdef Q_OS_WINDOWS
339  putenv(langenv);
340 #endif
341  }
342 }
343 
344 QString KCatalog::translate(const QByteArray &msgid) const
345 {
346  if (!d->localeDir.isEmpty()) {
347  QMutexLocker locker(&catalogStaticData()->mutex);
348  d->setupGettextEnv();
349  const char *msgid_char = msgid.constData();
350  const char *msgstr = dgettext(d->domain.constData(), msgid_char);
351  d->resetSystemLanguage();
352  return msgstr != msgid_char // Yes we want pointer comparison
353  ? QString::fromUtf8(msgstr)
354  : QString();
355  } else {
356  return QString();
357  }
358 }
359 
360 QString KCatalog::translate(const QByteArray &msgctxt, const QByteArray &msgid) const
361 {
362  if (!d->localeDir.isEmpty()) {
363  QMutexLocker locker(&catalogStaticData()->mutex);
364  d->setupGettextEnv();
365  const char *msgid_char = msgid.constData();
366  const char *msgstr = dpgettext_expr(d->domain.constData(), msgctxt.constData(), msgid_char);
367  d->resetSystemLanguage();
368  return msgstr != msgid_char // Yes we want pointer comparison
369  ? QString::fromUtf8(msgstr)
370  : QString();
371  } else {
372  return QString();
373  }
374 }
375 
376 QString KCatalog::translate(const QByteArray &msgid, const QByteArray &msgid_plural, qulonglong n) const
377 {
378  if (!d->localeDir.isEmpty()) {
379  QMutexLocker locker(&catalogStaticData()->mutex);
380  d->setupGettextEnv();
381  const char *msgid_char = msgid.constData();
382  const char *msgid_plural_char = msgid_plural.constData();
383  const char *msgstr = dngettext(d->domain.constData(), msgid_char, msgid_plural_char, n);
384  d->resetSystemLanguage();
385  // If original and translation are same, dngettext will return
386  // the original pointer, which is generally fine, except in
387  // the corner cases where e.g. msgstr[1] is same as msgid.
388  // Therefore check for pointer difference only with msgid or
389  // only with msgid_plural, and not with both.
390  return (n == 1 && msgstr != msgid_char) || (n != 1 && msgstr != msgid_plural_char) ? QString::fromUtf8(msgstr) : QString();
391  } else {
392  return QString();
393  }
394 }
395 
396 QString KCatalog::translate(const QByteArray &msgctxt, const QByteArray &msgid, const QByteArray &msgid_plural, qulonglong n) const
397 {
398  if (!d->localeDir.isEmpty()) {
399  QMutexLocker locker(&catalogStaticData()->mutex);
400  d->setupGettextEnv();
401  const char *msgid_char = msgid.constData();
402  const char *msgid_plural_char = msgid_plural.constData();
403  const char *msgstr = dnpgettext_expr(d->domain.constData(), msgctxt.constData(), msgid_char, msgid_plural_char, n);
404  d->resetSystemLanguage();
405  return (n == 1 && msgstr != msgid_char) || (n != 1 && msgstr != msgid_plural_char) ? QString::fromUtf8(msgstr) : QString();
406  } else {
407  return QString();
408  }
409 }
410 
411 void KCatalog::addDomainLocaleDir(const QByteArray &domain, const QString &path)
412 {
413  QMutexLocker(&catalogStaticData()->mutex);
414  catalogStaticData()->customCatalogDirs.insert(domain, path);
415 }
const QChar * constData() const const
QString fromUtf8(const char *str, int size)
int size() const const
QByteArray encodeName(const QString &fileName)
QString writableLocation(QStandardPaths::StandardLocation type)
void clear()
QString locate(QStandardPaths::StandardLocation type, const QString &fileName, QStandardPaths::LocateOptions options)
bool exists() const const
bool exists() const const
Q_GLOBAL_STATIC(Internal::StaticControl, s_instance) class ControlPrivate
void prepend(const T &value)
QByteArray mid(int pos, int len) const const
bool isEmpty() const const
QCoreApplication * instance()
bool mkpath(const QString &dirPath) const const
bool startsWith(const QByteArray &ba) const const
QString absolutePath() const const
const char * constData() const const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString left(int n) const const
QStringList locateAll(QStandardPaths::StandardLocation type, const QString &fileName, QStandardPaths::LocateOptions options)
QSet::iterator insert(const T &value)
QString decodeName(const QByteArray &localFileName)
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Tue Aug 16 2022 04:07:13 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.