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(t: 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, base: 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(t: 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(ch: u' ', from: 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(ch: u' ', from: off + maxlen) + 1;
96 if (!idx)
97 break;
98#endif
99 }
100 lines.append(t: line.mid(position: off, n: idx - off));
101 off = idx;
102 }
103 lines.append(t: line.mid(position: off));
104 }
105 }
106 } else if (lines.size() > 1) {
107 lines.prepend(t: QString());
108 }
109 }
110 return prefix + keyword + QLatin1String(" \"")
111 + lines.join(sep: 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(ba: "\n"))
131 in.chop(n: 1);
132 return poEscapedLines(prefix, addSpace, lines: in.split(sep: 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(ch: u' ', from: 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(ch: u' ', from: off + maxlen);
147 if (idx < 0)
148 break;
149#endif
150 }
151 lines.append(t: line.mid(position: off, n: idx - off));
152 off = idx + 1;
153 }
154 lines.append(t: line.mid(position: off));
155 return poEscapedLines(prefix, addSpace, lines);
156}
157
158struct PoItem
159{
160public:
161 PoItem()
162 : isPlural(false), isFuzzy(false)
163 {}
164
165
166public:
167 QByteArray id;
168 QByteArray context;
169 QByteArray tscomment;
170 QByteArray oldTscomment;
171 QByteArray lineNumber;
172 QByteArray fileName;
173 QByteArray references;
174 QByteArray translatorComments;
175 QByteArray automaticComments;
176 QByteArray msgId;
177 QByteArray oldMsgId;
178 QList<QByteArray> msgStr;
179 bool isPlural;
180 bool isFuzzy;
181 QHash<QString, QString> extra;
182};
183
184
185static bool isTranslationLine(const QByteArray &line)
186{
187 return line.startsWith(bv: "#~ msgstr") || line.startsWith(bv: "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(i: l);
198 if (line.isEmpty() || !line.startsWith(bv: 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(error: QString::fromLatin1(
216 ba: "PO parsing error: extra characters on line %1.")
217 .arg(a: 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(index: stoff, len: offset - stoff).toUInt(ok: 0, base: 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(index: stoff, len: offset - stoff).toUInt(ok: 0, base: 16);
274 break;
275 default:
276 cd.appendError(error: QString::fromLatin1(
277 ba: "PO parsing error: invalid escape '\\%1' (line %2).")
278 .arg(a: QChar((uint)c)).arg(a: 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(error: QString::fromLatin1(
294 ba: "PO parsing error: premature end of line %1.").arg(a: 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(i: l);
302 for (int i = 1; ; i++) {
303 if (prefix.at(i) != ' ') {
304 prefix.truncate(pos: i);
305 break;
306 }
307 }
308 for (; l < lines.size(); ++l) {
309 const QByteArray &line = lines.at(i: l);
310 if (line.startsWith(bv: prefix)) {
311 if (l > firstLine)
312 msg += '\n';
313 msg += line.mid(index: 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(index: sep + 1, len: j - sep - 1);
338 comment->truncate(pos: sep);
339 *context = *comment;
340 *comment = tmp;
341 } else {
342 comment->truncate(pos: j);
343 }
344}
345
346static QString makePoHeader(const QString &str)
347{
348 return "po-header-"_L1 + str.toLower().replace(before: u'-', after: 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(asize: 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(t: dev.readLine().trimmed());
401 lines.append(t: 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(i: l);
408 if (line.isEmpty())
409 continue;
410 if (isTranslationLine(line)) {
411 bool isObsolete = line.startsWith(bv: "#~ msgstr");
412 const QByteArray prefix = isObsolete ? "#~ " : "";
413 while (true) {
414 int idx = line.indexOf(ch: ' ', from: prefix.size());
415 QByteArray str = slurpEscapedString(lines, l, offset: idx, prefix, cd);
416 item.msgStr.append(t: str);
417 if (l + 1 >= lines.size() || !isTranslationLine(line: lines.at(i: l + 1)))
418 break;
419 ++l;
420 line = lines.at(i: 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(sep: '\n')) {
427 if (hdr.isEmpty())
428 continue;
429 int idx = hdr.indexOf(ch: ':');
430 if (idx < 0) {
431 cd.appendError(error: QString::fromLatin1(ba: "Unexpected PO header format '%1'")
432 .arg(a: QString::fromLatin1(ba: hdr)));
433 error = true;
434 break;
435 }
436 QByteArray hdrName = hdr.left(n: idx).trimmed();
437 QByteArray hdrValue = hdr.mid(index: idx + 1).trimmed();
438 hdrOrder << hdrName;
439 if (hdrName == "X-Language") {
440 translator.setLanguageCode(QString::fromLatin1(ba: hdrValue));
441 } else if (hdrName == "X-Source-Language") {
442 translator.setSourceLanguageCode(QString::fromLatin1(ba: 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(bv: "text/plain; charset=")) {
451 cd.appendError(error: QString::fromLatin1(ba: "Unexpected Content-Type header '%1'")
452 .arg(a: QString::fromLatin1(ba: 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(index: 20);
458 auto enc = QStringConverter::encodingForName(name: cod);
459 if (!enc) {
460 cd.appendError(error: QString::fromLatin1(ba: "Unsupported encoding '%1'")
461 .arg(a: QString::fromLatin1(ba: 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(error: QString::fromLatin1(ba: "Unexpected Content-Transfer-Encoding '%1'")
472 .arg(a: QString::fromLatin1(ba: hdrValue)));
473 return false;
474 }
475 } else if (hdrName == "X-Virgin-Header") {
476 // legacy
477 } else {
478 extras[makePoHeader(str: QString::fromLatin1(ba: hdrName))] = hdrValue;
479 }
480 }
481 if (!pluralForms.isEmpty()) {
482 if (translator.languageCode().isEmpty()) {
483 extras[makePoHeader(str: "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(that: hdrOrder, sep: ',');
499 goto doneho;
500 }
501 if (hdrOrder.at(i: 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(that: lines.mid(pos: 0, len: lastCmtLine + 1), sep: '\n');
511 }
512 for (auto it = extras.cbegin(), end = extras.cend(); it != end; ++it)
513 translator.setExtra(ba: it.key(), var: toUnicode(it.value()));
514 item = PoItem();
515 continue;
516 }
517 // build translator message
518 TranslatorMessage msg;
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(sep: QRegularExpression("\\s"_L1), behavior: Qt::SkipEmptyParts)) {
525 int pos = ref.indexOf(ch: u':');
526 int lpos = ref.lastIndexOf(c: u':');
527 if (pos != -1 && pos == lpos) {
528 bool ok;
529 int lno = ref.mid(position: pos + 1).toInt(ok: &ok);
530 if (ok) {
531 msg.addReference(fileName: ref.left(n: pos), lineNumber: 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(t&: item.msgStr)) {
552 QString str = toUnicode(bstr);
553 str.replace(before: Translator::TextVariantSeparator, after: Translator::BinaryVariantSeparator);
554 translations << str;
555 }
556 msg.setTranslations(translations);
557 bool isFuzzy = item.isFuzzy || (!msg.sourceText().isEmpty() && !msg.isTranslated());
558 if (isObsolete && isFuzzy)
559 msg.setType(TranslatorMessage::Obsolete);
560 else if (isObsolete)
561 msg.setType(TranslatorMessage::Vanished);
562 else if (isFuzzy)
563 msg.setType(TranslatorMessage::Unfinished);
564 else
565 msg.setType(TranslatorMessage::Finished);
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(c: '#')) {
574 switch (line.size() < 2 ? 0 : line.at(i: 1)) {
575 case ':':
576 item.references += line.mid(index: 3);
577 item.references += '\n';
578 break;
579 case ',': {
580 QStringList flags =
581 QString::fromLatin1(ba: line.mid(index: 2))
582 .split(sep: QRegularExpression("[, ]"_L1), behavior: Qt::SkipEmptyParts);
583 if (flags.removeOne(t: "fuzzy"_L1))
584 item.isFuzzy = true;
585 flags.removeOne(t: "qt-format"_L1);
586 const auto it = item.extra.constFind(key: "po-flags"_L1);
587 if (it != item.extra.cend())
588 flags.prepend(t: *it);
589 if (!flags.isEmpty())
590 item.extra["po-flags"_L1] = flags.join(sep: ", "_L1);
591 break;
592 }
593 case 0:
594 item.translatorComments += '\n';
595 break;
596 case ' ':
597 slurpComment(msg&: item.translatorComments, lines, l);
598 break;
599 case '.':
600 if (line.startsWith(bv: "#. ts-context ")) { // legacy
601 item.context = line.mid(index: 14);
602 } else if (line.startsWith(bv: "#. ts-id ")) {
603 item.id = line.mid(index: 9);
604 } else {
605 item.automaticComments += line.mid(index: 3);
606
607 }
608 break;
609 case '|':
610 if (line.startsWith(bv: "#| msgid ")) {
611 item.oldMsgId = slurpEscapedString(lines, l, offset: 9, prefix: "#| ", cd);
612 } else if (line.startsWith(bv: "#| msgid_plural ")) {
613 QByteArray extra = slurpEscapedString(lines, l, offset: 16, prefix: "#| ", cd);
614 if (extra != item.oldMsgId)
615 item.extra["po-old_msgid_plural"_L1] = toUnicode(extra);
616 } else if (line.startsWith(bv: "#| msgctxt ")) {
617 item.oldTscomment = slurpEscapedString(lines, l, offset: 11, prefix: "#| ", cd);
618 if (qtContexts)
619 splitContext(comment: &item.oldTscomment, context: &item.context);
620 } else {
621 cd.appendError(error: QString("PO-format parse error in line %1: '%2'"_L1)
622 .arg(a: l + 1)
623 .arg(a: toUnicode(lines[l])));
624 error = true;
625 }
626 break;
627 case '~':
628 if (line.startsWith(bv: "#~ msgid ")) {
629 item.msgId = slurpEscapedString(lines, l, offset: 9, prefix: "#~ ", cd);
630 } else if (line.startsWith(bv: "#~ msgid_plural ")) {
631 QByteArray extra = slurpEscapedString(lines, l, offset: 16, prefix: "#~ ", cd);
632 if (extra != item.msgId)
633 item.extra["po-msgid_plural"_L1] = toUnicode(extra);
634 item.isPlural = true;
635 } else if (line.startsWith(bv: "#~ msgctxt ")) {
636 item.tscomment = slurpEscapedString(lines, l, offset: 11, prefix: "#~ ", cd);
637 if (qtContexts)
638 splitContext(comment: &item.tscomment, context: &item.context);
639 } else if (line.startsWith(bv: "#~| msgid ")) {
640 item.oldMsgId = slurpEscapedString(lines, l, offset: 10, prefix: "#~| ", cd);
641 } else if (line.startsWith(bv: "#~| msgid_plural ")) {
642 QByteArray extra = slurpEscapedString(lines, l, offset: 17, prefix: "#~| ", cd);
643 if (extra != item.oldMsgId)
644 item.extra["po-old_msgid_plural"_L1] = toUnicode(extra);
645 } else if (line.startsWith(bv: "#~| msgctxt ")) {
646 item.oldTscomment = slurpEscapedString(lines, l, offset: 12, prefix: "#~| ", cd);
647 if (qtContexts)
648 splitContext(comment: &item.oldTscomment, context: &item.context);
649 } else {
650 cd.appendError(error: QString("PO-format parse error in line %1: '%2'"_L1)
651 .arg(a: l + 1)
652 .arg(a: toUnicode(lines[l])));
653 error = true;
654 }
655 break;
656 default:
657 cd.appendError(error: QString("PO-format parse error in line %1: '%2'"_L1)
658 .arg(a: l + 1)
659 .arg(a: toUnicode(lines[l])));
660 error = true;
661 break;
662 }
663 lastCmtLine = l;
664 } else if (line.startsWith(bv: "msgctxt ")) {
665 item.tscomment = slurpEscapedString(lines, l, offset: 8, prefix: QByteArray(), cd);
666 if (qtContexts)
667 splitContext(comment: &item.tscomment, context: &item.context);
668 } else if (line.startsWith(bv: "msgid ")) {
669 item.msgId = slurpEscapedString(lines, l, offset: 6, prefix: QByteArray(), cd);
670 } else if (line.startsWith(bv: "msgid_plural ")) {
671 QByteArray extra = slurpEscapedString(lines, l, offset: 13, prefix: 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(error: QString("PO-format error in line %1: '%2'"_L1)
677 .arg(a: l + 1)
678 .arg(a: 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(str: qName))
690 hdrOrder << qName;
691 headers[makePoHeader(str: qName)] = value;
692}
693
694static QString escapeComment(const QString &in, bool escape)
695{
696 QString out = in;
697 if (escape) {
698 out.replace(c: u'~', after: "~~"_L1);
699 out.replace(c: u'|', after: "~|"_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(ba: "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(ba: "po-headers"_L1).split(sep: u',', behavior: Qt::SkipEmptyParts);
724 // Keep in sync with loadPO
725 addPoHeader(headers, hdrOrder, name: "MIME-Version", value: "1.0"_L1);
726 addPoHeader(headers, hdrOrder, name: "Content-Type", value: "text/plain; charset=UTF-8"_L1);
727 addPoHeader(headers, hdrOrder, name: "Content-Transfer-Encoding", value: "8bit"_L1);
728 if (!translator.languageCode().isEmpty()) {
729 QLocale::Language l;
730 QLocale::Territory c;
731 Translator::languageAndTerritory(languageCode: translator.languageCode(), langPtr: &l, territoryPtr: &c);
732 const char *gettextRules;
733 if (getNumerusInfo(language: l, territory: c, rules: 0, forms: 0, gettextRules: &gettextRules))
734 addPoHeader(headers, hdrOrder, name: "Plural-Forms", value: QLatin1String(gettextRules));
735 addPoHeader(headers, hdrOrder, name: "X-Language", value: translator.languageCode());
736 }
737 if (!translator.sourceLanguageCode().isEmpty())
738 addPoHeader(headers, hdrOrder, name: "X-Source-Language", value: translator.sourceLanguageCode());
739 if (qtContexts)
740 addPoHeader(headers, hdrOrder, name: "X-Qt-Contexts", value: "true"_L1);
741 QString hdrStr;
742 for (const QString &hdr : std::as_const(t&: hdrOrder)) {
743 hdrStr += hdr;
744 hdrStr += ": "_L1;
745 hdrStr += headers.value(key: makePoHeader(str: hdr));
746 hdrStr += u'\n';
747 }
748 out << poEscapedString(prefix: QString(), keyword: QString::fromLatin1(ba: "msgstr"), noWrap: true, ba: hdrStr);
749
750 for (const TranslatorMessage &msg : translator.messages()) {
751 out << Qt::endl;
752
753 if (!msg.translatorComment().isEmpty())
754 out << poEscapedLines(prefix: "#"_L1, addSpace: true, in0: msg.translatorComment());
755
756 if (!msg.extraComment().isEmpty())
757 out << poEscapedLines(prefix: "#."_L1, addSpace: true, in0: msg.extraComment());
758
759 if (!msg.id().isEmpty())
760 out << "#. ts-id "_L1 << msg.id() << '\n';
761
762 QString xrefs = msg.extra(ba: "po-references"_L1);
763 if (!msg.fileName().isEmpty() || !xrefs.isEmpty()) {
764 QStringList refs;
765 for (const TranslatorMessage::Reference &ref : msg.allReferences())
766 refs.append(t: QString("%2:%1"_L1).arg(a: ref.lineNumber()).arg(a: ref.fileName()));
767 if (!xrefs.isEmpty())
768 refs << xrefs;
769 out << poWrappedEscapedLines(prefix: "#:"_L1, addSpace: true, line: refs.join(sep: 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(t: "fuzzy"_L1);
778 const auto itr = msg.extras().constFind(key: "po-flags"_L1);
779 if (itr != msg.extras().cend()) {
780 const QStringList atoms = itr->split(sep: ", "_L1);
781 for (const QString &atom : atoms)
782 if (atom.endsWith(s: str_format)) {
783 skipFormat = true;
784 break;
785 }
786 if (atoms.contains(str: "no-wrap"_L1))
787 noWrap = true;
788 flags.append(t: *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(ch: u'%', from: off)) >= 0;) {
795 if (++off >= source.size())
796 break;
797 if (source.at(i: off) == u'n' || source.at(i: off).isDigit()) {
798 flags.append(t: "qt-format"_L1);
799 break;
800 }
801 }
802 }
803 if (!flags.isEmpty())
804 out << "#, " << flags.join(sep: ", "_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, keyword: "msgctxt"_L1, noWrap,
811 ba: escapeComment(in: msg.oldComment(), escape: qtContexts));
812 if (!msg.oldSourceText().isEmpty())
813 out << poEscapedString(prefix, keyword: "msgid"_L1, noWrap, ba: msg.oldSourceText());
814 QString plural = msg.extra(ba: "po-old_msgid_plural"_L1);
815 if (!plural.isEmpty())
816 out << poEscapedString(prefix, keyword: "msgid_plural"_L1, noWrap, ba: plural);
817 prefix = QLatin1String(isObsolete ? "#~ " : "");
818 if (!msg.context().isEmpty())
819 out << poEscapedString(prefix, keyword: "msgctxt"_L1, noWrap,
820 ba: escapeComment(in: msg.context(), escape: true) + u'|'
821 + escapeComment(in: msg.comment(), escape: true));
822 else if (!msg.comment().isEmpty())
823 out << poEscapedString(prefix, keyword: "msgctxt"_L1, noWrap,
824 ba: escapeComment(in: msg.comment(), escape: qtContexts));
825 out << poEscapedString(prefix, keyword: "msgid"_L1, noWrap, ba: msg.sourceText());
826 if (!msg.isPlural()) {
827 QString transl = msg.translation();
828 transl.replace(before: Translator::BinaryVariantSeparator, after: Translator::TextVariantSeparator);
829 out << poEscapedString(prefix, keyword: "msgstr"_L1, noWrap, ba: transl);
830 } else {
831 QString plural = msg.extra(ba: "po-msgid_plural"_L1);
832 if (plural.isEmpty())
833 plural = msg.sourceText();
834 out << poEscapedString(prefix, keyword: "msgid_plural"_L1, noWrap, ba: 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(before: QChar(Translator::BinaryVariantSeparator),
839 after: QChar(Translator::TextVariantSeparator));
840 out << poEscapedString(prefix, keyword: QString::fromLatin1(ba: "msgstr[%1]").arg(a: i), noWrap,
841 ba: str);
842 }
843 }
844 }
845 return ok;
846}
847
848static bool savePOT(const Translator &translator, QIODevice &dev, ConversionData &cd)
849{
850 Translator ttor = translator;
851 ttor.dropTranslations();
852 return savePO(translator: ttor, dev, cd);
853}
854
855int initPO()
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;
862 format.fileType = Translator::FileFormat::TranslationSource;
863 format.priority = 1;
864 Translator::registerFileFormat(format);
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;
869 format.fileType = Translator::FileFormat::TranslationSource;
870 format.priority = -1;
871 Translator::registerFileFormat(format);
872 return 1;
873}
874
875Q_CONSTRUCTOR_FUNCTION(initPO)
876
877QT_END_NAMESPACE
878

source code of qttools/src/linguist/shared/po.cpp