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