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

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