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