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

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