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 const auto &headers = item.msgStr.constFirst().split('\n');
427 for (const QByteArray &hdr : headers) {
428 if (hdr.isEmpty())
429 continue;
430 int idx = hdr.indexOf(':');
431 if (idx < 0) {
432 cd.appendError(QString::fromLatin1("Unexpected PO header format '%1'")
433 .arg(QString::fromLatin1(hdr)));
434 error = true;
435 break;
436 }
437 QByteArray hdrName = hdr.left(idx).trimmed();
438 QByteArray hdrValue = hdr.mid(idx + 1).trimmed();
439 hdrOrder << hdrName;
440 if (hdrName == "X-Language") {
441 translator.setLanguageCode(QString::fromLatin1(hdrValue));
442 } else if (hdrName == "X-Source-Language") {
443 translator.setSourceLanguageCode(QString::fromLatin1(hdrValue));
444 } else if (hdrName == "X-Qt-Contexts") {
445 qtContexts = (hdrValue == "true");
446 } else if (hdrName == "Plural-Forms") {
447 pluralForms = hdrValue;
448 } else if (hdrName == "MIME-Version") {
449 // just assume it is 1.0
450 } else if (hdrName == "Content-Type") {
451 if (!hdrValue.startsWith("text/plain; charset=")) {
452 cd.appendError(QString::fromLatin1("Unexpected Content-Type header '%1'")
453 .arg(QString::fromLatin1(hdrValue)));
454 error = true;
455 // This will avoid a flood of conversion errors.
456 toUnicode = QStringDecoder(QStringConverter::Latin1);
457 } else {
458 QByteArray cod = hdrValue.mid(20);
459 auto enc = QStringConverter::encodingForName(cod);
460 if (!enc) {
461 cd.appendError(QString::fromLatin1("Unsupported encoding '%1'")
462 .arg(QString::fromLatin1(cod)));
463 error = true;
464 // This will avoid a flood of conversion errors.
465 toUnicode = QStringDecoder(QStringConverter::Latin1);
466 } else {
467 toUnicode = QStringDecoder(*enc);
468 }
469 }
470 } else if (hdrName == "Content-Transfer-Encoding") {
471 if (hdrValue != "8bit") {
472 cd.appendError(QString::fromLatin1("Unexpected Content-Transfer-Encoding '%1'")
473 .arg(QString::fromLatin1(hdrValue)));
474 return false;
475 }
476 } else if (hdrName == "X-Virgin-Header") {
477 // legacy
478 } else {
479 extras[makePoHeader(QString::fromLatin1(hdrName))] = hdrValue;
480 }
481 }
482 if (!pluralForms.isEmpty()) {
483 if (translator.languageCode().isEmpty()) {
484 extras[makePoHeader("Plural-Forms"_L1)] = pluralForms;
485 } else {
486 // FIXME: have fun with making a consistency check ...
487 }
488 }
489 // Eliminate the field if only headers we added are present in standard order.
490 // Keep in sync with savePO
491 static const char * const dfltHdrs[] = {
492 "MIME-Version", "Content-Type", "Content-Transfer-Encoding",
493 "Plural-Forms", "X-Language", "X-Source-Language", "X-Qt-Contexts"
494 };
495 uint cdh = 0;
496 for (int cho = 0; cho < hdrOrder.size(); cho++) {
497 for (;; cdh++) {
498 if (cdh == sizeof(dfltHdrs)/sizeof(dfltHdrs[0])) {
499 extras["po-headers"_L1] = QByteArrayList_join(hdrOrder, ',');
500 goto doneho;
501 }
502 if (hdrOrder.at(cho) == dfltHdrs[cdh]) {
503 cdh++;
504 break;
505 }
506 }
507 }
508 doneho:
509 if (lastCmtLine != -1) {
510 extras["po-header_comment"_L1] =
511 QByteArrayList_join(lines.mid(0, lastCmtLine + 1), '\n');
512 }
513 for (auto it = extras.cbegin(), end = extras.cend(); it != end; ++it)
514 translator.setExtra(it.key(), toUnicode(it.value()));
515 item = PoItem();
516 continue;
517 }
518 // build translator message
520 msg.setContext(toUnicode(item.context));
521 if (!item.references.isEmpty()) {
522 QString xrefs;
523 const auto &refs = QString(toUnicode(item.references))
524 .split(QRegularExpression("\\s"_L1), Qt::SkipEmptyParts);
525 for (const QString &ref : refs) {
526 int pos = ref.indexOf(u':');
527 int lpos = ref.lastIndexOf(u':');
528 if (pos != -1 && pos == lpos) {
529 bool ok;
530 int lno = ref.mid(pos + 1).toInt(&ok);
531 if (ok) {
532 msg.addReference(ref.left(pos), lno, -1, -1);
533 continue;
534 }
535 }
536 if (!xrefs.isEmpty())
537 xrefs += u' ';
538 xrefs += ref;
539 }
540 if (!xrefs.isEmpty())
541 item.extra["po-references"_L1] = xrefs;
542 }
543 msg.setId(toUnicode(item.id));
544 msg.setSourceText(toUnicode(item.msgId));
545 msg.setOldSourceText(toUnicode(item.oldMsgId));
546 msg.setComment(toUnicode(item.tscomment));
547 msg.setOldComment(toUnicode(item.oldTscomment));
548 msg.setExtraComment(toUnicode(item.automaticComments));
549 msg.setTranslatorComment(toUnicode(item.translatorComments));
550 msg.setPlural(item.isPlural || item.msgStr.size() > 1);
551 QStringList translations;
552 for (const QByteArray &bstr : std::as_const(item.msgStr)) {
553 QString str = toUnicode(bstr);
554 str.replace(Translator::TextVariantSeparator, Translator::BinaryVariantSeparator);
555 translations << str;
556 }
557 msg.setTranslations(translations);
558 bool isFuzzy = item.isFuzzy || (!msg.sourceText().isEmpty() && !msg.isTranslated());
559 if (isObsolete && isFuzzy)
561 else if (isObsolete)
563 else if (isFuzzy)
565 else
567 msg.setExtras(item.extra);
568
569 //qDebug() << "WRITE: " << context;
570 //qDebug() << "SOURCE: " << msg.sourceText();
571 //qDebug() << flags << msg.m_extra;
572 translator.append(msg);
573 item = PoItem();
574 } else if (line.startsWith('#')) {
575 switch (line.size() < 2 ? 0 : line.at(1)) {
576 case ':':
577 item.references += line.mid(3);
578 item.references += '\n';
579 break;
580 case ',': {
581 QStringList flags =
582 QString::fromLatin1(line.mid(2))
583 .split(QRegularExpression("[, ]"_L1), Qt::SkipEmptyParts);
584 if (flags.removeOne("fuzzy"_L1))
585 item.isFuzzy = true;
586 flags.removeOne("qt-format"_L1);
587 const auto it = item.extra.constFind("po-flags"_L1);
588 if (it != item.extra.cend())
589 flags.prepend(*it);
590 if (!flags.isEmpty())
591 item.extra["po-flags"_L1] = flags.join(", "_L1);
592 break;
593 }
594 case 0:
595 item.translatorComments += '\n';
596 break;
597 case ' ':
598 slurpComment(item.translatorComments, lines, l);
599 break;
600 case '.':
601 if (line.startsWith("#. ts-context ")) { // legacy
602 item.context = line.mid(14);
603 } else if (line.startsWith("#. ts-id ")) {
604 item.id = line.mid(9);
605 } else {
606 item.automaticComments += line.mid(3);
607
608 }
609 break;
610 case '|':
611 if (line.startsWith("#| msgid ")) {
612 item.oldMsgId = slurpEscapedString(lines, l, 9, "#| ", cd);
613 } else if (line.startsWith("#| msgid_plural ")) {
614 QByteArray extra = slurpEscapedString(lines, l, 16, "#| ", cd);
615 if (extra != item.oldMsgId)
616 item.extra["po-old_msgid_plural"_L1] = 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("PO-format parse error in line %1: '%2'"_L1)
623 .arg(l + 1)
624 .arg(toUnicode(lines[l])));
625 error = true;
626 }
627 break;
628 case '~':
629 if (line.startsWith("#~ msgid ")) {
630 item.msgId = slurpEscapedString(lines, l, 9, "#~ ", cd);
631 } else if (line.startsWith("#~ msgid_plural ")) {
632 QByteArray extra = slurpEscapedString(lines, l, 16, "#~ ", cd);
633 if (extra != item.msgId)
634 item.extra["po-msgid_plural"_L1] = 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["po-old_msgid_plural"_L1] = toUnicode(extra);
646 } else if (line.startsWith("#~| msgctxt ")) {
647 item.oldTscomment = slurpEscapedString(lines, l, 12, "#~| ", cd);
648 if (qtContexts)
649 splitContext(&item.oldTscomment, &item.context);
650 } else {
651 cd.appendError(QString("PO-format parse error in line %1: '%2'"_L1)
652 .arg(l + 1)
653 .arg(toUnicode(lines[l])));
654 error = true;
655 }
656 break;
657 default:
658 cd.appendError(QString("PO-format parse error in line %1: '%2'"_L1)
659 .arg(l + 1)
660 .arg(toUnicode(lines[l])));
661 error = true;
662 break;
663 }
664 lastCmtLine = l;
665 } else if (line.startsWith("msgctxt ")) {
666 item.tscomment = slurpEscapedString(lines, l, 8, QByteArray(), cd);
667 if (qtContexts)
668 splitContext(&item.tscomment, &item.context);
669 } else if (line.startsWith("msgid ")) {
670 item.msgId = slurpEscapedString(lines, l, 6, QByteArray(), cd);
671 } else if (line.startsWith("msgid_plural ")) {
672 QByteArray extra = slurpEscapedString(lines, l, 13, QByteArray(), cd);
673 if (extra != item.msgId)
674 item.extra["po-msgid_plural"_L1] = toUnicode(extra);
675 item.isPlural = true;
676 } else {
677 cd.appendError(QString("PO-format error in line %1: '%2'"_L1)
678 .arg(l + 1)
679 .arg(toUnicode(lines[l])));
680 error = true;
681 }
682 }
683 return !error && cd.errors().isEmpty();
684}
685
686static void addPoHeader(Translator::ExtraData &headers, QStringList &hdrOrder,
687 const char *name, const QString &value)
688{
689 QString qName = QLatin1String(name);
690 if (!hdrOrder.contains(qName))
691 hdrOrder << qName;
692 headers[makePoHeader(qName)] = value;
693}
694
695static QString escapeComment(const QString &in, bool escape)
696{
697 QString out = in;
698 if (escape) {
699 out.replace(u'~', "~~"_L1);
700 out.replace(u'|', "~|"_L1);
701 }
702 return out;
703}
704
705bool savePO(const Translator &translator, QIODevice &dev, ConversionData &)
706{
707 QString str_format = "-format"_L1;
708
709 bool ok = true;
710 QTextStream out(&dev);
711
712 bool qtContexts = false;
713 for (const TranslatorMessage &msg : translator.messages())
714 if (!msg.context().isEmpty()) {
715 qtContexts = true;
716 break;
717 }
718
719 QString cmt = translator.extra("po-header_comment"_L1);
720 if (!cmt.isEmpty())
721 out << cmt << '\n';
722 out << "msgid \"\"\n";
723 Translator::ExtraData headers = translator.extras();
724 QStringList hdrOrder = translator.extra("po-headers"_L1).split(u',', Qt::SkipEmptyParts);
725 // Keep in sync with loadPO
726 addPoHeader(headers, hdrOrder, "MIME-Version", "1.0"_L1);
727 addPoHeader(headers, hdrOrder, "Content-Type", "text/plain; charset=UTF-8"_L1);
728 addPoHeader(headers, hdrOrder, "Content-Transfer-Encoding", "8bit"_L1);
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", "true"_L1);
742 QString hdrStr;
743 for (const QString &hdr : std::as_const(hdrOrder)) {
744 hdrStr += hdr;
745 hdrStr += ": "_L1;
746 hdrStr += headers.value(makePoHeader(hdr));
747 hdrStr += u'\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("#"_L1, true, msg.translatorComment());
756
757 if (!msg.extraComment().isEmpty())
758 out << poEscapedLines("#."_L1, true, msg.extraComment());
759
760 if (!msg.id().isEmpty())
761 out << "#. ts-id "_L1 << msg.id() << '\n';
762
763 QString xrefs = msg.extra("po-references"_L1);
764 if (!msg.fileName().isEmpty() || !xrefs.isEmpty()) {
765 QStringList refs;
766 for (const TranslatorMessage::Reference &ref : msg.allReferences())
767 refs.append(QString("%2:%1"_L1).arg(ref.lineNumber()).arg(ref.fileName()));
768 if (!xrefs.isEmpty())
769 refs << xrefs;
770 out << poWrappedEscapedLines("#:"_L1, true, refs.join(u' '));
771 }
772
773 bool noWrap = false;
774 bool skipFormat = false;
775 QStringList flags;
776 if ((msg.type() == TranslatorMessage::Unfinished
777 || msg.type() == TranslatorMessage::Obsolete) && msg.isTranslated())
778 flags.append("fuzzy"_L1);
779 const auto itr = msg.extras().constFind("po-flags"_L1);
780 if (itr != msg.extras().cend()) {
781 const QStringList atoms = itr->split(", "_L1);
782 for (const QString &atom : atoms)
783 if (atom.endsWith(str_format)) {
784 skipFormat = true;
785 break;
786 }
787 if (atoms.contains("no-wrap"_L1))
788 noWrap = true;
789 flags.append(*itr);
790 }
791 if (!skipFormat) {
792 QString source = msg.sourceText();
793 // This is fuzzy logic, as we don't know whether the string is
794 // actually used with QString::arg().
795 for (int off = 0; (off = source.indexOf(u'%', off)) >= 0;) {
796 if (++off >= source.size())
797 break;
798 if (source.at(off) == u'n' || source.at(off).isDigit()) {
799 flags.append("qt-format"_L1);
800 break;
801 }
802 }
803 }
804 if (!flags.isEmpty())
805 out << "#, " << flags.join(", "_L1) << '\n';
806
807 bool isObsolete = (msg.type() == TranslatorMessage::Obsolete
808 || msg.type() == TranslatorMessage::Vanished);
809 QString prefix = QLatin1String(isObsolete ? "#~| " : "#| ");
810 if (!msg.oldComment().isEmpty())
811 out << poEscapedString(prefix, "msgctxt"_L1, noWrap,
812 escapeComment(msg.oldComment(), qtContexts));
813 if (!msg.oldSourceText().isEmpty())
814 out << poEscapedString(prefix, "msgid"_L1, noWrap, msg.oldSourceText());
815 QString plural = msg.extra("po-old_msgid_plural"_L1);
816 if (!plural.isEmpty())
817 out << poEscapedString(prefix, "msgid_plural"_L1, noWrap, plural);
818 prefix = QLatin1String(isObsolete ? "#~ " : "");
819 if (!msg.context().isEmpty())
820 out << poEscapedString(prefix, "msgctxt"_L1, noWrap,
821 escapeComment(msg.context(), true) + u'|'
822 + escapeComment(msg.comment(), true));
823 else if (!msg.comment().isEmpty())
824 out << poEscapedString(prefix, "msgctxt"_L1, noWrap,
825 escapeComment(msg.comment(), qtContexts));
826 out << poEscapedString(prefix, "msgid"_L1, noWrap, msg.sourceText());
827 if (!msg.isPlural()) {
828 QString transl = msg.translation();
829 transl.replace(Translator::BinaryVariantSeparator, Translator::TextVariantSeparator);
830 out << poEscapedString(prefix, "msgstr"_L1, noWrap, transl);
831 } else {
832 QString plural = msg.extra("po-msgid_plural"_L1);
833 if (plural.isEmpty())
834 plural = msg.sourceText();
835 out << poEscapedString(prefix, "msgid_plural"_L1, noWrap, plural);
836 const QStringList &translations = msg.translations();
837 for (int i = 0; i != translations.size(); ++i) {
838 QString str = translations.at(i);
839 str.replace(QChar(Translator::BinaryVariantSeparator),
840 QChar(Translator::TextVariantSeparator));
841 out << poEscapedString(prefix, QString::fromLatin1("msgstr[%1]").arg(i), noWrap,
842 str);
843 }
844 }
845 }
846 return ok;
847}
848
849static bool savePOT(const Translator &translator, QIODevice &dev, ConversionData &cd)
850{
851 Translator ttor = translator;
853 return savePO(ttor, dev, cd);
854}
855
857{
858 Translator::FileFormat format;
859 format.extension = "po"_L1;
860 format.untranslatedDescription = QT_TRANSLATE_NOOP("FMT", "GNU Gettext localization files");
861 format.loader = &loadPO;
862 format.saver = &savePO;
864 format.priority = 1;
866 format.extension = "pot"_L1;
867 format.untranslatedDescription = QT_TRANSLATE_NOOP("FMT", "GNU Gettext localization template files");
868 format.loader = &loadPO;
869 format.saver = &savePOT;
871 format.priority = -1;
873 return 1;
874}
875
876Q_CONSTRUCTOR_FUNCTION(initPO)
877
878QT_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:155
static void addPoHeader(Translator::ExtraData &headers, QStringList &hdrOrder, const char *name, const QString &value)
Definition po.cpp:686
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:705
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:695
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:856
static bool savePOT(const Translator &translator, QIODevice &dev, ConversionData &cd)
Definition po.cpp:849
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:164