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 | |
44 | QT_BEGIN_NAMESPACE |
45 | |
46 | static Q_LOGGING_CATEGORY(lcTranslator, "qt.core.qtranslator" ) |
47 | |
48 | namespace { |
49 | enum Tag { Tag_End = 1, Tag_SourceText16, Tag_Translation, Tag_Context16, Tag_Obsolete1, |
50 | Tag_SourceText, Tag_Context, , Tag_Obsolete2 }; |
51 | } |
52 | |
53 | /* |
54 | $ mcookie |
55 | 3cb86418caef9c95cd211cbf60a1bddd |
56 | $ |
57 | */ |
58 | |
59 | // magic number for the file |
60 | static const int MagicLength = 16; |
61 | static const uchar magic[MagicLength] = { |
62 | 0x3c, 0xb8, 0x64, 0x18, 0xca, 0xef, 0x9c, 0x95, |
63 | 0xcd, 0x21, 0x1c, 0xbf, 0x60, 0xa1, 0xbd, 0xdd |
64 | }; |
65 | |
66 | static inline QString dotQmLiteral() { return QStringLiteral(".qm" ); } |
67 | |
68 | static 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 | |
77 | static 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 | |
91 | static void elfHash_finish(uint &h) |
92 | { |
93 | if (!h) |
94 | h = 1; |
95 | } |
96 | |
97 | static 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 | */ |
111 | static 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 | */ |
179 | static 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 | |
258 | class QTranslatorPrivate : public QObjectPrivate |
259 | { |
260 | Q_DECLARE_PUBLIC(QTranslator) |
261 | public: |
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 *, |
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 | |
389 | QTranslator::QTranslator(QObject * parent) |
390 | : QObject(*new QTranslatorPrivate, parent) |
391 | { |
392 | } |
393 | |
394 | /*! |
395 | Destroys the object and frees any allocated resources. |
396 | */ |
397 | |
398 | QTranslator::~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 | |
451 | bool 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 | |
501 | bool 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 | |
605 | Q_NEVER_INLINE |
606 | static 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 | |
615 | static 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 | */ |
778 | bool 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 | */ |
802 | bool 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 | |
813 | static quint8 read8(const uchar *data) |
814 | { |
815 | return qFromBigEndian<quint8>(src: data); |
816 | } |
817 | |
818 | static quint16 read16(const uchar *data) |
819 | { |
820 | return qFromBigEndian<quint16>(src: data); |
821 | } |
822 | |
823 | static quint32 read32(const uchar *data) |
824 | { |
825 | return qFromBigEndian<quint32>(src: data); |
826 | } |
827 | |
828 | bool 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 | |
904 | static QString getMessage(const uchar *m, const uchar *end, const char *context, |
905 | const char *sourceText, const char *, 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 = 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 | } |
963 | end: |
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 | |
971 | QString QTranslatorPrivate::do_translate(const char *context, const char *sourceText, |
972 | const char *, 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 | |
1063 | searchDependencies: |
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 | |
1078 | void 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 | */ |
1132 | QString 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 | */ |
1143 | bool 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 | */ |
1155 | QString 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 | */ |
1170 | QString QTranslator::filePath() const |
1171 | { |
1172 | Q_D(const QTranslator); |
1173 | return d->filePath; |
1174 | } |
1175 | |
1176 | QT_END_NAMESPACE |
1177 | |
1178 | #include "moc_qtranslator.cpp" |
1179 | |
1180 | #endif // QT_NO_TRANSLATION |
1181 | |