Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
po.cpp
Go to the documentation of this file.
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#include <QtCore/QDebug>
7#include <QtCore/QIODevice>
8#include <QtCore/QHash>
9#include <QtCore/QRegularExpression>
10#include <QtCore/QString>
11#include <QtCore/QStringConverter>
12#include <QtCore/QTextStream>
13
14#include <ctype.h>
15
16// Uncomment if you wish to hard wrap long lines in .po files. Note that this
17// affects only msg strings, not comments.
18//#define HARD_WRAP_LONG_WORDS
19
21
22static const int MAX_LEN = 79;
23
24static QString poEscapedString(const QString &prefix, const QString &keyword,
25 bool noWrap, const QString &ba)
26{
27 QStringList lines;
28 int off = 0;
29 QString res;
30 while (off < ba.size()) {
31 ushort c = ba[off++].unicode();
32 switch (c) {
33 case '\n':
34 res += QLatin1String("\\n");
35 lines.append(res);
36 res.clear();
37 break;
38 case '\r':
39 res += QLatin1String("\\r");
40 break;
41 case '\t':
42 res += QLatin1String("\\t");
43 break;
44 case '\v':
45 res += QLatin1String("\\v");
46 break;
47 case '\a':
48 res += QLatin1String("\\a");
49 break;
50 case '\b':
51 res += QLatin1String("\\b");
52 break;
53 case '\f':
54 res += QLatin1String("\\f");
55 break;
56 case '"':
57 res += QLatin1String("\\\"");
58 break;
59 case '\\':
60 res += QLatin1String("\\\\");
61 break;
62 default:
63 if (c < 32) {
64 res += QLatin1String("\\x");
65 res += QString::number(c, 16);
66 if (off < ba.size() && isxdigit(ba[off].unicode()))
67 res += QLatin1String("\"\"");
68 } else {
69 res += QChar(c);
70 }
71 break;
72 }
73 }
74 if (!res.isEmpty())
75 lines.append(res);
76 if (!lines.isEmpty()) {
77 if (!noWrap) {
78 if (lines.size() != 1 ||
79 lines.first().size() > MAX_LEN - keyword.size() - prefix.size() - 3)
80 {
81 const QStringList olines = lines;
82 lines = QStringList(QString());
83 const int maxlen = MAX_LEN - prefix.size() - 2;
84 for (const QString &line : olines) {
85 int off = 0;
86 while (off + maxlen < line.size()) {
87 int idx = line.lastIndexOf(QLatin1Char(' '), off + maxlen - 1) + 1;
88 if (idx == off) {
89#ifdef HARD_WRAP_LONG_WORDS
90 // This doesn't seem too nice, but who knows ...
91 idx = off + maxlen;
92#else
93 idx = line.indexOf(QLatin1Char(' '), off + maxlen) + 1;
94 if (!idx)
95 break;
96#endif
97 }
98 lines.append(line.mid(off, idx - off));
99 off = idx;
100 }
101 lines.append(line.mid(off));
102 }
103 }
104 } else if (lines.size() > 1) {
105 lines.prepend(QString());
106 }
107 }
108 return prefix + keyword + QLatin1String(" \"") +
109 lines.join(QLatin1String("\"\n") + prefix + QLatin1Char('"')) +
110 QLatin1String("\"\n");
111}
112
113static QString poEscapedLines(const QString &prefix, bool addSpace, const QStringList &lines)
114{
115 QString out;
116 for (const QString &line : lines) {
117 out += prefix;
118 if (addSpace && !line.isEmpty())
119 out += QLatin1Char(' ' );
120 out += line;
121 out += QLatin1Char('\n');
122 }
123 return out;
124}
125
126static QString poEscapedLines(const QString &prefix, bool addSpace, const QString &in0)
127{
128 QString in = in0;
129 if (in == QString::fromLatin1("\n"))
130 in.chop(1);
131 return poEscapedLines(prefix, addSpace, in.split(QLatin1Char('\n')));
132}
133
134static QString poWrappedEscapedLines(const QString &prefix, bool addSpace, const QString &line)
135{
136 const int maxlen = MAX_LEN - prefix.size() - addSpace;
137 QStringList lines;
138 int off = 0;
139 while (off + maxlen < line.size()) {
140 int idx = line.lastIndexOf(QLatin1Char(' '), off + maxlen - 1);
141 if (idx < off) {
142#if 0 //def HARD_WRAP_LONG_WORDS
143 // This cannot work without messing up semantics, so do not even try.
144#else
145 idx = line.indexOf(QLatin1Char(' '), off + maxlen);
146 if (idx < 0)
147 break;
148#endif
149 }
150 lines.append(line.mid(off, idx - off));
151 off = idx + 1;
152 }
153 lines.append(line.mid(off));
154 return poEscapedLines(prefix, addSpace, lines);
155}
156
182
183
184static bool isTranslationLine(const QByteArray &line)
185{
186 return line.startsWith("#~ msgstr") || line.startsWith("msgstr");
187}
188
189static QByteArray slurpEscapedString(const QList<QByteArray> &lines, int &l,
190 int offset, const QByteArray &prefix, ConversionData &cd)
191{
192 QByteArray msg;
193 int stoff;
194
195 for (; l < lines.size(); ++l) {
196 const QByteArray &line = lines.at(l);
197 if (line.isEmpty() || !line.startsWith(prefix))
198 break;
199 while (isspace(line[offset])) // No length check, as string has no trailing spaces.
200 offset++;
201 if (line[offset] != '"')
202 break;
203 offset++;
204 forever {
205 if (offset == line.size())
206 goto premature_eol;
207 uchar c = line[offset++];
208 if (c == '"') {
209 if (offset == line.size())
210 break;
211 while (isspace(line[offset]))
212 offset++;
213 if (line[offset++] != '"') {
214 cd.appendError(QString::fromLatin1(
215 "PO parsing error: extra characters on line %1.")
216 .arg(l + 1));
217 break;
218 }
219 continue;
220 }
221 if (c == '\\') {
222 if (offset == line.size())
223 goto premature_eol;
224 c = line[offset++];
225 switch (c) {
226 case 'r':
227 msg += '\r'; // Maybe just throw it away?
228 break;
229 case 'n':
230 msg += '\n';
231 break;
232 case 't':
233 msg += '\t';
234 break;
235 case 'v':
236 msg += '\v';
237 break;
238 case 'a':
239 msg += '\a';
240 break;
241 case 'b':
242 msg += '\b';
243 break;
244 case 'f':
245 msg += '\f';
246 break;
247 case '"':
248 msg += '"';
249 break;
250 case '\\':
251 msg += '\\';
252 break;
253 case '0':
254 case '1':
255 case '2':
256 case '3':
257 case '4':
258 case '5':
259 case '6':
260 case '7':
261 stoff = offset - 1;
262 while ((c = line[offset]) >= '0' && c <= '7')
263 if (++offset == line.size())
264 goto premature_eol;
265 msg += line.mid(stoff, offset - stoff).toUInt(0, 8);
266 break;
267 case 'x':
268 stoff = offset;
269 while (isxdigit(line[offset]))
270 if (++offset == line.size())
271 goto premature_eol;
272 msg += line.mid(stoff, offset - stoff).toUInt(0, 16);
273 break;
274 default:
275 cd.appendError(QString::fromLatin1(
276 "PO parsing error: invalid escape '\\%1' (line %2).")
277 .arg(QChar((uint)c)).arg(l + 1));
278 msg += '\\';
279 msg += c;
280 break;
281 }
282 } else {
283 msg += c;
284 }
285 }
286 offset = prefix.size();
287 }
288 --l;
289 return msg;
290
291premature_eol:
292 cd.appendError(QString::fromLatin1(
293 "PO parsing error: premature end of line %1.").arg(l + 1));
294 return QByteArray();
295}
296
297static void slurpComment(QByteArray &msg, const QList<QByteArray> &lines, int & l)
298{
299 int firstLine = l;
300 QByteArray prefix = lines.at(l);
301 for (int i = 1; ; i++) {
302 if (prefix.at(i) != ' ') {
303 prefix.truncate(i);
304 break;
305 }
306 }
307 for (; l < lines.size(); ++l) {
308 const QByteArray &line = lines.at(l);
309 if (line.startsWith(prefix)) {
310 if (l > firstLine)
311 msg += '\n';
312 msg += line.mid(prefix.size());
313 } else if (line == "#") {
314 msg += '\n';
315 } else {
316 break;
317 }
318 }
319 --l;
320}
321
322static void splitContext(QByteArray *comment, QByteArray *context)
323{
324 char *data = comment->data();
325 int len = comment->size();
326 int sep = -1, j = 0;
327
328 for (int i = 0; i < len; i++, j++) {
329 if (data[i] == '~' && i + 1 < len)
330 i++;
331 else if (data[i] == '|')
332 sep = j;
333 data[j] = data[i];
334 }
335 if (sep >= 0) {
336 QByteArray tmp = comment->mid(sep + 1, j - sep - 1);
337 comment->truncate(sep);
338 *context = *comment;
339 *comment = tmp;
340 } else {
341 comment->truncate(j);
342 }
343}
344
345static QString makePoHeader(const QString &str)
346{
347 return QLatin1String("po-header-") + str.toLower().replace(QLatin1Char('-'), QLatin1Char('_'));
348}
349
350static QByteArray QByteArrayList_join(const QList<QByteArray> &that, char sep)
351{
352 int totalLength = 0;
353 const int size = that.size();
354
355 for (int i = 0; i < size; ++i)
356 totalLength += that.at(i).size();
357
358 if (size > 0)
359 totalLength += size - 1;
360
361 QByteArray res;
362 if (totalLength == 0)
363 return res;
364 res.reserve(totalLength);
365 for (int i = 0; i < that.size(); ++i) {
366 if (i)
367 res += sep;
368 res += that.at(i);
369 }
370 return res;
371}
372
373bool loadPO(Translator &translator, QIODevice &dev, ConversionData &cd)
374{
375 QStringDecoder toUnicode(QStringConverter::Utf8, QStringDecoder::Flag::Stateless);
376 bool error = false;
377
378 // format of a .po file entry:
379 // white-space
380 // # translator-comments
381 // #. automatic-comments
382 // #: reference...
383 // #, flag...
384 // #~ msgctxt, msgid*, msgstr - used for obsoleted messages
385 // #| msgctxt, msgid* previous untranslated-string - for fuzzy message
386 // #~| msgctxt, msgid* previous untranslated-string - for fuzzy obsoleted messages
387 // msgctx string-context
388 // msgid untranslated-string
389 // -- For singular:
390 // msgstr translated-string
391 // -- For plural:
392 // msgid_plural untranslated-string-plural
393 // msgstr[0] translated-string
394 // ...
395
396 // we need line based lookahead below.
397 QList<QByteArray> lines;
398 while (!dev.atEnd())
399 lines.append(dev.readLine().trimmed());
400 lines.append(QByteArray());
401
402 int l = 0, lastCmtLine = -1;
403 bool qtContexts = false;
404 PoItem item;
405 for (; l != lines.size(); ++l) {
406 QByteArray line = lines.at(l);
407 if (line.isEmpty())
408 continue;
409 if (isTranslationLine(line)) {
410 bool isObsolete = line.startsWith("#~ msgstr");
411 const QByteArray prefix = isObsolete ? "#~ " : "";
412 while (true) {
413 int idx = line.indexOf(' ', prefix.size());
414 QByteArray str = slurpEscapedString(lines, l, idx, prefix, cd);
415 item.msgStr.append(str);
416 if (l + 1 >= lines.size() || !isTranslationLine(lines.at(l + 1)))
417 break;
418 ++l;
419 line = lines.at(l);
420 }
421 if (item.msgId.isEmpty()) {
422 QHash<QString, QByteArray> extras;
423 QList<QByteArray> hdrOrder;
424 QByteArray pluralForms;
425 for (const QByteArray &hdr : item.msgStr.first().split('\n')) {
426 if (hdr.isEmpty())
427 continue;
428 int idx = hdr.indexOf(':');
429 if (idx < 0) {
430 cd.appendError(QString::fromLatin1("Unexpected PO header format '%1'")
431 .arg(QString::fromLatin1(hdr)));
432 error = true;
433 break;
434 }
435 QByteArray hdrName = hdr.left(idx).trimmed();
436 QByteArray hdrValue = hdr.mid(idx + 1).trimmed();
437 hdrOrder << hdrName;
438 if (hdrName == "X-Language") {
439 translator.setLanguageCode(QString::fromLatin1(hdrValue));
440 } else if (hdrName == "X-Source-Language") {
441 translator.setSourceLanguageCode(QString::fromLatin1(hdrValue));
442 } else if (hdrName == "X-Qt-Contexts") {
443 qtContexts = (hdrValue == "true");
444 } else if (hdrName == "Plural-Forms") {
445 pluralForms = hdrValue;
446 } else if (hdrName == "MIME-Version") {
447 // just assume it is 1.0
448 } else if (hdrName == "Content-Type") {
449 if (!hdrValue.startsWith("text/plain; charset=")) {
450 cd.appendError(QString::fromLatin1("Unexpected Content-Type header '%1'")
451 .arg(QString::fromLatin1(hdrValue)));
452 error = true;
453 // This will avoid a flood of conversion errors.
454 toUnicode = QStringDecoder(QStringConverter::Latin1);
455 } else {
456 QByteArray cod = hdrValue.mid(20);
457 auto enc = QStringConverter::encodingForName(cod);
458 if (!enc) {
459 cd.appendError(QString::fromLatin1("Unsupported encoding '%1'")
460 .arg(QString::fromLatin1(cod)));
461 error = true;
462 // This will avoid a flood of conversion errors.
463 toUnicode = QStringDecoder(QStringConverter::Latin1);
464 } else {
465 toUnicode = QStringDecoder(*enc);
466 }
467 }
468 } else if (hdrName == "Content-Transfer-Encoding") {
469 if (hdrValue != "8bit") {
470 cd.appendError(QString::fromLatin1("Unexpected Content-Transfer-Encoding '%1'")
471 .arg(QString::fromLatin1(hdrValue)));
472 return false;
473 }
474 } else if (hdrName == "X-Virgin-Header") {
475 // legacy
476 } else {
477 extras[makePoHeader(QString::fromLatin1(hdrName))] = hdrValue;
478 }
479 }
480 if (!pluralForms.isEmpty()) {
481 if (translator.languageCode().isEmpty()) {
482 extras[makePoHeader(QLatin1String("Plural-Forms"))] = pluralForms;
483 } else {
484 // FIXME: have fun with making a consistency check ...
485 }
486 }
487 // Eliminate the field if only headers we added are present in standard order.
488 // Keep in sync with savePO
489 static const char * const dfltHdrs[] = {
490 "MIME-Version", "Content-Type", "Content-Transfer-Encoding",
491 "Plural-Forms", "X-Language", "X-Source-Language", "X-Qt-Contexts"
492 };
493 uint cdh = 0;
494 for (int cho = 0; cho < hdrOrder.size(); cho++) {
495 for (;; cdh++) {
496 if (cdh == sizeof(dfltHdrs)/sizeof(dfltHdrs[0])) {
497 extras[QLatin1String("po-headers")] =
498 QByteArrayList_join(hdrOrder, ',');
499 goto doneho;
500 }
501 if (hdrOrder.at(cho) == dfltHdrs[cdh]) {
502 cdh++;
503 break;
504 }
505 }
506 }
507 doneho:
508 if (lastCmtLine != -1) {
509 extras[QLatin1String("po-header_comment")] =
510 QByteArrayList_join(lines.mid(0, lastCmtLine + 1), '\n');
511 }
512 for (auto it = extras.cbegin(), end = extras.cend(); it != end; ++it)
513 translator.setExtra(it.key(), toUnicode(it.value()));
514 item = PoItem();
515 continue;
516 }
517 // build translator message
519 msg.setContext(toUnicode(item.context));
520 if (!item.references.isEmpty()) {
521 QString xrefs;
522 for (const QString &ref :
523 QString(toUnicode(item.references)).split(
524 QRegularExpression(QLatin1String("\\s")), Qt::SkipEmptyParts)) {
525 int pos = ref.indexOf(QLatin1Char(':'));
526 int lpos = ref.lastIndexOf(QLatin1Char(':'));
527 if (pos != -1 && pos == lpos) {
528 bool ok;
529 int lno = ref.mid(pos + 1).toInt(&ok);
530 if (ok) {
531 msg.addReference(ref.left(pos), lno);
532 continue;
533 }
534 }
535 if (!xrefs.isEmpty())
536 xrefs += QLatin1Char(' ');
537 xrefs += ref;
538 }
539 if (!xrefs.isEmpty())
540 item.extra[QLatin1String("po-references")] = xrefs;
541 }
542 msg.setId(toUnicode(item.id));
543 msg.setSourceText(toUnicode(item.msgId));
544 msg.setOldSourceText(toUnicode(item.oldMsgId));
545 msg.setComment(toUnicode(item.tscomment));
546 msg.setOldComment(toUnicode(item.oldTscomment));
547 msg.setExtraComment(toUnicode(item.automaticComments));
548 msg.setTranslatorComment(toUnicode(item.translatorComments));
549 msg.setPlural(item.isPlural || item.msgStr.size() > 1);
550 QStringList translations;
551 for (const QByteArray &bstr : std::as_const(item.msgStr)) {
552 QString str = toUnicode(bstr);
553 str.replace(Translator::TextVariantSeparator, Translator::BinaryVariantSeparator);
554 translations << str;
555 }
556 msg.setTranslations(translations);
557 bool isFuzzy = item.isFuzzy || (!msg.sourceText().isEmpty() && !msg.isTranslated());
558 if (isObsolete && isFuzzy)
560 else if (isObsolete)
562 else if (isFuzzy)
564 else
566 msg.setExtras(item.extra);
567
568 //qDebug() << "WRITE: " << context;
569 //qDebug() << "SOURCE: " << msg.sourceText();
570 //qDebug() << flags << msg.m_extra;
571 translator.append(msg);
572 item = PoItem();
573 } else if (line.startsWith('#')) {
574 switch (line.size() < 2 ? 0 : line.at(1)) {
575 case ':':
576 item.references += line.mid(3);
577 item.references += '\n';
578 break;
579 case ',': {
580 QStringList flags =
581 QString::fromLatin1(line.mid(2)).split(
582 QRegularExpression(QLatin1String("[, ]")), Qt::SkipEmptyParts);
583 if (flags.removeOne(QLatin1String("fuzzy")))
584 item.isFuzzy = true;
585 flags.removeOne(QLatin1String("qt-format"));
586 const auto it = item.extra.constFind(QLatin1String("po-flags"));
587 if (it != item.extra.cend())
588 flags.prepend(*it);
589 if (!flags.isEmpty())
590 item.extra[QLatin1String("po-flags")] = flags.join(QLatin1String(", "));
591 break;
592 }
593 case 0:
594 item.translatorComments += '\n';
595 break;
596 case ' ':
597 slurpComment(item.translatorComments, lines, l);
598 break;
599 case '.':
600 if (line.startsWith("#. ts-context ")) { // legacy
601 item.context = line.mid(14);
602 } else if (line.startsWith("#. ts-id ")) {
603 item.id = line.mid(9);
604 } else {
605 item.automaticComments += line.mid(3);
606
607 }
608 break;
609 case '|':
610 if (line.startsWith("#| msgid ")) {
611 item.oldMsgId = slurpEscapedString(lines, l, 9, "#| ", cd);
612 } else if (line.startsWith("#| msgid_plural ")) {
613 QByteArray extra = slurpEscapedString(lines, l, 16, "#| ", cd);
614 if (extra != item.oldMsgId)
615 item.extra[QLatin1String("po-old_msgid_plural")] =
616 toUnicode(extra);
617 } else if (line.startsWith("#| msgctxt ")) {
618 item.oldTscomment = slurpEscapedString(lines, l, 11, "#| ", cd);
619 if (qtContexts)
620 splitContext(&item.oldTscomment, &item.context);
621 } else {
622 cd.appendError(QString(QLatin1String("PO-format parse error in line %1: '%2'"))
623 .arg(l + 1).arg(toUnicode(lines[l])));
624 error = true;
625 }
626 break;
627 case '~':
628 if (line.startsWith("#~ msgid ")) {
629 item.msgId = slurpEscapedString(lines, l, 9, "#~ ", cd);
630 } else if (line.startsWith("#~ msgid_plural ")) {
631 QByteArray extra = slurpEscapedString(lines, l, 16, "#~ ", cd);
632 if (extra != item.msgId)
633 item.extra[QLatin1String("po-msgid_plural")] =
634 toUnicode(extra);
635 item.isPlural = true;
636 } else if (line.startsWith("#~ msgctxt ")) {
637 item.tscomment = slurpEscapedString(lines, l, 11, "#~ ", cd);
638 if (qtContexts)
639 splitContext(&item.tscomment, &item.context);
640 } else if (line.startsWith("#~| msgid ")) {
641 item.oldMsgId = slurpEscapedString(lines, l, 10, "#~| ", cd);
642 } else if (line.startsWith("#~| msgid_plural ")) {
643 QByteArray extra = slurpEscapedString(lines, l, 17, "#~| ", cd);
644 if (extra != item.oldMsgId)
645 item.extra[QLatin1String("po-old_msgid_plural")] =
646 toUnicode(extra);
647 } else if (line.startsWith("#~| msgctxt ")) {
648 item.oldTscomment = slurpEscapedString(lines, l, 12, "#~| ", cd);
649 if (qtContexts)
650 splitContext(&item.oldTscomment, &item.context);
651 } else {
652 cd.appendError(QString(QLatin1String("PO-format parse error in line %1: '%2'"))
653 .arg(l + 1).arg(toUnicode(lines[l])));
654 error = true;
655 }
656 break;
657 default:
658 cd.appendError(QString(QLatin1String("PO-format parse error in line %1: '%2'"))
659 .arg(l + 1).arg(toUnicode(lines[l])));
660 error = true;
661 break;
662 }
663 lastCmtLine = l;
664 } else if (line.startsWith("msgctxt ")) {
665 item.tscomment = slurpEscapedString(lines, l, 8, QByteArray(), cd);
666 if (qtContexts)
667 splitContext(&item.tscomment, &item.context);
668 } else if (line.startsWith("msgid ")) {
669 item.msgId = slurpEscapedString(lines, l, 6, QByteArray(), cd);
670 } else if (line.startsWith("msgid_plural ")) {
671 QByteArray extra = slurpEscapedString(lines, l, 13, QByteArray(), cd);
672 if (extra != item.msgId)
673 item.extra[QLatin1String("po-msgid_plural")] = toUnicode(extra);
674 item.isPlural = true;
675 } else {
676 cd.appendError(QString(QLatin1String("PO-format error in line %1: '%2'"))
677 .arg(l + 1).arg(toUnicode(lines[l])));
678 error = true;
679 }
680 }
681 return !error && cd.errors().isEmpty();
682}
683
684static void addPoHeader(Translator::ExtraData &headers, QStringList &hdrOrder,
685 const char *name, const QString &value)
686{
687 QString qName = QLatin1String(name);
688 if (!hdrOrder.contains(qName))
689 hdrOrder << qName;
690 headers[makePoHeader(qName)] = value;
691}
692
693static QString escapeComment(const QString &in, bool escape)
694{
695 QString out = in;
696 if (escape) {
697 out.replace(QLatin1Char('~'), QLatin1String("~~"));
698 out.replace(QLatin1Char('|'), QLatin1String("~|"));
699 }
700 return out;
701}
702
703bool savePO(const Translator &translator, QIODevice &dev, ConversionData &)
704{
705 QString str_format = QLatin1String("-format");
706
707 bool ok = true;
708 QTextStream out(&dev);
709
710 bool qtContexts = false;
711 for (const TranslatorMessage &msg : translator.messages())
712 if (!msg.context().isEmpty()) {
713 qtContexts = true;
714 break;
715 }
716
717 QString cmt = translator.extra(QLatin1String("po-header_comment"));
718 if (!cmt.isEmpty())
719 out << cmt << '\n';
720 out << "msgid \"\"\n";
721 Translator::ExtraData headers = translator.extras();
722 QStringList hdrOrder = translator.extra(QLatin1String("po-headers")).split(
723 QLatin1Char(','), Qt::SkipEmptyParts);
724 // Keep in sync with loadPO
725 addPoHeader(headers, hdrOrder, "MIME-Version", QLatin1String("1.0"));
726 addPoHeader(headers, hdrOrder, "Content-Type",
727 QLatin1String("text/plain; charset=UTF-8"));
728 addPoHeader(headers, hdrOrder, "Content-Transfer-Encoding", QLatin1String("8bit"));
729 if (!translator.languageCode().isEmpty()) {
730 QLocale::Language l;
731 QLocale::Territory c;
732 Translator::languageAndTerritory(translator.languageCode(), &l, &c);
733 const char *gettextRules;
734 if (getNumerusInfo(l, c, 0, 0, &gettextRules))
735 addPoHeader(headers, hdrOrder, "Plural-Forms", QLatin1String(gettextRules));
736 addPoHeader(headers, hdrOrder, "X-Language", translator.languageCode());
737 }
738 if (!translator.sourceLanguageCode().isEmpty())
739 addPoHeader(headers, hdrOrder, "X-Source-Language", translator.sourceLanguageCode());
740 if (qtContexts)
741 addPoHeader(headers, hdrOrder, "X-Qt-Contexts", QLatin1String("true"));
742 QString hdrStr;
743 for (const QString &hdr : std::as_const(hdrOrder)) {
744 hdrStr += hdr;
745 hdrStr += QLatin1String(": ");
746 hdrStr += headers.value(makePoHeader(hdr));
747 hdrStr += QLatin1Char('\n');
748 }
749 out << poEscapedString(QString(), QString::fromLatin1("msgstr"), true, hdrStr);
750
751 for (const TranslatorMessage &msg : translator.messages()) {
752 out << Qt::endl;
753
754 if (!msg.translatorComment().isEmpty())
755 out << poEscapedLines(QLatin1String("#"), true, msg.translatorComment());
756
757 if (!msg.extraComment().isEmpty())
758 out << poEscapedLines(QLatin1String("#."), true, msg.extraComment());
759
760 if (!msg.id().isEmpty())
761 out << QLatin1String("#. ts-id ") << msg.id() << '\n';
762
763 QString xrefs = msg.extra(QLatin1String("po-references"));
764 if (!msg.fileName().isEmpty() || !xrefs.isEmpty()) {
765 QStringList refs;
766 for (const TranslatorMessage::Reference &ref : msg.allReferences())
767 refs.append(QString(QLatin1String("%2:%1"))
768 .arg(ref.lineNumber()).arg(ref.fileName()));
769 if (!xrefs.isEmpty())
770 refs << xrefs;
771 out << poWrappedEscapedLines(QLatin1String("#:"), true, refs.join(QLatin1Char(' ')));
772 }
773
774 bool noWrap = false;
775 bool skipFormat = false;
776 QStringList flags;
777 if ((msg.type() == TranslatorMessage::Unfinished
778 || msg.type() == TranslatorMessage::Obsolete) && msg.isTranslated())
779 flags.append(QLatin1String("fuzzy"));
780 const auto itr = msg.extras().constFind(QLatin1String("po-flags"));
781 if (itr != msg.extras().cend()) {
782 const QStringList atoms = itr->split(QLatin1String(", "));
783 for (const QString &atom : atoms)
784 if (atom.endsWith(str_format)) {
785 skipFormat = true;
786 break;
787 }
788 if (atoms.contains(QLatin1String("no-wrap")))
789 noWrap = true;
790 flags.append(*itr);
791 }
792 if (!skipFormat) {
793 QString source = msg.sourceText();
794 // This is fuzzy logic, as we don't know whether the string is
795 // actually used with QString::arg().
796 for (int off = 0; (off = source.indexOf(QLatin1Char('%'), off)) >= 0; ) {
797 if (++off >= source.size())
798 break;
799 if (source.at(off) == QLatin1Char('n') || source.at(off).isDigit()) {
800 flags.append(QLatin1String("qt-format"));
801 break;
802 }
803 }
804 }
805 if (!flags.isEmpty())
806 out << "#, " << flags.join(QLatin1String(", ")) << '\n';
807
808 bool isObsolete = (msg.type() == TranslatorMessage::Obsolete
809 || msg.type() == TranslatorMessage::Vanished);
810 QString prefix = QLatin1String(isObsolete ? "#~| " : "#| ");
811 if (!msg.oldComment().isEmpty())
812 out << poEscapedString(prefix, QLatin1String("msgctxt"), noWrap,
813 escapeComment(msg.oldComment(), qtContexts));
814 if (!msg.oldSourceText().isEmpty())
815 out << poEscapedString(prefix, QLatin1String("msgid"), noWrap, msg.oldSourceText());
816 QString plural = msg.extra(QLatin1String("po-old_msgid_plural"));
817 if (!plural.isEmpty())
818 out << poEscapedString(prefix, QLatin1String("msgid_plural"), noWrap, plural);
819 prefix = QLatin1String(isObsolete ? "#~ " : "");
820 if (!msg.context().isEmpty())
821 out << poEscapedString(prefix, QLatin1String("msgctxt"), noWrap,
822 escapeComment(msg.context(), true) + QLatin1Char('|')
823 + escapeComment(msg.comment(), true));
824 else if (!msg.comment().isEmpty())
825 out << poEscapedString(prefix, QLatin1String("msgctxt"), noWrap,
826 escapeComment(msg.comment(), qtContexts));
827 out << poEscapedString(prefix, QLatin1String("msgid"), noWrap, msg.sourceText());
828 if (!msg.isPlural()) {
829 QString transl = msg.translation();
830 transl.replace(Translator::BinaryVariantSeparator, Translator::TextVariantSeparator);
831 out << poEscapedString(prefix, QLatin1String("msgstr"), noWrap, transl);
832 } else {
833 QString plural = msg.extra(QLatin1String("po-msgid_plural"));
834 if (plural.isEmpty())
835 plural = msg.sourceText();
836 out << poEscapedString(prefix, QLatin1String("msgid_plural"), noWrap, plural);
837 const QStringList &translations = msg.translations();
838 for (int i = 0; i != translations.size(); ++i) {
839 QString str = translations.at(i);
840 str.replace(QChar(Translator::BinaryVariantSeparator),
841 QChar(Translator::TextVariantSeparator));
842 out << poEscapedString(prefix, QString::fromLatin1("msgstr[%1]").arg(i), noWrap,
843 str);
844 }
845 }
846 }
847 return ok;
848}
849
850static bool savePOT(const Translator &translator, QIODevice &dev, ConversionData &cd)
851{
852 Translator ttor = translator;
854 return savePO(ttor, dev, cd);
855}
856
858{
859 Translator::FileFormat format;
860 format.extension = QLatin1String("po");
861 format.untranslatedDescription = QT_TRANSLATE_NOOP("FMT", "GNU Gettext localization files");
862 format.loader = &loadPO;
863 format.saver = &savePO;
865 format.priority = 1;
867 format.extension = QLatin1String("pot");
868 format.untranslatedDescription = QT_TRANSLATE_NOOP("FMT", "GNU Gettext localization template files");
869 format.loader = &loadPO;
870 format.saver = &savePOT;
872 format.priority = -1;
874 return 1;
875}
876
877Q_CONSTRUCTOR_FUNCTION(initPO)
878
879QT_END_NAMESPACE
bool isTranslated() const
void setPlural(bool isplural)
void setExtras(const ExtraData &extras)
void dropTranslations()
static void registerFileFormat(const FileFormat &format)
void append(const TranslatorMessage &msg)
const ExtraData & extras() const
Definition translator.h:162
static void addPoHeader(Translator::ExtraData &headers, QStringList &hdrOrder, const char *name, const QString &value)
Definition po.cpp:684
static QString poEscapedLines(const QString &prefix, bool addSpace, const QStringList &lines)
Definition po.cpp:113
static void splitContext(QByteArray *comment, QByteArray *context)
Definition po.cpp:322
static QString poEscapedString(const QString &prefix, const QString &keyword, bool noWrap, const QString &ba)
Definition po.cpp:24
static void slurpComment(QByteArray &msg, const QList< QByteArray > &lines, int &l)
Definition po.cpp:297
static QT_BEGIN_NAMESPACE const int MAX_LEN
Definition po.cpp:22
bool savePO(const Translator &translator, QIODevice &dev, ConversionData &)
Definition po.cpp:703
static QByteArray slurpEscapedString(const QList< QByteArray > &lines, int &l, int offset, const QByteArray &prefix, ConversionData &cd)
Definition po.cpp:189
static QString escapeComment(const QString &in, bool escape)
Definition po.cpp:693
static QString poWrappedEscapedLines(const QString &prefix, bool addSpace, const QString &line)
Definition po.cpp:134
static QString makePoHeader(const QString &str)
Definition po.cpp:345
static bool isTranslationLine(const QByteArray &line)
Definition po.cpp:184
static QByteArray QByteArrayList_join(const QList< QByteArray > &that, char sep)
Definition po.cpp:350
int initPO()
Definition po.cpp:857
static bool savePOT(const Translator &translator, QIODevice &dev, ConversionData &cd)
Definition po.cpp:850
bool loadPO(Translator &translator, QIODevice &dev, ConversionData &cd)
Definition po.cpp:373
Definition po.cpp:158
QByteArray id
Definition po.cpp:166
bool isFuzzy
Definition po.cpp:179
QByteArray references
Definition po.cpp:172
QByteArray automaticComments
Definition po.cpp:174
QByteArray oldMsgId
Definition po.cpp:176
QHash< QString, QString > extra
Definition po.cpp:180
PoItem()
Definition po.cpp:160
QByteArray fileName
Definition po.cpp:171
QByteArray lineNumber
Definition po.cpp:170
QByteArray tscomment
Definition po.cpp:168
QByteArray translatorComments
Definition po.cpp:173
QByteArray context
Definition po.cpp:167
QList< QByteArray > msgStr
Definition po.cpp:177
bool isPlural
Definition po.cpp:178
QByteArray oldTscomment
Definition po.cpp:169
QByteArray msgId
Definition po.cpp:175
const char * untranslatedDescription
Definition translator.h:171