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 | |
18 | QT_BEGIN_NAMESPACE |
19 | |
20 | // magic number for the file |
21 | static const int MagicLength = 16; |
22 | static 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 | |
28 | namespace { |
29 | |
30 | enum 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 | = 8, |
39 | Tag_Obsolete2 = 9 |
40 | }; |
41 | |
42 | enum Prefix { |
43 | NoPrefix, |
44 | Hash, |
45 | HashContext, |
46 | HashContextSourceText, |
47 | |
48 | }; |
49 | |
50 | } // namespace anon |
51 | |
52 | static 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 | |
71 | class ByteTranslatorMessage |
72 | { |
73 | public: |
74 | ByteTranslatorMessage( |
75 | const QByteArray &context, |
76 | const QByteArray &sourceText, |
77 | const QByteArray &, |
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 &() const { return m_comment; } |
87 | const QStringList &translations() const { return m_translations; } |
88 | bool operator<(const ByteTranslatorMessage& m) const; |
89 | |
90 | private: |
91 | QByteArray m_context; |
92 | QByteArray m_sourcetext; |
93 | QByteArray ; |
94 | QStringList m_translations; |
95 | }; |
96 | |
97 | Q_DECLARE_TYPEINFO(ByteTranslatorMessage, Q_RELOCATABLE_TYPE); |
98 | |
99 | bool 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 | |
108 | class Releaser |
109 | { |
110 | public: |
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 ); |
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 | |
143 | private: |
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 | |
168 | QByteArray 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 | |
178 | uint Releaser::msgHash(const ByteTranslatorMessage &msg) |
179 | { |
180 | return elfHash(ba: msg.sourceText() + msg.comment()); |
181 | } |
182 | |
183 | Prefix 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 | |
196 | void 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 | |
223 | bool 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 | |
262 | void 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 | |
385 | void Releaser::insert(const TranslatorMessage &message, const QStringList &tlns, bool ) |
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 | |
402 | void 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 | |
408 | void Releaser::setNumerusRules(const QByteArray &rules) |
409 | { |
410 | m_numerusRules = rules; |
411 | } |
412 | |
413 | void Releaser::setDependencies(const QStringList &dependencies) |
414 | { |
415 | m_dependencies = dependencies; |
416 | } |
417 | |
418 | static quint8 read8(const uchar *data) |
419 | { |
420 | return *data; |
421 | } |
422 | |
423 | static quint32 read32(const uchar *data) |
424 | { |
425 | return (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | (data[3]); |
426 | } |
427 | |
428 | static 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 | |
435 | bool 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, ; |
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 | |
609 | static 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 | |
619 | bool 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 = |
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 | |
705 | int 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 | |
720 | Q_CONSTRUCTOR_FUNCTION(initQM) |
721 | |
722 | QT_END_NAMESPACE |
723 | |