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 | = 8, |
64 | Tag_Obsolete2 = 9 |
65 | }; |
66 | |
67 | enum Prefix { |
68 | NoPrefix, |
69 | Hash, |
70 | HashContext, |
71 | HashContextSourceText, |
72 | |
73 | }; |
74 | |
75 | } // namespace anon |
76 | |
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: |
99 | ByteTranslatorMessage( |
100 | const QByteArray &context, |
101 | const QByteArray &sourceText, |
102 | const QByteArray &, |
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 &() const { return m_comment; } |
112 | 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 ; |
119 | QStringList m_translations; |
120 | }; |
121 | |
122 | Q_DECLARE_TYPEINFO(ByteTranslatorMessage, Q_MOVABLE_TYPE); |
123 | |
124 | 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 { |
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 ); |
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 | |
193 | 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 | |
203 | uint Releaser::msgHash(const ByteTranslatorMessage &msg) |
204 | { |
205 | return elfHash(ba: msg.sourceText() + msg.comment()); |
206 | } |
207 | |
208 | Prefix 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 | |
221 | 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 | |
248 | bool 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 | |
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(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 | |
414 | void Releaser::insert(const TranslatorMessage &message, const QStringList &tlns, bool ) |
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 | |
431 | void 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 | |
437 | void Releaser::setNumerusRules(const QByteArray &rules) |
438 | { |
439 | m_numerusRules = rules; |
440 | } |
441 | |
442 | void Releaser::setDependencies(const QStringList &dependencies) |
443 | { |
444 | m_dependencies = dependencies; |
445 | } |
446 | |
447 | static quint8 read8(const uchar *data) |
448 | { |
449 | return *data; |
450 | } |
451 | |
452 | static quint32 read32(const uchar *data) |
453 | { |
454 | return (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | (data[3]); |
455 | } |
456 | |
457 | static 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 | |
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(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, ; |
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 | |
639 | static 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 | |
649 | bool 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 = |
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 | |
735 | int 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 | |
750 | Q_CONSTRUCTOR_FUNCTION(initQM) |
751 | |
752 | QT_END_NAMESPACE |
753 | |