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(QChar(Translator::TextVariantSeparator),
554 QChar(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)).split(
583 QRegularExpression(QLatin1String("[, ]")), Qt::SkipEmptyParts);
584 if (flags.removeOne(QLatin1String("fuzzy")))
585 item.isFuzzy = true;
586 flags.removeOne(QLatin1String("qt-format"));
587 const auto it = item.extra.constFind(QLatin1String("po-flags"));
588 if (it != item.extra.cend())
589 flags.prepend(*it);
590 if (!flags.isEmpty())
591 item.extra[QLatin1String("po-flags")] = flags.join(QLatin1String(", "));
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[QLatin1String("po-old_msgid_plural")] =
617 toUnicode(extra);
618 } else if (line.startsWith("#| msgctxt ")) {
619 item.oldTscomment = slurpEscapedString(lines, l, 11, "#| ", cd);
620 if (qtContexts)
621 splitContext(&item.oldTscomment, &item.context);
622 } else {
623 cd.appendError(QString(QLatin1String("PO-format parse error in line %1: '%2'"))
624 .arg(l + 1).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[QLatin1String("po-msgid_plural")] =
635 toUnicode(extra);
636 item.isPlural = true;
637 } else if (line.startsWith("#~ msgctxt ")) {
638 item.tscomment = slurpEscapedString(lines, l, 11, "#~ ", cd);
639 if (qtContexts)
640 splitContext(&item.tscomment, &item.context);
641 } else if (line.startsWith("#~| msgid ")) {
642 item.oldMsgId = slurpEscapedString(lines, l, 10, "#~| ", cd);
643 } else if (line.startsWith("#~| msgid_plural ")) {
644 QByteArray extra = slurpEscapedString(lines, l, 17, "#~| ", cd);
645 if (extra != item.oldMsgId)
646 item.extra[QLatin1String("po-old_msgid_plural")] =
647 toUnicode(extra);
648 } else if (line.startsWith("#~| msgctxt ")) {
649 item.oldTscomment = slurpEscapedString(lines, l, 12, "#~| ", cd);
650 if (qtContexts)
651 splitContext(&item.oldTscomment, &item.context);
652 } else {
653 cd.appendError(QString(QLatin1String("PO-format parse error in line %1: '%2'"))
654 .arg(l + 1).arg(toUnicode(lines[l])));
655 error = true;
656 }
657 break;
658 default:
659 cd.appendError(QString(QLatin1String("PO-format parse error in line %1: '%2'"))
660 .arg(l + 1).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[QLatin1String("po-msgid_plural")] = toUnicode(extra);
675 item.isPlural = true;
676 } else {
677 cd.appendError(QString(QLatin1String("PO-format error in line %1: '%2'"))
678 .arg(l + 1).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(QLatin1Char('~'), QLatin1String("~~"));
699 out.replace(QLatin1Char('|'), QLatin1String("~|"));
700 }
701 return out;
702}
703
704bool savePO(const Translator &translator, QIODevice &dev, ConversionData &)
705{
706 QString str_format = QLatin1String("-format");
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(QLatin1String("po-header_comment"));
719 if (!cmt.isEmpty())
720 out << cmt << '\n';
721 out << "msgid \"\"\n";
722 Translator::ExtraData headers = translator.extras();
723 QStringList hdrOrder = translator.extra(QLatin1String("po-headers")).split(
724 QLatin1Char(','), Qt::SkipEmptyParts);
725 // Keep in sync with loadPO
726 addPoHeader(headers, hdrOrder, "MIME-Version", QLatin1String("1.0"));
727 addPoHeader(headers, hdrOrder, "Content-Type",
728 QLatin1String("text/plain; charset=UTF-8"));
729 addPoHeader(headers, hdrOrder, "Content-Transfer-Encoding", QLatin1String("8bit"));
730 if (!translator.languageCode().isEmpty()) {
731 QLocale::Language l;
732 QLocale::Territory c;
733 Translator::languageAndTerritory(translator.languageCode(), &l, &c);
734 const char *gettextRules;
735 if (getNumerusInfo(l, c, 0, 0, &gettextRules))
736 addPoHeader(headers, hdrOrder, "Plural-Forms", QLatin1String(gettextRules));
737 addPoHeader(headers, hdrOrder, "X-Language", translator.languageCode());
738 }
739 if (!translator.sourceLanguageCode().isEmpty())
740 addPoHeader(headers, hdrOrder, "X-Source-Language", translator.sourceLanguageCode());
741 if (qtContexts)
742 addPoHeader(headers, hdrOrder, "X-Qt-Contexts", QLatin1String("true"));
743 QString hdrStr;
744 for (const QString &hdr : std::as_const(hdrOrder)) {
745 hdrStr += hdr;
746 hdrStr += QLatin1String(": ");
747 hdrStr += headers.value(makePoHeader(hdr));
748 hdrStr += QLatin1Char('\n');
749 }
750 out << poEscapedString(QString(), QString::fromLatin1("msgstr"), true, hdrStr);
751
752 for (const TranslatorMessage &msg : translator.messages()) {
753 out << Qt::endl;
754
755 if (!msg.translatorComment().isEmpty())
756 out << poEscapedLines(QLatin1String("#"), true, msg.translatorComment());
757
758 if (!msg.extraComment().isEmpty())
759 out << poEscapedLines(QLatin1String("#."), true, msg.extraComment());
760
761 if (!msg.id().isEmpty())
762 out << QLatin1String("#. ts-id ") << msg.id() << '\n';
763
764 QString xrefs = msg.extra(QLatin1String("po-references"));
765 if (!msg.fileName().isEmpty() || !xrefs.isEmpty()) {
766 QStringList refs;
767 for (const TranslatorMessage::Reference &ref : msg.allReferences())
768 refs.append(QString(QLatin1String("%2:%1"))
769 .arg(ref.lineNumber()).arg(ref.fileName()));
770 if (!xrefs.isEmpty())
771 refs << xrefs;
772 out << poWrappedEscapedLines(QLatin1String("#:"), true, refs.join(QLatin1Char(' ')));
773 }
774
775 bool noWrap = false;
776 bool skipFormat = false;
777 QStringList flags;
778 if ((msg.type() == TranslatorMessage::Unfinished
779 || msg.type() == TranslatorMessage::Obsolete) && msg.isTranslated())
780 flags.append(QLatin1String("fuzzy"));
781 const auto itr = msg.extras().constFind(QLatin1String("po-flags"));
782 if (itr != msg.extras().cend()) {
783 const QStringList atoms = itr->split(QLatin1String(", "));
784 for (const QString &atom : atoms)
785 if (atom.endsWith(str_format)) {
786 skipFormat = true;
787 break;
788 }
789 if (atoms.contains(QLatin1String("no-wrap")))
790 noWrap = true;
791 flags.append(*itr);
792 }
793 if (!skipFormat) {
794 QString source = msg.sourceText();
795 // This is fuzzy logic, as we don't know whether the string is
796 // actually used with QString::arg().
797 for (int off = 0; (off = source.indexOf(QLatin1Char('%'), off)) >= 0; ) {
798 if (++off >= source.size())
799 break;
800 if (source.at(off) == QLatin1Char('n') || source.at(off).isDigit()) {
801 flags.append(QLatin1String("qt-format"));
802 break;
803 }
804 }
805 }
806 if (!flags.isEmpty())
807 out << "#, " << flags.join(QLatin1String(", ")) << '\n';
808
809 bool isObsolete = (msg.type() == TranslatorMessage::Obsolete
810 || msg.type() == TranslatorMessage::Vanished);
811 QString prefix = QLatin1String(isObsolete ? "#~| " : "#| ");
812 if (!msg.oldComment().isEmpty())
813 out << poEscapedString(prefix, QLatin1String("msgctxt"), noWrap,
814 escapeComment(msg.oldComment(), qtContexts));
815 if (!msg.oldSourceText().isEmpty())
816 out << poEscapedString(prefix, QLatin1String("msgid"), noWrap, msg.oldSourceText());
817 QString plural = msg.extra(QLatin1String("po-old_msgid_plural"));
818 if (!plural.isEmpty())
819 out << poEscapedString(prefix, QLatin1String("msgid_plural"), noWrap, plural);
820 prefix = QLatin1String(isObsolete ? "#~ " : "");
821 if (!msg.context().isEmpty())
822 out << poEscapedString(prefix, QLatin1String("msgctxt"), noWrap,
823 escapeComment(msg.context(), true) + QLatin1Char('|')
824 + escapeComment(msg.comment(), true));
825 else if (!msg.comment().isEmpty())
826 out << poEscapedString(prefix, QLatin1String("msgctxt"), noWrap,
827 escapeComment(msg.comment(), qtContexts));
828 out << poEscapedString(prefix, QLatin1String("msgid"), noWrap, msg.sourceText());
829 if (!msg.isPlural()) {
830 QString transl = msg.translation();
831 transl.replace(QChar(Translator::BinaryVariantSeparator),
832 QChar(Translator::TextVariantSeparator));
833 out << poEscapedString(prefix, QLatin1String("msgstr"), noWrap, transl);
834 } else {
835 QString plural = msg.extra(QLatin1String("po-msgid_plural"));
836 if (plural.isEmpty())
837 plural = msg.sourceText();
838 out << poEscapedString(prefix, QLatin1String("msgid_plural"), noWrap, plural);
839 const QStringList &translations = msg.translations();
840 for (int i = 0; i != translations.size(); ++i) {
841 QString str = translations.at(i);
842 str.replace(QChar(Translator::BinaryVariantSeparator),
843 QChar(Translator::TextVariantSeparator));
844 out << poEscapedString(prefix, QString::fromLatin1("msgstr[%1]").arg(i), noWrap,
845 str);
846 }
847 }
848 }
849 return ok;
850}
851
852static bool savePOT(const Translator &translator, QIODevice &dev, ConversionData &cd)
853{
854 Translator ttor = translator;
856 return savePO(ttor, dev, cd);
857}
858
860{
861 Translator::FileFormat format;
862 format.extension = QLatin1String("po");
863 format.untranslatedDescription = QT_TRANSLATE_NOOP("FMT", "GNU Gettext localization files");
864 format.loader = &loadPO;
865 format.saver = &savePO;
867 format.priority = 1;
869 format.extension = QLatin1String("pot");
870 format.untranslatedDescription = QT_TRANSLATE_NOOP("FMT", "GNU Gettext localization template files");
871 format.loader = &loadPO;
872 format.saver = &savePOT;
874 format.priority = -1;
876 return 1;
877}
878
879Q_CONSTRUCTOR_FUNCTION(initPO)
880
881QT_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:685
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:704
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:694
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:859
static bool savePOT(const Translator &translator, QIODevice &dev, ConversionData &cd)
Definition po.cpp:852
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