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