1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include "translator.h"
5
6#ifndef QT_BOOTSTRAPPED
7#include <QtCore/QCoreApplication>
8#endif
9#include <QtCore/QDataStream>
10#include <QtCore/QDebug>
11#include <QtCore/QDir>
12#include <QtCore/QFile>
13#include <QtCore/QFileInfo>
14#include <QtCore/QMap>
15#include <QtCore/QString>
16#include <QtCore/QStringDecoder>
17
18QT_BEGIN_NAMESPACE
19
20// magic number for the file
21static const int MagicLength = 16;
22static const uchar magic[MagicLength] = {
23 0x3c, 0xb8, 0x64, 0x18, 0xca, 0xef, 0x9c, 0x95,
24 0xcd, 0x21, 0x1c, 0xbf, 0x60, 0xa1, 0xbd, 0xdd
25};
26
27
28namespace {
29
30enum Tag {
31 Tag_End = 1,
32 Tag_SourceText16 = 2,
33 Tag_Translation = 3,
34 Tag_Context16 = 4,
35 Tag_Obsolete1 = 5,
36 Tag_SourceText = 6,
37 Tag_Context = 7,
38 Tag_Comment = 8,
39 Tag_Obsolete2 = 9
40};
41
42enum Prefix {
43 NoPrefix,
44 Hash,
45 HashContext,
46 HashContextSourceText,
47 HashContextSourceTextComment
48};
49
50} // namespace anon
51
52static uint elfHash(const QByteArray &ba)
53{
54 const uchar *k = (const uchar *)ba.data();
55 uint h = 0;
56 uint g;
57
58 if (k) {
59 while (*k) {
60 h = (h << 4) + *k++;
61 if ((g = (h & 0xf0000000)) != 0)
62 h ^= g >> 24;
63 h &= ~g;
64 }
65 }
66 if (!h)
67 h = 1;
68 return h;
69}
70
71class ByteTranslatorMessage
72{
73public:
74 ByteTranslatorMessage(
75 const QByteArray &context,
76 const QByteArray &sourceText,
77 const QByteArray &comment,
78 const QStringList &translations) :
79 m_context(context),
80 m_sourcetext(sourceText),
81 m_comment(comment),
82 m_translations(translations)
83 {}
84 const QByteArray &context() const { return m_context; }
85 const QByteArray &sourceText() const { return m_sourcetext; }
86 const QByteArray &comment() const { return m_comment; }
87 const QStringList &translations() const { return m_translations; }
88 bool operator<(const ByteTranslatorMessage& m) const;
89
90private:
91 QByteArray m_context;
92 QByteArray m_sourcetext;
93 QByteArray m_comment;
94 QStringList m_translations;
95};
96
97Q_DECLARE_TYPEINFO(ByteTranslatorMessage, Q_RELOCATABLE_TYPE);
98
99bool ByteTranslatorMessage::operator<(const ByteTranslatorMessage& m) const
100{
101 if (m_context != m.m_context)
102 return m_context < m.m_context;
103 if (m_sourcetext != m.m_sourcetext)
104 return m_sourcetext < m.m_sourcetext;
105 return m_comment < m.m_comment;
106}
107
108class Releaser
109{
110public:
111 struct Offset {
112 Offset()
113 : h(0), o(0)
114 {}
115 Offset(uint hash, uint offset)
116 : h(hash), o(offset)
117 {}
118
119 bool operator<(const Offset &other) const {
120 return (h != other.h) ? h < other.h : o < other.o;
121 }
122 bool operator==(const Offset &other) const {
123 return h == other.h && o == other.o;
124 }
125 uint h;
126 uint o;
127 };
128
129 enum { Contexts = 0x2f, Hashes = 0x42, Messages = 0x69, NumerusRules = 0x88, Dependencies = 0x96, Language = 0xa7 };
130
131 Releaser(const QString &language) : m_language(language) {}
132
133 bool save(QIODevice *iod);
134
135 void insert(const TranslatorMessage &msg, const QStringList &tlns, bool forceComment);
136 void insertIdBased(const TranslatorMessage &message, const QStringList &tlns);
137
138 void squeeze(TranslatorSaveMode mode);
139
140 void setNumerusRules(const QByteArray &rules);
141 void setDependencies(const QStringList &dependencies);
142
143private:
144 Q_DISABLE_COPY(Releaser)
145
146 // This should reproduce the byte array fetched from the source file, which
147 // on turn should be the same as passed to the actual tr(...) calls
148 QByteArray originalBytes(const QString &str) const;
149
150 static Prefix commonPrefix(const ByteTranslatorMessage &m1, const ByteTranslatorMessage &m2);
151
152 static uint msgHash(const ByteTranslatorMessage &msg);
153
154 void writeMessage(const ByteTranslatorMessage & msg, QDataStream & stream,
155 TranslatorSaveMode strip, Prefix prefix) const;
156
157 QString m_language;
158 // for squeezed but non-file data, this is what needs to be deleted
159 QByteArray m_messageArray;
160 QByteArray m_offsetArray;
161 QByteArray m_contextArray;
162 QMap<ByteTranslatorMessage, void *> m_messages;
163 QByteArray m_numerusRules;
164 QStringList m_dependencies;
165 QByteArray m_dependencyArray;
166};
167
168QByteArray Releaser::originalBytes(const QString &str) const
169{
170 if (str.isEmpty()) {
171 // Do not use QByteArray() here as the result of the serialization
172 // will be different.
173 return QByteArray("");
174 }
175 return str.toUtf8();
176}
177
178uint Releaser::msgHash(const ByteTranslatorMessage &msg)
179{
180 return elfHash(ba: msg.sourceText() + msg.comment());
181}
182
183Prefix Releaser::commonPrefix(const ByteTranslatorMessage &m1, const ByteTranslatorMessage &m2)
184{
185 if (msgHash(msg: m1) != msgHash(msg: m2))
186 return NoPrefix;
187 if (m1.context() != m2.context())
188 return Hash;
189 if (m1.sourceText() != m2.sourceText())
190 return HashContext;
191 if (m1.comment() != m2.comment())
192 return HashContextSourceText;
193 return HashContextSourceTextComment;
194}
195
196void Releaser::writeMessage(const ByteTranslatorMessage &msg, QDataStream &stream,
197 TranslatorSaveMode mode, Prefix prefix) const
198{
199 for (int i = 0; i < msg.translations().size(); ++i)
200 stream << quint8(Tag_Translation) << msg.translations().at(i);
201
202 if (mode == SaveEverything)
203 prefix = HashContextSourceTextComment;
204
205 // lrelease produces "wrong" QM files for QByteArrays that are .isNull().
206 switch (prefix) {
207 default:
208 case HashContextSourceTextComment:
209 stream << quint8(Tag_Comment) << msg.comment();
210 Q_FALLTHROUGH();
211 case HashContextSourceText:
212 stream << quint8(Tag_SourceText) << msg.sourceText();
213 Q_FALLTHROUGH();
214 case HashContext:
215 stream << quint8(Tag_Context) << msg.context();
216 break;
217 }
218
219 stream << quint8(Tag_End);
220}
221
222
223bool Releaser::save(QIODevice *iod)
224{
225 QDataStream s(iod);
226 s.writeRawData((const char *)magic, len: MagicLength);
227
228 if (!m_language.isEmpty()) {
229 QByteArray lang = originalBytes(str: m_language);
230 quint32 las = quint32(lang.size());
231 s << quint8(Language) << las;
232 s.writeRawData(lang, len: las);
233 }
234 if (!m_dependencyArray.isEmpty()) {
235 quint32 das = quint32(m_dependencyArray.size());
236 s << quint8(Dependencies) << das;
237 s.writeRawData(m_dependencyArray.constData(), len: das);
238 }
239 if (!m_offsetArray.isEmpty()) {
240 quint32 oas = quint32(m_offsetArray.size());
241 s << quint8(Hashes) << oas;
242 s.writeRawData(m_offsetArray.constData(), len: oas);
243 }
244 if (!m_messageArray.isEmpty()) {
245 quint32 mas = quint32(m_messageArray.size());
246 s << quint8(Messages) << mas;
247 s.writeRawData(m_messageArray.constData(), len: mas);
248 }
249 if (!m_contextArray.isEmpty()) {
250 quint32 cas = quint32(m_contextArray.size());
251 s << quint8(Contexts) << cas;
252 s.writeRawData(m_contextArray.constData(), len: cas);
253 }
254 if (!m_numerusRules.isEmpty()) {
255 quint32 nrs = m_numerusRules.size();
256 s << quint8(NumerusRules) << nrs;
257 s.writeRawData(m_numerusRules.constData(), len: nrs);
258 }
259 return true;
260}
261
262void Releaser::squeeze(TranslatorSaveMode mode)
263{
264 m_dependencyArray.clear();
265 QDataStream depstream(&m_dependencyArray, QIODevice::WriteOnly);
266 for (const QString &dep : std::as_const(t&: m_dependencies))
267 depstream << dep;
268
269 if (m_messages.isEmpty() && mode == SaveEverything)
270 return;
271
272 const auto messages = m_messages;
273
274 // re-build contents
275 m_messageArray.clear();
276 m_offsetArray.clear();
277 m_contextArray.clear();
278 m_messages.clear();
279
280 QMap<Offset, void *> offsets;
281
282 QDataStream ms(&m_messageArray, QIODevice::WriteOnly);
283 int cpPrev = 0, cpNext = 0;
284 for (auto it = messages.cbegin(), end = messages.cend(); it != end; ++it) {
285 cpPrev = cpNext;
286 const auto next = std::next(x: it);
287 if (next == end)
288 cpNext = 0;
289 else
290 cpNext = commonPrefix(m1: it.key(), m2: next.key());
291 offsets.insert(key: Offset(msgHash(msg: it.key()), ms.device()->pos()), value: (void *)0);
292 writeMessage(msg: it.key(), stream&: ms, mode, prefix: Prefix(qMax(a: cpPrev, b: cpNext + 1)));
293 }
294
295 auto offset = offsets.cbegin();
296 QDataStream ds(&m_offsetArray, QIODevice::WriteOnly);
297 while (offset != offsets.cend()) {
298 Offset k = offset.key();
299 ++offset;
300 ds << quint32(k.h) << quint32(k.o);
301 }
302
303 if (mode == SaveStripped) {
304 QMap<QByteArray, int> contextSet;
305 for (auto it = messages.cbegin(), end = messages.cend(); it != end; ++it)
306 ++contextSet[it.key().context()];
307
308 quint16 hTableSize;
309 if (contextSet.size() < 200)
310 hTableSize = (contextSet.size() < 60) ? 151 : 503;
311 else if (contextSet.size() < 2500)
312 hTableSize = (contextSet.size() < 750) ? 1511 : 5003;
313 else
314 hTableSize = (contextSet.size() < 10000) ? 15013 : 3 * contextSet.size() / 2;
315
316 QMultiMap<int, QByteArray> hashMap;
317 for (auto c = contextSet.cbegin(), end = contextSet.cend(); c != end; ++c)
318 hashMap.insert(key: elfHash(ba: c.key()) % hTableSize, value: c.key());
319
320 /*
321 The contexts found in this translator are stored in a hash
322 table to provide fast lookup. The context array has the
323 following format:
324
325 quint16 hTableSize;
326 quint16 hTable[hTableSize];
327 quint8 contextPool[...];
328
329 The context pool stores the contexts as Pascal strings:
330
331 quint8 len;
332 quint8 data[len];
333
334 Let's consider the look-up of context "FunnyDialog". A
335 hash value between 0 and hTableSize - 1 is computed, say h.
336 If hTable[h] is 0, "FunnyDialog" is not covered by this
337 translator. Else, we check in the contextPool at offset
338 2 * hTable[h] to see if "FunnyDialog" is one of the
339 contexts stored there, until we find it or we meet the
340 empty string.
341 */
342 m_contextArray.resize(size: 2 + (hTableSize << 1));
343 QDataStream t(&m_contextArray, QIODevice::WriteOnly);
344
345 quint16 *hTable = new quint16[hTableSize];
346 memset(s: hTable, c: 0, n: hTableSize * sizeof(quint16));
347
348 t << hTableSize;
349 t.device()->seek(pos: 2 + (hTableSize << 1));
350 t << quint16(0); // the entry at offset 0 cannot be used
351 uint upto = 2;
352
353 auto entry = hashMap.constBegin();
354 while (entry != hashMap.constEnd()) {
355 int i = entry.key();
356 hTable[i] = quint16(upto >> 1);
357
358 do {
359 const char *con = entry.value().constData();
360 uint len = uint(entry.value().size());
361 len = qMin(a: len, b: 255u);
362 t << quint8(len);
363 t.writeRawData(con, len);
364 upto += 1 + len;
365 ++entry;
366 } while (entry != hashMap.constEnd() && entry.key() == i);
367 if (upto & 0x1) {
368 // offsets have to be even
369 t << quint8(0); // empty string
370 ++upto;
371 }
372 }
373 t.device()->seek(pos: 2);
374 for (int j = 0; j < hTableSize; j++)
375 t << hTable[j];
376 delete [] hTable;
377
378 if (upto > 131072) {
379 qWarning(msg: "Releaser::squeeze: Too many contexts");
380 m_contextArray.clear();
381 }
382 }
383}
384
385void Releaser::insert(const TranslatorMessage &message, const QStringList &tlns, bool forceComment)
386{
387 ByteTranslatorMessage bmsg(originalBytes(str: message.context()),
388 originalBytes(str: message.sourceText()),
389 originalBytes(str: message.comment()),
390 tlns);
391 if (!forceComment) {
392 ByteTranslatorMessage bmsg2(
393 bmsg.context(), bmsg.sourceText(), QByteArray(""), bmsg.translations());
394 if (!m_messages.contains(key: bmsg2)) {
395 m_messages.insert(key: bmsg2, value: 0);
396 return;
397 }
398 }
399 m_messages.insert(key: bmsg, value: 0);
400}
401
402void Releaser::insertIdBased(const TranslatorMessage &message, const QStringList &tlns)
403{
404 ByteTranslatorMessage bmsg("", originalBytes(str: message.id()), "", tlns);
405 m_messages.insert(key: bmsg, value: 0);
406}
407
408void Releaser::setNumerusRules(const QByteArray &rules)
409{
410 m_numerusRules = rules;
411}
412
413void Releaser::setDependencies(const QStringList &dependencies)
414{
415 m_dependencies = dependencies;
416}
417
418static quint8 read8(const uchar *data)
419{
420 return *data;
421}
422
423static quint32 read32(const uchar *data)
424{
425 return (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | (data[3]);
426}
427
428static void fromBytes(const char *str, int len, QString *out, bool *utf8Fail)
429{
430 QStringDecoder toUnicode(QStringDecoder::Utf8, QStringDecoder::Flag::Stateless);
431 *out = toUnicode(QByteArrayView(str, len));
432 *utf8Fail = toUnicode.hasError();
433}
434
435bool loadQM(Translator &translator, QIODevice &dev, ConversionData &cd)
436{
437 QByteArray ba = dev.readAll();
438 const uchar *data = (uchar*)ba.data();
439 int len = ba.size();
440 if (len < MagicLength || memcmp(s1: data, s2: magic, n: MagicLength) != 0) {
441 cd.appendError(error: QLatin1String("QM-Format error: magic marker missing"));
442 return false;
443 }
444
445 enum { Contexts = 0x2f, Hashes = 0x42, Messages = 0x69, NumerusRules = 0x88, Dependencies = 0x96, Language = 0xa7 };
446
447 // for squeezed but non-file data, this is what needs to be deleted
448 const uchar *messageArray = nullptr;
449 const uchar *offsetArray = nullptr;
450 uint offsetLength = 0;
451
452 bool ok = true;
453 bool utf8Fail = false;
454 const uchar *end = data + len;
455
456 data += MagicLength;
457
458 while (data < end - 4) {
459 quint8 tag = read8(data: data++);
460 quint32 blockLen = read32(data);
461 //qDebug() << "TAG:" << tag << "BLOCKLEN:" << blockLen;
462 data += 4;
463 if (!tag || !blockLen)
464 break;
465 if (data + blockLen > end) {
466 ok = false;
467 break;
468 }
469
470 if (tag == Hashes) {
471 offsetArray = data;
472 offsetLength = blockLen;
473 //qDebug() << "HASHES: " << blockLen << QByteArray((const char *)data, blockLen).toHex();
474 } else if (tag == Messages) {
475 messageArray = data;
476 //qDebug() << "MESSAGES: " << blockLen << QByteArray((const char *)data, blockLen).toHex();
477 } else if (tag == Dependencies) {
478 QStringList dependencies;
479 QDataStream stream(QByteArray::fromRawData(data: (const char*)data, size: blockLen));
480 QString dep;
481 while (!stream.atEnd()) {
482 stream >> dep;
483 dependencies.append(t: dep);
484 }
485 translator.setDependencies(dependencies);
486 } else if (tag == Language) {
487 QString language;
488 fromBytes(str: (const char *)data, len: blockLen, out: &language, utf8Fail: &utf8Fail);
489 translator.setLanguageCode(language);
490 }
491
492 data += blockLen;
493 }
494
495
496 size_t numItems = offsetLength / (2 * sizeof(quint32));
497 //qDebug() << "NUMITEMS: " << numItems;
498
499 QString strProN = QLatin1String("%n");
500 QLocale::Language l;
501 QLocale::Territory c;
502 Translator::languageAndTerritory(languageCode: translator.languageCode(), langPtr: &l, territoryPtr: &c);
503 QStringList numerusForms;
504 bool guessPlurals = true;
505 if (getNumerusInfo(language: l, territory: c, rules: 0, forms: &numerusForms, gettextRules: 0))
506 guessPlurals = (numerusForms.size() == 1);
507
508 QString context, sourcetext, comment;
509 QStringList translations;
510
511 for (const uchar *start = offsetArray; start != offsetArray + (numItems << 3); start += 8) {
512 //quint32 hash = read32(start);
513 quint32 ro = read32(data: start + 4);
514 //qDebug() << "\nHASH:" << hash;
515 const uchar *m = messageArray + ro;
516
517 for (;;) {
518 uchar tag = read8(data: m++);
519 //qDebug() << "Tag:" << tag << " ADDR: " << m;
520 switch(tag) {
521 case Tag_End:
522 goto end;
523 case Tag_Translation: {
524 int len = read32(data: m);
525 m += 4;
526
527 // -1 indicates an empty string
528 // Otherwise streaming format is UTF-16 -> 2 bytes per character
529 if ((len != -1) && (len & 1)) {
530 cd.appendError(error: QLatin1String("QM-Format error"));
531 return false;
532 }
533 QString str;
534 if (len != -1)
535 str = QString((const QChar *)m, len / 2);
536 if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) {
537 for (int i = 0; i < str.size(); ++i)
538 str[i] = QChar((str.at(i).unicode() >> 8) +
539 ((str.at(i).unicode() << 8) & 0xff00));
540 }
541 translations << str;
542 m += len;
543 break;
544 }
545 case Tag_Obsolete1:
546 m += 4;
547 //qDebug() << "OBSOLETE";
548 break;
549 case Tag_SourceText: {
550 quint32 len = read32(data: m);
551 m += 4;
552 //qDebug() << "SOURCE LEN: " << len;
553 //qDebug() << "SOURCE: " << QByteArray((const char*)m, len);
554 fromBytes(str: (const char*)m, len, out: &sourcetext, utf8Fail: &utf8Fail);
555 m += len;
556 break;
557 }
558 case Tag_Context: {
559 quint32 len = read32(data: m);
560 m += 4;
561 //qDebug() << "CONTEXT LEN: " << len;
562 //qDebug() << "CONTEXT: " << QByteArray((const char*)m, len);
563 fromBytes(str: (const char*)m, len, out: &context, utf8Fail: &utf8Fail);
564 m += len;
565 break;
566 }
567 case Tag_Comment: {
568 quint32 len = read32(data: m);
569 m += 4;
570 //qDebug() << "COMMENT LEN: " << len;
571 //qDebug() << "COMMENT: " << QByteArray((const char*)m, len);
572 fromBytes(str: (const char*)m, len, out: &comment, utf8Fail: &utf8Fail);
573 m += len;
574 break;
575 }
576 default:
577 //qDebug() << "UNKNOWN TAG" << tag;
578 break;
579 }
580 }
581 end:;
582 TranslatorMessage msg;
583 msg.setType(TranslatorMessage::Finished);
584 if (translations.size() > 1) {
585 // If guessPlurals is not false here, plural form discard messages
586 // will be spewn out later.
587 msg.setPlural(true);
588 } else if (guessPlurals) {
589 // This might cause false positives, so it is a fallback only.
590 if (sourcetext.contains(s: strProN))
591 msg.setPlural(true);
592 }
593 msg.setTranslations(translations);
594 translations.clear();
595 msg.setContext(context);
596 msg.setSourceText(sourcetext);
597 msg.setComment(comment);
598 translator.append(msg);
599 }
600 if (utf8Fail) {
601 cd.appendError(error: QLatin1String("Error: File contains invalid UTF-8 sequences."));
602 return false;
603 }
604 return ok;
605}
606
607
608
609static bool containsStripped(const Translator &translator, const TranslatorMessage &msg)
610{
611 for (const TranslatorMessage &tmsg : translator.messages())
612 if (tmsg.sourceText() == msg.sourceText()
613 && tmsg.context() == msg.context()
614 && tmsg.comment().isEmpty())
615 return true;
616 return false;
617}
618
619bool saveQM(const Translator &translator, QIODevice &dev, ConversionData &cd)
620{
621 Releaser releaser(translator.languageCode());
622 QLocale::Language l;
623 QLocale::Territory c;
624 Translator::languageAndTerritory(languageCode: translator.languageCode(), langPtr: &l, territoryPtr: &c);
625 QByteArray rules;
626 if (getNumerusInfo(language: l, territory: c, rules: &rules, forms: 0, gettextRules: 0))
627 releaser.setNumerusRules(rules);
628
629 int finished = 0;
630 int unfinished = 0;
631 int untranslated = 0;
632 int missingIds = 0;
633 int droppedData = 0;
634
635 for (int i = 0; i != translator.messageCount(); ++i) {
636 const TranslatorMessage &msg = translator.message(i);
637 TranslatorMessage::Type typ = msg.type();
638 if (typ != TranslatorMessage::Obsolete && typ != TranslatorMessage::Vanished) {
639 if (cd.m_idBased && msg.id().isEmpty()) {
640 ++missingIds;
641 continue;
642 }
643 if (typ == TranslatorMessage::Unfinished) {
644 if (msg.translation().isEmpty() && !cd.m_idBased && cd.m_unTrPrefix.isEmpty()) {
645 ++untranslated;
646 continue;
647 } else {
648 if (cd.ignoreUnfinished())
649 continue;
650 ++unfinished;
651 }
652 } else {
653 ++finished;
654 }
655 QStringList tlns = msg.translations();
656 if (msg.type() == TranslatorMessage::Unfinished
657 && (cd.m_idBased || !cd.m_unTrPrefix.isEmpty()))
658 for (int j = 0; j < tlns.size(); ++j)
659 if (tlns.at(i: j).isEmpty())
660 tlns[j] = cd.m_unTrPrefix + msg.sourceText();
661 if (cd.m_idBased) {
662 if (!msg.context().isEmpty() || !msg.comment().isEmpty())
663 ++droppedData;
664 releaser.insertIdBased(message: msg, tlns);
665 } else {
666 // Drop the comment in (context, sourceText, comment),
667 // unless the context is empty,
668 // unless (context, sourceText, "") already exists or
669 // unless we already dropped the comment of (context,
670 // sourceText, comment0).
671 bool forceComment =
672 msg.comment().isEmpty()
673 || msg.context().isEmpty()
674 || containsStripped(translator, msg);
675 releaser.insert(message: msg, tlns, forceComment);
676 }
677 }
678 }
679
680 if (missingIds)
681 cd.appendError(error: QCoreApplication::translate(context: "LRelease",
682 key: "Dropped %n message(s) which had no ID.", disambiguation: 0,
683 n: missingIds));
684 if (droppedData)
685 cd.appendError(error: QCoreApplication::translate(context: "LRelease",
686 key: "Excess context/disambiguation dropped from %n message(s).", disambiguation: 0,
687 n: droppedData));
688
689 releaser.setDependencies(translator.dependencies());
690 releaser.squeeze(mode: cd.m_saveMode);
691 bool saved = releaser.save(iod: &dev);
692 if (saved && cd.isVerbose()) {
693 int generatedCount = finished + unfinished;
694 cd.appendError(error: QCoreApplication::translate(context: "LRelease",
695 key: " Generated %n translation(s) (%1 finished and %2 unfinished)", disambiguation: 0,
696 n: generatedCount).arg(a: finished).arg(a: unfinished));
697 if (untranslated)
698 cd.appendError(error: QCoreApplication::translate(context: "LRelease",
699 key: " Ignored %n untranslated source text(s)", disambiguation: 0,
700 n: untranslated));
701 }
702 return saved;
703}
704
705int initQM()
706{
707 Translator::FileFormat format;
708
709 format.extension = QLatin1String("qm");
710 format.untranslatedDescription = QT_TRANSLATE_NOOP("FMT", "Compiled Qt translations");
711 format.fileType = Translator::FileFormat::TranslationBinary;
712 format.priority = 0;
713 format.loader = &loadQM;
714 format.saver = &saveQM;
715 Translator::registerFileFormat(format);
716
717 return 1;
718}
719
720Q_CONSTRUCTOR_FUNCTION(initQM)
721
722QT_END_NAMESPACE
723

source code of qttools/src/linguist/shared/qm.cpp