1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qplatformdefs.h"
5
6#include "qtranslator.h"
7
8#ifndef QT_NO_TRANSLATION
9
10#include "qfileinfo.h"
11#include "qstring.h"
12#include "qstringlist.h"
13#include "qcoreapplication.h"
14#include "qcoreapplication_p.h"
15#include "qdatastream.h"
16#include "qendian.h"
17#include "qfile.h"
18#include "qalgorithms.h"
19#include "qtranslator_p.h"
20#include "qlocale.h"
21#include "qlogging.h"
22#include "qloggingcategory.h"
23#include "qdebug.h"
24#include "qendian.h"
25#include "qresource.h"
26
27#include <QtCore/private/qduplicatetracker_p.h>
28
29#if defined(Q_OS_UNIX) && !defined(Q_OS_INTEGRITY)
30# define QT_USE_MMAP
31# include "private/qcore_unix_p.h"
32// for mmap
33# include <sys/mman.h>
34#endif
35
36#include <stdlib.h>
37#include <new>
38
39#include "qobject_p.h"
40
41#include <vector>
42#include <memory>
43
44QT_BEGIN_NAMESPACE
45
46static Q_LOGGING_CATEGORY(lcTranslator, "qt.core.qtranslator")
47
48namespace {
49enum Tag { Tag_End = 1, Tag_SourceText16, Tag_Translation, Tag_Context16, Tag_Obsolete1,
50 Tag_SourceText, Tag_Context, Tag_Comment, Tag_Obsolete2 };
51}
52
53/*
54$ mcookie
553cb86418caef9c95cd211cbf60a1bddd
56$
57*/
58
59// magic number for the file
60static const int MagicLength = 16;
61static const uchar magic[MagicLength] = {
62 0x3c, 0xb8, 0x64, 0x18, 0xca, 0xef, 0x9c, 0x95,
63 0xcd, 0x21, 0x1c, 0xbf, 0x60, 0xa1, 0xbd, 0xdd
64};
65
66static inline QString dotQmLiteral() { return QStringLiteral(".qm"); }
67
68static bool match(const uchar *found, uint foundLen, const char *target, uint targetLen)
69{
70 // catch the case if \a found has a zero-terminating symbol and \a len includes it.
71 // (normalize it to be without the zero-terminating symbol)
72 if (foundLen > 0 && found[foundLen-1] == '\0')
73 --foundLen;
74 return ((targetLen == foundLen) && memcmp(s1: found, s2: target, n: foundLen) == 0);
75}
76
77static void elfHash_continue(const char *name, uint &h)
78{
79 const uchar *k;
80 uint g;
81
82 k = (const uchar *) name;
83 while (*k) {
84 h = (h << 4) + *k++;
85 if ((g = (h & 0xf0000000)) != 0)
86 h ^= g >> 24;
87 h &= ~g;
88 }
89}
90
91static void elfHash_finish(uint &h)
92{
93 if (!h)
94 h = 1;
95}
96
97static uint elfHash(const char *name)
98{
99 uint hash = 0;
100 elfHash_continue(name, h&: hash);
101 elfHash_finish(h&: hash);
102 return hash;
103}
104
105/*
106 \internal
107
108 Determines whether \a rules are valid "numerus rules". Test input with this
109 function before calling numerusHelper, below.
110 */
111static bool isValidNumerusRules(const uchar *rules, uint rulesSize)
112{
113 // Disabled computation of maximum numerus return value
114 // quint32 numerus = 0;
115
116 if (rulesSize == 0)
117 return true;
118
119 quint32 offset = 0;
120 do {
121 uchar opcode = rules[offset];
122 uchar op = opcode & Q_OP_MASK;
123
124 if (opcode & 0x80)
125 return false; // Bad op
126
127 if (++offset == rulesSize)
128 return false; // Missing operand
129
130 // right operand
131 ++offset;
132
133 switch (op)
134 {
135 case Q_EQ:
136 case Q_LT:
137 case Q_LEQ:
138 break;
139
140 case Q_BETWEEN:
141 if (offset != rulesSize) {
142 // third operand
143 ++offset;
144 break;
145 }
146 return false; // Missing operand
147
148 default:
149 return false; // Bad op (0)
150 }
151
152 // ++numerus;
153 if (offset == rulesSize)
154 return true;
155
156 } while (((rules[offset] == Q_AND)
157 || (rules[offset] == Q_OR)
158 || (rules[offset] == Q_NEWRULE))
159 && ++offset != rulesSize);
160
161 // Bad op
162 return false;
163}
164
165/*
166 \internal
167
168 This function does no validation of input and assumes it is well-behaved,
169 these assumptions can be checked with isValidNumerusRules, above.
170
171 Determines which translation to use based on the value of \a n. The return
172 value is an index identifying the translation to be used.
173
174 \a rules is a character array of size \a rulesSize containing bytecode that
175 operates on the value of \a n and ultimately determines the result.
176
177 This function has O(1) space and O(rulesSize) time complexity.
178 */
179static uint numerusHelper(int n, const uchar *rules, uint rulesSize)
180{
181 uint result = 0;
182 uint i = 0;
183
184 if (rulesSize == 0)
185 return 0;
186
187 for (;;) {
188 bool orExprTruthValue = false;
189
190 for (;;) {
191 bool andExprTruthValue = true;
192
193 for (;;) {
194 bool truthValue = true;
195 int opcode = rules[i++];
196
197 int leftOperand = n;
198 if (opcode & Q_MOD_10) {
199 leftOperand %= 10;
200 } else if (opcode & Q_MOD_100) {
201 leftOperand %= 100;
202 } else if (opcode & Q_LEAD_1000) {
203 while (leftOperand >= 1000)
204 leftOperand /= 1000;
205 }
206
207 int op = opcode & Q_OP_MASK;
208 int rightOperand = rules[i++];
209
210 switch (op) {
211 case Q_EQ:
212 truthValue = (leftOperand == rightOperand);
213 break;
214 case Q_LT:
215 truthValue = (leftOperand < rightOperand);
216 break;
217 case Q_LEQ:
218 truthValue = (leftOperand <= rightOperand);
219 break;
220 case Q_BETWEEN:
221 int bottom = rightOperand;
222 int top = rules[i++];
223 truthValue = (leftOperand >= bottom && leftOperand <= top);
224 }
225
226 if (opcode & Q_NOT)
227 truthValue = !truthValue;
228
229 andExprTruthValue = andExprTruthValue && truthValue;
230
231 if (i == rulesSize || rules[i] != Q_AND)
232 break;
233 ++i;
234 }
235
236 orExprTruthValue = orExprTruthValue || andExprTruthValue;
237
238 if (i == rulesSize || rules[i] != Q_OR)
239 break;
240 ++i;
241 }
242
243 if (orExprTruthValue)
244 return result;
245
246 ++result;
247
248 if (i == rulesSize)
249 return result;
250
251 i++; // Q_NEWRULE
252 }
253
254 Q_ASSERT(false);
255 return 0;
256}
257
258class QTranslatorPrivate : public QObjectPrivate
259{
260 Q_DECLARE_PUBLIC(QTranslator)
261public:
262 enum { Contexts = 0x2f, Hashes = 0x42, Messages = 0x69, NumerusRules = 0x88, Dependencies = 0x96, Language = 0xa7 };
263
264 QTranslatorPrivate() :
265#if defined(QT_USE_MMAP)
266 used_mmap(0),
267#endif
268 unmapPointer(nullptr), unmapLength(0), resource(nullptr),
269 messageArray(nullptr), offsetArray(nullptr), contextArray(nullptr), numerusRulesArray(nullptr),
270 messageLength(0), offsetLength(0), contextLength(0), numerusRulesLength(0) {}
271
272#if defined(QT_USE_MMAP)
273 bool used_mmap : 1;
274#endif
275 char *unmapPointer; // used memory (mmap, new or resource file)
276 qsizetype unmapLength;
277
278 // The resource object in case we loaded the translations from a resource
279 std::unique_ptr<QResource> resource;
280
281 // used if the translator has dependencies
282 std::vector<std::unique_ptr<QTranslator>> subTranslators;
283
284 // Pointers and offsets into unmapPointer[unmapLength] array, or user
285 // provided data array
286 const uchar *messageArray;
287 const uchar *offsetArray;
288 const uchar *contextArray;
289 const uchar *numerusRulesArray;
290 uint messageLength;
291 uint offsetLength;
292 uint contextLength;
293 uint numerusRulesLength;
294
295 QString language;
296 QString filePath;
297
298 bool do_load(const QString &filename, const QString &directory);
299 bool do_load(const uchar *data, qsizetype len, const QString &directory);
300 QString do_translate(const char *context, const char *sourceText, const char *comment,
301 int n) const;
302 void clear();
303};
304
305/*!
306 \class QTranslator
307 \inmodule QtCore
308
309 \brief The QTranslator class provides internationalization support for text
310 output.
311
312 \ingroup i18n
313
314 An object of this class contains a set of translations from a
315 source language to a target language. QTranslator provides
316 functions to look up translations in a translation file.
317 Translation files are created using \l{Qt Linguist}.
318
319 The most common use of QTranslator is to: load a translation
320 file, and install it using QCoreApplication::installTranslator().
321
322 Here's an example \c main() function using the
323 QTranslator:
324
325 \snippet hellotrmain.cpp 0
326
327 Note that the translator must be created \e before the
328 application's widgets.
329
330 Most applications will never need to do anything else with this
331 class. The other functions provided by this class are useful for
332 applications that work on translator files.
333
334 \section1 Looking up Translations
335
336 It is possible to look up a translation using translate() (as tr()
337 and QCoreApplication::translate() do). The translate() function takes
338 up to three parameters:
339
340 \list
341 \li The \e context - usually the class name for the tr() caller.
342 \li The \e {source text} - usually the argument to tr().
343 \li The \e disambiguation - an optional string that helps disambiguate
344 different uses of the same text in the same context.
345 \endlist
346
347 For example, the "Cancel" in a dialog might have "Anuluj" when the
348 program runs in Polish (in this case the source text would be
349 "Cancel"). The context would (normally) be the dialog's class
350 name; there would normally be no comment, and the translated text
351 would be "Anuluj".
352
353 But it's not always so simple. The Spanish version of a printer
354 dialog with settings for two-sided printing and binding would
355 probably require both "Activado" and "Activada" as translations
356 for "Enabled". In this case the source text would be "Enabled" in
357 both cases, and the context would be the dialog's class name, but
358 the two items would have disambiguations such as "two-sided printing"
359 for one and "binding" for the other. The disambiguation enables the
360 translator to choose the appropriate gender for the Spanish version,
361 and enables Qt to distinguish between translations.
362
363 \section1 Using Multiple Translations
364
365 Multiple translation files can be installed in an application.
366 Translations are searched for in the reverse order in which they were
367 installed, so the most recently installed translation file is searched
368 for translations first and the earliest translation file is searched
369 last. The search stops as soon as a translation containing a matching
370 string is found.
371
372 This mechanism makes it possible for a specific translation to be
373 "selected" or given priority over the others; simply uninstall the
374 translator from the application by passing it to the
375 QCoreApplication::removeTranslator() function and reinstall it with
376 QCoreApplication::installTranslator(). It will then be the first
377 translation to be searched for matching strings.
378
379 \sa QCoreApplication::installTranslator(), QCoreApplication::removeTranslator(),
380 QObject::tr(), QCoreApplication::translate(), {I18N Example},
381 {Hello tr() Example}, {Arrow Pad Example}, {Troll Print Example}
382*/
383
384/*!
385 Constructs an empty message file object with parent \a parent that
386 is not connected to any file.
387*/
388
389QTranslator::QTranslator(QObject * parent)
390 : QObject(*new QTranslatorPrivate, parent)
391{
392}
393
394/*!
395 Destroys the object and frees any allocated resources.
396*/
397
398QTranslator::~QTranslator()
399{
400 if (QCoreApplication::instance())
401 QCoreApplication::removeTranslator(messageFile: this);
402 Q_D(QTranslator);
403 d->clear();
404}
405
406/*!
407
408 Loads \a filename + \a suffix (".qm" if the \a suffix is not
409 specified), which may be an absolute file name or relative to \a
410 directory. Returns \c true if the translation is successfully loaded;
411 otherwise returns \c false.
412
413 If \a directory is not specified, the current directory is used
414 (i.e., as \l{QDir::}{currentPath()}).
415
416 The previous contents of this translator object are discarded.
417
418 If the file name does not exist, other file names are tried
419 in the following order:
420
421 \list 1
422 \li File name without \a suffix appended.
423 \li File name with text after a character in \a search_delimiters
424 stripped ("_." is the default for \a search_delimiters if it is
425 an empty string) and \a suffix.
426 \li File name stripped without \a suffix appended.
427 \li File name stripped further, etc.
428 \endlist
429
430 For example, an application running in the fr_CA locale
431 (French-speaking Canada) might call load("foo.fr_ca",
432 "/opt/foolib"). load() would then try to open the first existing
433 readable file from this list:
434
435 \list 1
436 \li \c /opt/foolib/foo.fr_ca.qm
437 \li \c /opt/foolib/foo.fr_ca
438 \li \c /opt/foolib/foo.fr.qm
439 \li \c /opt/foolib/foo.fr
440 \li \c /opt/foolib/foo.qm
441 \li \c /opt/foolib/foo
442 \endlist
443
444 Usually, it is better to use the QTranslator::load(const QLocale &,
445 const QString &, const QString &, const QString &, const QString &)
446 function instead, because it uses \l{QLocale::uiLanguages()} and not simply
447 the locale name, which refers to the formatting of dates and numbers and not
448 necessarily the UI language.
449*/
450
451bool QTranslator::load(const QString & filename, const QString & directory,
452 const QString & search_delimiters,
453 const QString & suffix)
454{
455 Q_D(QTranslator);
456 d->clear();
457
458 QString prefix;
459 if (QFileInfo(filename).isRelative()) {
460 prefix = directory;
461 if (prefix.size() && !prefix.endsWith(c: u'/'))
462 prefix += u'/';
463 }
464
465 const QString suffixOrDotQM = suffix.isNull() ? dotQmLiteral() : suffix;
466 QStringView fname(filename);
467 QString realname;
468 const QString delims = search_delimiters.isNull() ? QStringLiteral("_.") : search_delimiters;
469
470 for (;;) {
471 QFileInfo fi;
472
473 realname = prefix + fname + suffixOrDotQM;
474 fi.setFile(realname);
475 if (fi.isReadable() && fi.isFile())
476 break;
477
478 realname = prefix + fname;
479 fi.setFile(realname);
480 if (fi.isReadable() && fi.isFile())
481 break;
482
483 int rightmost = 0;
484 for (int i = 0; i < (int)delims.size(); i++) {
485 int k = fname.lastIndexOf(c: delims[i]);
486 if (k > rightmost)
487 rightmost = k;
488 }
489
490 // no truncations? fail
491 if (rightmost == 0)
492 return false;
493
494 fname.truncate(n: rightmost);
495 }
496
497 // realname is now the fully qualified name of a readable file.
498 return d->do_load(filename: realname, directory);
499}
500
501bool QTranslatorPrivate::do_load(const QString &realname, const QString &directory)
502{
503 QTranslatorPrivate *d = this;
504 bool ok = false;
505
506 if (realname.startsWith(c: u':')) {
507 // If the translation is in a non-compressed resource file, the data is already in
508 // memory, so no need to use QFile to copy it again.
509 Q_ASSERT(!d->resource);
510 d->resource = std::make_unique<QResource>(args: realname);
511 if (resource->isValid() && resource->compressionAlgorithm() == QResource::NoCompression
512 && resource->size() >= MagicLength
513 && !memcmp(s1: resource->data(), s2: magic, n: MagicLength)) {
514 d->unmapLength = resource->size();
515 d->unmapPointer = reinterpret_cast<char *>(const_cast<uchar *>(resource->data()));
516#if defined(QT_USE_MMAP)
517 d->used_mmap = false;
518#endif
519 ok = true;
520 } else {
521 resource = nullptr;
522 }
523 }
524
525 if (!ok) {
526 QFile file(realname);
527 if (!file.open(flags: QIODevice::ReadOnly | QIODevice::Unbuffered))
528 return false;
529
530 qint64 fileSize = file.size();
531 if (fileSize < MagicLength || fileSize > std::numeric_limits<qsizetype>::max())
532 return false;
533
534 {
535 char magicBuffer[MagicLength];
536 if (MagicLength != file.read(data: magicBuffer, maxlen: MagicLength)
537 || memcmp(s1: magicBuffer, s2: magic, n: MagicLength))
538 return false;
539 }
540
541 d->unmapLength = qsizetype(fileSize);
542
543#ifdef QT_USE_MMAP
544
545#ifndef MAP_FILE
546#define MAP_FILE 0
547#endif
548#ifndef MAP_FAILED
549#define MAP_FAILED reinterpret_cast<void *>(-1)
550#endif
551
552 int fd = file.handle();
553 if (fd >= 0) {
554 int protection = PROT_READ; // read-only memory
555 int flags = MAP_FILE | MAP_PRIVATE; // swap-backed map from file
556 void *ptr = QT_MMAP(addr: nullptr, len: d->unmapLength,// any address, whole file
557 prot: protection, flags: flags,
558 fd: fd, offset: 0); // from offset 0 of fd
559 if (ptr != MAP_FAILED) {
560 file.close();
561 d->used_mmap = true;
562 d->unmapPointer = static_cast<char *>(ptr);
563 ok = true;
564 }
565 }
566#endif // QT_USE_MMAP
567
568 if (!ok) {
569 d->unmapPointer = new (std::nothrow) char[d->unmapLength];
570 if (d->unmapPointer) {
571 file.seek(offset: 0);
572 qint64 readResult = file.read(data: d->unmapPointer, maxlen: d->unmapLength);
573 if (readResult == qint64(unmapLength))
574 ok = true;
575 }
576 }
577 }
578
579 if (ok) {
580 const QString base_dir =
581 !directory.isEmpty() ? directory : QFileInfo(realname).absolutePath();
582 if (d->do_load(data: reinterpret_cast<const uchar *>(d->unmapPointer), len: d->unmapLength,
583 directory: base_dir)) {
584 d->filePath = realname;
585 return true;
586 }
587 }
588
589#if defined(QT_USE_MMAP)
590 if (used_mmap) {
591 used_mmap = false;
592 munmap(addr: unmapPointer, len: unmapLength);
593 } else
594#endif
595 if (!d->resource)
596 delete [] unmapPointer;
597
598 d->resource = nullptr;
599 d->unmapPointer = nullptr;
600 d->unmapLength = 0;
601
602 return false;
603}
604
605Q_NEVER_INLINE
606static bool is_readable_file(const QString &name)
607{
608 const QFileInfo fi(name);
609 const bool isReadableFile = fi.isReadable() && fi.isFile();
610 qCDebug(lcTranslator) << "Testing file" << name << isReadableFile;
611
612 return isReadableFile;
613}
614
615static QString find_translation(const QLocale & locale,
616 const QString & filename,
617 const QString & prefix,
618 const QString & directory,
619 const QString & suffix)
620{
621 qCDebug(lcTranslator).noquote().nospace() << "Searching translation for "
622 << filename << prefix << locale << suffix
623 << " in " << directory;
624 QString path;
625 if (QFileInfo(filename).isRelative()) {
626 path = directory;
627 if (!path.isEmpty() && !path.endsWith(c: u'/'))
628 path += u'/';
629 }
630 const QString suffixOrDotQM = suffix.isNull() ? dotQmLiteral() : suffix;
631
632 QString realname;
633 realname += path + filename + prefix; // using += in the hope for some reserve capacity
634 const qsizetype realNameBaseSize = realname.size();
635
636 // see http://www.unicode.org/reports/tr35/#LanguageMatching for inspiration
637
638 // For each language_country returned by locale.uiLanguages(), add
639 // also a lowercase version to the list. Since these languages are
640 // used to create file names, this is important on case-sensitive
641 // file systems, where otherwise a file called something like
642 // "prefix_en_us.qm" won't be found under the "en_US" locale. Note
643 // that the Qt resource system is always case-sensitive, even on
644 // Windows (in other words: this codepath is *not* UNIX-only).
645 QStringList languages = locale.uiLanguages(separator: QLocale::TagSeparator::Underscore);
646 qCDebug(lcTranslator) << "Requested UI languages" << languages;
647
648 QDuplicateTracker<QString> duplicates(languages.size() * 2);
649 for (const auto &l : std::as_const(t&: languages))
650 (void)duplicates.hasSeen(s: l);
651
652 for (qsizetype i = languages.size() - 1; i >= 0; --i) {
653 QString language = languages.at(i);
654
655 // Add candidates for each entry where we progressively truncate sections
656 // from the end, until a matching language tag is found. For compatibility
657 // reasons (see QTBUG-124898) we add a special case: if we find a
658 // language_Script_Territory entry (i.e. an entry with two sections), try
659 // language_Territory as well as language_Script. Use QDuplicateTracker
660 // so that we don't add any entries as fallbacks that are already in the
661 // list anyway.
662 // This is a kludge, and such entries are added at the end of the candidate
663 // list; from 6.9 on, this is fixed in QLocale::uiLanguages().
664 QStringList fallbacks;
665 const auto addIfNew = [&duplicates, &fallbacks](const QString &fallback) {
666 if (!duplicates.hasSeen(s: fallback))
667 fallbacks.append(t: fallback);
668 };
669
670 while (true) {
671 const qsizetype last = language.lastIndexOf(c: u'_');
672 if (last < 0) // no more sections
673 break;
674
675 const qsizetype first = language.indexOf(c: u'_');
676 // two sections, add fallback without script
677 if (first != last && language.count(c: u'_') == 2) {
678 QString fallback = language.left(n: first) + language.mid(position: last);
679 addIfNew(fallback);
680 }
681 QString fallback = language.left(n: last);
682 addIfNew(fallback);
683
684 language.truncate(pos: last);
685 }
686 for (qsizetype j = fallbacks.size() - 1; j >= 0; --j)
687 languages.insert(i: i + 1, t: fallbacks.at(i: j));
688 }
689
690 qCDebug(lcTranslator) << "Augmented UI languages" << languages;
691 for (qsizetype i = languages.size() - 1; i >= 0; --i) {
692 const QString &lang = languages.at(i);
693 QString lowerLang = lang.toLower();
694 if (lang != lowerLang)
695 languages.insert(i: i + 1, t: lowerLang);
696 }
697
698 for (QString localeName : std::as_const(t&: languages)) {
699 // try each locale with and without suffix
700 realname += localeName + suffixOrDotQM;
701 if (is_readable_file(name: realname))
702 return realname;
703
704 realname.truncate(pos: realNameBaseSize + localeName.size());
705 if (is_readable_file(name: realname))
706 return realname;
707
708 realname.truncate(pos: realNameBaseSize);
709 }
710
711 const int realNameBaseSizeFallbacks = path.size() + filename.size();
712
713 // realname == path + filename + prefix;
714 if (!suffix.isNull()) {
715 realname.replace(i: realNameBaseSizeFallbacks, len: prefix.size(), after: suffix);
716 // realname == path + filename;
717 if (is_readable_file(name: realname))
718 return realname;
719 realname.replace(i: realNameBaseSizeFallbacks, len: suffix.size(), after: prefix);
720 }
721
722 // realname == path + filename + prefix;
723 if (is_readable_file(name: realname))
724 return realname;
725
726 realname.truncate(pos: realNameBaseSizeFallbacks);
727 // realname == path + filename;
728 if (is_readable_file(name: realname))
729 return realname;
730
731 realname.truncate(pos: 0);
732 return realname;
733}
734
735/*!
736 \since 4.8
737
738 Loads \a filename + \a prefix + \l{QLocale::uiLanguages()}{ui language
739 name} + \a suffix (".qm" if the \a suffix is not specified), which may be
740 an absolute file name or relative to \a directory. Returns \c true if the
741 translation is successfully loaded; otherwise returns \c false.
742
743 The previous contents of this translator object are discarded.
744
745 If the file name does not exist, other file names are tried
746 in the following order:
747
748 \list 1
749 \li File name without \a suffix appended.
750 \li File name with ui language part after a "_" character stripped and \a suffix.
751 \li File name with ui language part stripped without \a suffix appended.
752 \li File name with ui language part stripped further, etc.
753 \endlist
754
755 For example, an application running in the \a locale with the following
756 \l{QLocale::uiLanguages()}{ui languages} - "es", "fr-CA", "de" might call
757 load(QLocale(), "foo", ".", "/opt/foolib", ".qm"). load() would
758 replace '-' (dash) with '_' (underscore) in the ui language and then try to
759 open the first existing readable file from this list:
760
761 \list 1
762 \li \c /opt/foolib/foo.es.qm
763 \li \c /opt/foolib/foo.es
764 \li \c /opt/foolib/foo.fr_CA.qm
765 \li \c /opt/foolib/foo.fr_CA
766 \li \c /opt/foolib/foo.fr.qm
767 \li \c /opt/foolib/foo.fr
768 \li \c /opt/foolib/foo.de.qm
769 \li \c /opt/foolib/foo.de
770 \li \c /opt/foolib/foo.qm
771 \li \c /opt/foolib/foo.
772 \li \c /opt/foolib/foo
773 \endlist
774
775 On operating systems where file system is case sensitive, QTranslator also
776 tries to load a lower-cased version of the locale name.
777*/
778bool QTranslator::load(const QLocale & locale,
779 const QString & filename,
780 const QString & prefix,
781 const QString & directory,
782 const QString & suffix)
783{
784 Q_D(QTranslator);
785 d->clear();
786 QString fname = find_translation(locale, filename, prefix, directory, suffix);
787 return !fname.isEmpty() && d->do_load(realname: fname, directory);
788}
789
790/*!
791 \overload load()
792
793 Loads the QM file data \a data of length \a len into the
794 translator.
795
796 The data is not copied. The caller must be able to guarantee that \a data
797 will not be deleted or modified.
798
799 \a directory is only used to specify the base directory when loading the dependencies
800 of a QM file. If the file does not have dependencies, this argument is ignored.
801*/
802bool QTranslator::load(const uchar *data, int len, const QString &directory)
803{
804 Q_D(QTranslator);
805 d->clear();
806
807 if (!data || len < MagicLength || memcmp(s1: data, s2: magic, n: MagicLength))
808 return false;
809
810 return d->do_load(data, len, directory);
811}
812
813static quint8 read8(const uchar *data)
814{
815 return qFromBigEndian<quint8>(src: data);
816}
817
818static quint16 read16(const uchar *data)
819{
820 return qFromBigEndian<quint16>(src: data);
821}
822
823static quint32 read32(const uchar *data)
824{
825 return qFromBigEndian<quint32>(src: data);
826}
827
828bool QTranslatorPrivate::do_load(const uchar *data, qsizetype len, const QString &directory)
829{
830 bool ok = true;
831 const uchar *end = data + len;
832
833 data += MagicLength;
834
835 QStringList dependencies;
836 while (data < end - 5) {
837 quint8 tag = read8(data: data++);
838 quint32 blockLen = read32(data);
839 data += 4;
840 if (!tag || !blockLen)
841 break;
842 if (quint32(end - data) < blockLen) {
843 ok = false;
844 break;
845 }
846
847 if (tag == QTranslatorPrivate::Language) {
848 language = QString::fromUtf8(utf8: (const char *)data, size: blockLen);
849 } else if (tag == QTranslatorPrivate::Contexts) {
850 contextArray = data;
851 contextLength = blockLen;
852 } else if (tag == QTranslatorPrivate::Hashes) {
853 offsetArray = data;
854 offsetLength = blockLen;
855 } else if (tag == QTranslatorPrivate::Messages) {
856 messageArray = data;
857 messageLength = blockLen;
858 } else if (tag == QTranslatorPrivate::NumerusRules) {
859 numerusRulesArray = data;
860 numerusRulesLength = blockLen;
861 } else if (tag == QTranslatorPrivate::Dependencies) {
862 QDataStream stream(QByteArray::fromRawData(data: (const char*)data, size: blockLen));
863 QString dep;
864 while (!stream.atEnd()) {
865 stream >> dep;
866 dependencies.append(t: dep);
867 }
868 }
869
870 data += blockLen;
871 }
872
873 if (ok && !isValidNumerusRules(rules: numerusRulesArray, rulesSize: numerusRulesLength))
874 ok = false;
875 if (ok) {
876 subTranslators.reserve(n: std::size_t(dependencies.size()));
877 for (const QString &dependency : std::as_const(t&: dependencies)) {
878 auto translator = std::make_unique<QTranslator>();
879 ok = translator->load(filename: dependency, directory);
880 if (!ok)
881 break;
882 subTranslators.push_back(x: std::move(translator));
883 }
884
885 // In case some dependencies fail to load, unload all the other ones too.
886 if (!ok)
887 subTranslators.clear();
888 }
889
890 if (!ok) {
891 messageArray = nullptr;
892 contextArray = nullptr;
893 offsetArray = nullptr;
894 numerusRulesArray = nullptr;
895 messageLength = 0;
896 contextLength = 0;
897 offsetLength = 0;
898 numerusRulesLength = 0;
899 }
900
901 return ok;
902}
903
904static QString getMessage(const uchar *m, const uchar *end, const char *context,
905 const char *sourceText, const char *comment, uint numerus)
906{
907 const uchar *tn = nullptr;
908 uint tn_length = 0;
909 const uint sourceTextLen = uint(strlen(s: sourceText));
910 const uint contextLen = uint(strlen(s: context));
911 const uint commentLen = uint(strlen(s: comment));
912
913 for (;;) {
914 uchar tag = 0;
915 if (m < end)
916 tag = read8(data: m++);
917 switch ((Tag)tag) {
918 case Tag_End:
919 goto end;
920 case Tag_Translation: {
921 int len = read32(data: m);
922 if (len & 1)
923 return QString();
924 m += 4;
925 if (!numerus--) {
926 tn_length = len;
927 tn = m;
928 }
929 m += len;
930 break;
931 }
932 case Tag_Obsolete1:
933 m += 4;
934 break;
935 case Tag_SourceText: {
936 quint32 len = read32(data: m);
937 m += 4;
938 if (!match(found: m, foundLen: len, target: sourceText, targetLen: sourceTextLen))
939 return QString();
940 m += len;
941 }
942 break;
943 case Tag_Context: {
944 quint32 len = read32(data: m);
945 m += 4;
946 if (!match(found: m, foundLen: len, target: context, targetLen: contextLen))
947 return QString();
948 m += len;
949 }
950 break;
951 case Tag_Comment: {
952 quint32 len = read32(data: m);
953 m += 4;
954 if (*m && !match(found: m, foundLen: len, target: comment, targetLen: commentLen))
955 return QString();
956 m += len;
957 }
958 break;
959 default:
960 return QString();
961 }
962 }
963end:
964 if (!tn)
965 return QString();
966 QString str(tn_length / 2, Qt::Uninitialized);
967 qFromBigEndian<char16_t>(source: tn, count: str.size(), dest: str.data());
968 return str;
969}
970
971QString QTranslatorPrivate::do_translate(const char *context, const char *sourceText,
972 const char *comment, int n) const
973{
974 if (context == nullptr)
975 context = "";
976 if (sourceText == nullptr)
977 sourceText = "";
978 if (comment == nullptr)
979 comment = "";
980
981 uint numerus = 0;
982 size_t numItems = 0;
983
984 if (!offsetLength)
985 goto searchDependencies;
986
987 /*
988 Check if the context belongs to this QTranslator. If many
989 translators are installed, this step is necessary.
990 */
991 if (contextLength) {
992 quint16 hTableSize = read16(data: contextArray);
993 uint g = elfHash(name: context) % hTableSize;
994 const uchar *c = contextArray + 2 + (g << 1);
995 quint16 off = read16(data: c);
996 c += 2;
997 if (off == 0)
998 return QString();
999 c = contextArray + (2 + (hTableSize << 1) + (off << 1));
1000
1001 const uint contextLen = uint(strlen(s: context));
1002 for (;;) {
1003 quint8 len = read8(data: c++);
1004 if (len == 0)
1005 return QString();
1006 if (match(found: c, foundLen: len, target: context, targetLen: contextLen))
1007 break;
1008 c += len;
1009 }
1010 }
1011
1012 numItems = offsetLength / (2 * sizeof(quint32));
1013 if (!numItems)
1014 goto searchDependencies;
1015
1016 if (n >= 0)
1017 numerus = numerusHelper(n, rules: numerusRulesArray, rulesSize: numerusRulesLength);
1018
1019 for (;;) {
1020 quint32 h = 0;
1021 elfHash_continue(name: sourceText, h);
1022 elfHash_continue(name: comment, h);
1023 elfHash_finish(h);
1024
1025 const uchar *start = offsetArray;
1026 const uchar *end = start + ((numItems - 1) << 3);
1027 while (start <= end) {
1028 const uchar *middle = start + (((end - start) >> 4) << 3);
1029 uint hash = read32(data: middle);
1030 if (h == hash) {
1031 start = middle;
1032 break;
1033 } else if (hash < h) {
1034 start = middle + 8;
1035 } else {
1036 end = middle - 8;
1037 }
1038 }
1039
1040 if (start <= end) {
1041 // go back on equal key
1042 while (start != offsetArray && read32(data: start) == read32(data: start - 8))
1043 start -= 8;
1044
1045 while (start < offsetArray + offsetLength) {
1046 quint32 rh = read32(data: start);
1047 start += 4;
1048 if (rh != h)
1049 break;
1050 quint32 ro = read32(data: start);
1051 start += 4;
1052 QString tn = getMessage(m: messageArray + ro, end: messageArray + messageLength, context,
1053 sourceText, comment, numerus);
1054 if (!tn.isNull())
1055 return tn;
1056 }
1057 }
1058 if (!comment[0])
1059 break;
1060 comment = "";
1061 }
1062
1063searchDependencies:
1064 for (const auto &translator : subTranslators) {
1065 QString tn = translator->translate(context, sourceText, disambiguation: comment, n);
1066 if (!tn.isNull())
1067 return tn;
1068 }
1069 return QString();
1070}
1071
1072/*
1073 Empties this translator of all contents.
1074
1075 This function works with stripped translator files.
1076*/
1077
1078void QTranslatorPrivate::clear()
1079{
1080 Q_Q(QTranslator);
1081 if (unmapPointer && unmapLength) {
1082#if defined(QT_USE_MMAP)
1083 if (used_mmap) {
1084 used_mmap = false;
1085 munmap(addr: unmapPointer, len: unmapLength);
1086 } else
1087#endif
1088 if (!resource)
1089 delete [] unmapPointer;
1090 }
1091
1092 resource = nullptr;
1093 unmapPointer = nullptr;
1094 unmapLength = 0;
1095 messageArray = nullptr;
1096 contextArray = nullptr;
1097 offsetArray = nullptr;
1098 numerusRulesArray = nullptr;
1099 messageLength = 0;
1100 contextLength = 0;
1101 offsetLength = 0;
1102 numerusRulesLength = 0;
1103
1104 subTranslators.clear();
1105
1106 language.clear();
1107 filePath.clear();
1108
1109 if (QCoreApplicationPrivate::isTranslatorInstalled(translator: q))
1110 QCoreApplication::postEvent(receiver: QCoreApplication::instance(),
1111 event: new QEvent(QEvent::LanguageChange));
1112}
1113
1114/*!
1115 Returns the translation for the key (\a context, \a sourceText,
1116 \a disambiguation). If none is found, also tries (\a context, \a
1117 sourceText, ""). If that still fails, returns a null string.
1118
1119 \note Incomplete translations may result in unexpected behavior:
1120 If no translation for (\a context, \a sourceText, "")
1121 is provided, the method might in this case actually return a
1122 translation for a different \a disambiguation.
1123
1124 If \a n is not -1, it is used to choose an appropriate form for
1125 the translation (e.g. "%n file found" vs. "%n files found").
1126
1127 If you need to programmatically insert translations into a
1128 QTranslator, this function can be reimplemented.
1129
1130 \sa load()
1131*/
1132QString QTranslator::translate(const char *context, const char *sourceText, const char *disambiguation,
1133 int n) const
1134{
1135 Q_D(const QTranslator);
1136 return d->do_translate(context, sourceText, comment: disambiguation, n);
1137}
1138
1139/*!
1140 Returns \c true if this translator is empty, otherwise returns \c false.
1141 This function works with stripped and unstripped translation files.
1142*/
1143bool QTranslator::isEmpty() const
1144{
1145 Q_D(const QTranslator);
1146 return !d->messageArray && !d->offsetArray && !d->contextArray
1147 && d->subTranslators.empty();
1148}
1149
1150/*!
1151 \since 5.15
1152
1153 Returns the target language as stored in the translation file.
1154 */
1155QString QTranslator::language() const
1156{
1157 Q_D(const QTranslator);
1158 return d->language;
1159}
1160
1161/*!
1162 \since 5.15
1163
1164 Returns the path of the loaded translation file.
1165
1166 The file path is empty if no translation was loaded yet,
1167 the loading failed, or if the translation was not loaded
1168 from a file.
1169 */
1170QString QTranslator::filePath() const
1171{
1172 Q_D(const QTranslator);
1173 return d->filePath;
1174}
1175
1176QT_END_NAMESPACE
1177
1178#include "moc_qtranslator.cpp"
1179
1180#endif // QT_NO_TRANSLATION
1181

source code of qtbase/src/corelib/kernel/qtranslator.cpp