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
43 QT_BEGIN_NAMESPACE
44
45 // magic number for the file
46 static const int MagicLength = 16;
47 static 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
53 namespace {
54
55 enum 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
67 enum Prefix {
68 NoPrefix,
69 Hash,
70 HashContext,
71 HashContextSourceText,
72 HashContextSourceTextComment
73 };
74
75 } // namespace anon
76
elfHash(const QByteArray & ba)77 static 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
96 class ByteTranslatorMessage
97 {
98 public:
ByteTranslatorMessage(const QByteArray & context,const QByteArray & sourceText,const QByteArray & comment,const QStringList & translations)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 {}
context() const109 const QByteArray &context() const { return m_context; }
sourceText() const110 const QByteArray &sourceText() const { return m_sourcetext; }
comment() const111 const QByteArray &comment() const { return m_comment; }
translations() const112 const QStringList &translations() const { return m_translations; }
113 bool operator<(const ByteTranslatorMessage& m) const;
114
115 private:
116 QByteArray m_context;
117 QByteArray m_sourcetext;
118 QByteArray m_comment;
119 QStringList m_translations;
120 };
121
122 Q_DECLARE_TYPEINFO(ByteTranslatorMessage, Q_MOVABLE_TYPE);
123
operator <(const ByteTranslatorMessage & m) const124 bool 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
133 class Releaser
134 {
135 public:
136 struct Offset {
OffsetReleaser::Offset137 Offset()
138 : h(0), o(0)
139 {}
OffsetReleaser::Offset140 Offset(uint hash, uint offset)
141 : h(hash), o(offset)
142 {}
143
operator <Releaser::Offset144 bool operator<(const Offset &other) const {
145 return (h != other.h) ? h < other.h : o < other.o;
146 }
operator ==Releaser::Offset147 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
Releaser(const QString & language)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
168 private:
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
originalBytes(const QString & str) const193 QByteArray 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
msgHash(const ByteTranslatorMessage & msg)203 uint Releaser::msgHash(const ByteTranslatorMessage &msg)
204 {
205 return elfHash(msg.sourceText() + msg.comment());
206 }
207
commonPrefix(const ByteTranslatorMessage & m1,const ByteTranslatorMessage & m2)208 Prefix Releaser::commonPrefix(const ByteTranslatorMessage &m1, const ByteTranslatorMessage &m2)
209 {
210 if (msgHash(m1) != msgHash(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
writeMessage(const ByteTranslatorMessage & msg,QDataStream & stream,TranslatorSaveMode mode,Prefix prefix) const221 void 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
save(QIODevice * iod)248 bool Releaser::save(QIODevice *iod)
249 {
250 QDataStream s(iod);
251 s.writeRawData((const char *)magic, MagicLength);
252
253 if (!m_language.isEmpty()) {
254 QByteArray lang = originalBytes(m_language);
255 quint32 las = quint32(lang.size());
256 s << quint8(Language) << las;
257 s.writeRawData(lang, 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(), 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(), 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(), 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(), cas);
278 }
279 if (!m_numerusRules.isEmpty()) {
280 quint32 nrs = m_numerusRules.size();
281 s << quint8(NumerusRules) << nrs;
282 s.writeRawData(m_numerusRules.constData(), nrs);
283 }
284 return true;
285 }
286
squeeze(TranslatorSaveMode mode)287 void 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(it.key(), next.key());
318 offsets.insert(Offset(msgHash(it.key()), ms.device()->pos()), (void *)0);
319 writeMessage(it.key(), ms, mode, Prefix(qMax(cpPrev, 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(elfHash(c.key()) % hTableSize, 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(2 + (hTableSize << 1));
372 QDataStream t(&m_contextArray, QIODevice::WriteOnly);
373
374 quint16 *hTable = new quint16[hTableSize];
375 memset(hTable, 0, hTableSize * sizeof(quint16));
376
377 t << hTableSize;
378 t.device()->seek(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(len, 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(2);
403 for (int j = 0; j < hTableSize; j++)
404 t << hTable[j];
405 delete [] hTable;
406
407 if (upto > 131072) {
408 qWarning("Releaser::squeeze: Too many contexts");
409 m_contextArray.clear();
410 }
411 }
412 }
413
insert(const TranslatorMessage & message,const QStringList & tlns,bool forceComment)414 void Releaser::insert(const TranslatorMessage &message, const QStringList &tlns, bool forceComment)
415 {
416 ByteTranslatorMessage bmsg(originalBytes(message.context()),
417 originalBytes(message.sourceText()),
418 originalBytes(message.comment()),
419 tlns);
420 if (!forceComment) {
421 ByteTranslatorMessage bmsg2(
422 bmsg.context(), bmsg.sourceText(), QByteArray(""), bmsg.translations());
423 if (!m_messages.contains(bmsg2)) {
424 m_messages.insert(bmsg2, 0);
425 return;
426 }
427 }
428 m_messages.insert(bmsg, 0);
429 }
430
insertIdBased(const TranslatorMessage & message,const QStringList & tlns)431 void Releaser::insertIdBased(const TranslatorMessage &message, const QStringList &tlns)
432 {
433 ByteTranslatorMessage bmsg("", originalBytes(message.id()), "", tlns);
434 m_messages.insert(bmsg, 0);
435 }
436
setNumerusRules(const QByteArray & rules)437 void Releaser::setNumerusRules(const QByteArray &rules)
438 {
439 m_numerusRules = rules;
440 }
441
setDependencies(const QStringList & dependencies)442 void Releaser::setDependencies(const QStringList &dependencies)
443 {
444 m_dependencies = dependencies;
445 }
446
read8(const uchar * data)447 static quint8 read8(const uchar *data)
448 {
449 return *data;
450 }
451
read32(const uchar * data)452 static quint32 read32(const uchar *data)
453 {
454 return (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | (data[3]);
455 }
456
fromBytes(const char * str,int len,QString * out,bool * utf8Fail)457 static void fromBytes(const char *str, int len, QString *out, bool *utf8Fail)
458 {
459 static QTextCodec *utf8Codec = QTextCodec::codecForName("UTF-8");
460 QTextCodec::ConverterState cvtState;
461 *out = utf8Codec->toUnicode(str, len, &cvtState);
462 *utf8Fail = cvtState.invalidChars;
463 }
464
loadQM(Translator & translator,QIODevice & dev,ConversionData & cd)465 bool 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(data, magic, MagicLength) != 0) {
471 cd.appendError(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++);
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, blockLen));
510 QString dep;
511 while (!stream.atEnd()) {
512 stream >> dep;
513 dependencies.append(dep);
514 }
515 translator.setDependencies(dependencies);
516 } else if (tag == Language) {
517 QString language;
518 fromBytes((const char *)data, blockLen, &language, &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(translator.languageCode(), &l, &c);
533 QStringList numerusForms;
534 bool guessPlurals = true;
535 if (getNumerusInfo(l, c, 0, &numerusForms, 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(start + 4);
544 //qDebug() << "\nHASH:" << hash;
545 const uchar *m = messageArray + ro;
546
547 for (;;) {
548 uchar tag = read8(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(m);
555 if (len % 1) {
556 cd.appendError(QLatin1String("QM-Format error"));
557 return false;
558 }
559 m += 4;
560 QString str = QString((const QChar *)m, len/2);
561 if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) {
562 for (int i = 0; i < str.length(); ++i)
563 str[i] = QChar((str.at(i).unicode() >> 8) +
564 ((str.at(i).unicode() << 8) & 0xff00));
565 }
566 translations << str;
567 m += len;
568 break;
569 }
570 case Tag_Obsolete1:
571 m += 4;
572 //qDebug() << "OBSOLETE";
573 break;
574 case Tag_SourceText: {
575 quint32 len = read32(m);
576 m += 4;
577 //qDebug() << "SOURCE LEN: " << len;
578 //qDebug() << "SOURCE: " << QByteArray((const char*)m, len);
579 fromBytes((const char*)m, len, &sourcetext, &utf8Fail);
580 m += len;
581 break;
582 }
583 case Tag_Context: {
584 quint32 len = read32(m);
585 m += 4;
586 //qDebug() << "CONTEXT LEN: " << len;
587 //qDebug() << "CONTEXT: " << QByteArray((const char*)m, len);
588 fromBytes((const char*)m, len, &context, &utf8Fail);
589 m += len;
590 break;
591 }
592 case Tag_Comment: {
593 quint32 len = read32(m);
594 m += 4;
595 //qDebug() << "COMMENT LEN: " << len;
596 //qDebug() << "COMMENT: " << QByteArray((const char*)m, len);
597 fromBytes((const char*)m, len, &comment, &utf8Fail);
598 m += len;
599 break;
600 }
601 default:
602 //qDebug() << "UNKNOWN TAG" << tag;
603 break;
604 }
605 }
606 end:;
607 TranslatorMessage msg;
608 msg.setType(TranslatorMessage::Finished);
609 if (translations.count() > 1) {
610 // If guessPlurals is not false here, plural form discard messages
611 // will be spewn out later.
612 msg.setPlural(true);
613 } else if (guessPlurals) {
614 // This might cause false positives, so it is a fallback only.
615 if (sourcetext.contains(strProN))
616 msg.setPlural(true);
617 }
618 msg.setTranslations(translations);
619 translations.clear();
620 msg.setContext(context);
621 msg.setSourceText(sourcetext);
622 msg.setComment(comment);
623 translator.append(msg);
624 }
625 if (utf8Fail) {
626 cd.appendError(QLatin1String("Cannot read file with UTF-8 codec"));
627 return false;
628 }
629 return ok;
630 }
631
632
633
containsStripped(const Translator & translator,const TranslatorMessage & msg)634 static bool containsStripped(const Translator &translator, const TranslatorMessage &msg)
635 {
636 foreach (const TranslatorMessage &tmsg, translator.messages())
637 if (tmsg.sourceText() == msg.sourceText()
638 && tmsg.context() == msg.context()
639 && tmsg.comment().isEmpty())
640 return true;
641 return false;
642 }
643
saveQM(const Translator & translator,QIODevice & dev,ConversionData & cd)644 bool saveQM(const Translator &translator, QIODevice &dev, ConversionData &cd)
645 {
646 Releaser releaser(translator.languageCode());
647 QLocale::Language l;
648 QLocale::Country c;
649 Translator::languageAndCountry(translator.languageCode(), &l, &c);
650 QByteArray rules;
651 if (getNumerusInfo(l, c, &rules, 0, 0))
652 releaser.setNumerusRules(rules);
653
654 int finished = 0;
655 int unfinished = 0;
656 int untranslated = 0;
657 int missingIds = 0;
658 int droppedData = 0;
659
660 for (int i = 0; i != translator.messageCount(); ++i) {
661 const TranslatorMessage &msg = translator.message(i);
662 TranslatorMessage::Type typ = msg.type();
663 if (typ != TranslatorMessage::Obsolete && typ != TranslatorMessage::Vanished) {
664 if (cd.m_idBased && msg.id().isEmpty()) {
665 ++missingIds;
666 continue;
667 }
668 if (typ == TranslatorMessage::Unfinished) {
669 if (msg.translation().isEmpty() && !cd.m_idBased && cd.m_unTrPrefix.isEmpty()) {
670 ++untranslated;
671 continue;
672 } else {
673 if (cd.ignoreUnfinished())
674 continue;
675 ++unfinished;
676 }
677 } else {
678 ++finished;
679 }
680 QStringList tlns = msg.translations();
681 if (msg.type() == TranslatorMessage::Unfinished
682 && (cd.m_idBased || !cd.m_unTrPrefix.isEmpty()))
683 for (int j = 0; j < tlns.size(); ++j)
684 if (tlns.at(j).isEmpty())
685 tlns[j] = cd.m_unTrPrefix + msg.sourceText();
686 if (cd.m_idBased) {
687 if (!msg.context().isEmpty() || !msg.comment().isEmpty())
688 ++droppedData;
689 releaser.insertIdBased(msg, tlns);
690 } else {
691 // Drop the comment in (context, sourceText, comment),
692 // unless the context is empty,
693 // unless (context, sourceText, "") already exists or
694 // unless we already dropped the comment of (context,
695 // sourceText, comment0).
696 bool forceComment =
697 msg.comment().isEmpty()
698 || msg.context().isEmpty()
699 || containsStripped(translator, msg);
700 releaser.insert(msg, tlns, forceComment);
701 }
702 }
703 }
704
705 if (missingIds)
706 cd.appendError(QCoreApplication::translate("LRelease",
707 "Dropped %n message(s) which had no ID.", 0,
708 missingIds));
709 if (droppedData)
710 cd.appendError(QCoreApplication::translate("LRelease",
711 "Excess context/disambiguation dropped from %n message(s).", 0,
712 droppedData));
713
714 releaser.setDependencies(translator.dependencies());
715 releaser.squeeze(cd.m_saveMode);
716 bool saved = releaser.save(&dev);
717 if (saved && cd.isVerbose()) {
718 int generatedCount = finished + unfinished;
719 cd.appendError(QCoreApplication::translate("LRelease",
720 " Generated %n translation(s) (%1 finished and %2 unfinished)", 0,
721 generatedCount).arg(finished).arg(unfinished));
722 if (untranslated)
723 cd.appendError(QCoreApplication::translate("LRelease",
724 " Ignored %n untranslated source text(s)", 0,
725 untranslated));
726 }
727 return saved;
728 }
729
initQM()730 int initQM()
731 {
732 Translator::FileFormat format;
733
734 format.extension = QLatin1String("qm");
735 format.untranslatedDescription = QT_TRANSLATE_NOOP("FMT", "Compiled Qt translations");
736 format.fileType = Translator::FileFormat::TranslationBinary;
737 format.priority = 0;
738 format.loader = &loadQM;
739 format.saver = &saveQM;
740 Translator::registerFileFormat(format);
741
742 return 1;
743 }
744
745 Q_CONSTRUCTOR_FUNCTION(initQM)
746
747 QT_END_NAMESPACE
748