1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the Qt Linguist of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 as published by the Free Software
20** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21** included in the packaging of this file. Please review the following
22** information to ensure the GNU General Public License requirements will
23** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24**
25** $QT_END_LICENSE$
26**
27****************************************************************************/
28
29#include "translator.h"
30
31#include <QtCore/QDebug>
32#include <QtCore/QIODevice>
33#include <QtCore/QHash>
34#include <QtCore/QRegExp>
35#include <QtCore/QString>
36#include <QtCore/QTextCodec>
37#include <QtCore/QTextStream>
38
39#include <ctype.h>
40
41// Uncomment if you wish to hard wrap long lines in .po files. Note that this
42// affects only msg strings, not comments.
43//#define HARD_WRAP_LONG_WORDS
44
45QT_BEGIN_NAMESPACE
46
47static const int MAX_LEN = 79;
48
49static QString poEscapedString(const QString &prefix, const QString &keyword,
50 bool noWrap, const QString &ba)
51{
52 QStringList lines;
53 int off = 0;
54 QString res;
55 while (off < ba.length()) {
56 ushort c = ba[off++].unicode();
57 switch (c) {
58 case '\n':
59 res += QLatin1String("\\n");
60 lines.append(t: res);
61 res.clear();
62 break;
63 case '\r':
64 res += QLatin1String("\\r");
65 break;
66 case '\t':
67 res += QLatin1String("\\t");
68 break;
69 case '\v':
70 res += QLatin1String("\\v");
71 break;
72 case '\a':
73 res += QLatin1String("\\a");
74 break;
75 case '\b':
76 res += QLatin1String("\\b");
77 break;
78 case '\f':
79 res += QLatin1String("\\f");
80 break;
81 case '"':
82 res += QLatin1String("\\\"");
83 break;
84 case '\\':
85 res += QLatin1String("\\\\");
86 break;
87 default:
88 if (c < 32) {
89 res += QLatin1String("\\x");
90 res += QString::number(c, base: 16);
91 if (off < ba.length() && isxdigit(ba[off].unicode()))
92 res += QLatin1String("\"\"");
93 } else {
94 res += QChar(c);
95 }
96 break;
97 }
98 }
99 if (!res.isEmpty())
100 lines.append(t: res);
101 if (!lines.isEmpty()) {
102 if (!noWrap) {
103 if (lines.count() != 1 ||
104 lines.first().length() > MAX_LEN - keyword.length() - prefix.length() - 3)
105 {
106 QStringList olines = lines;
107 lines = QStringList(QString());
108 const int maxlen = MAX_LEN - prefix.length() - 2;
109 foreach (const QString &line, olines) {
110 int off = 0;
111 while (off + maxlen < line.length()) {
112 int idx = line.lastIndexOf(c: QLatin1Char(' '), from: off + maxlen - 1) + 1;
113 if (idx == off) {
114#ifdef HARD_WRAP_LONG_WORDS
115 // This doesn't seem too nice, but who knows ...
116 idx = off + maxlen;
117#else
118 idx = line.indexOf(c: QLatin1Char(' '), from: off + maxlen) + 1;
119 if (!idx)
120 break;
121#endif
122 }
123 lines.append(t: line.mid(position: off, n: idx - off));
124 off = idx;
125 }
126 lines.append(t: line.mid(position: off));
127 }
128 }
129 } else if (lines.count() > 1) {
130 lines.prepend(t: QString());
131 }
132 }
133 return prefix + keyword + QLatin1String(" \"") +
134 lines.join(sep: QLatin1String("\"\n") + prefix + QLatin1Char('"')) +
135 QLatin1String("\"\n");
136}
137
138static QString poEscapedLines(const QString &prefix, bool addSpace, const QStringList &lines)
139{
140 QString out;
141 foreach (const QString &line, lines) {
142 out += prefix;
143 if (addSpace && !line.isEmpty())
144 out += QLatin1Char(' ' );
145 out += line;
146 out += QLatin1Char('\n');
147 }
148 return out;
149}
150
151static QString poEscapedLines(const QString &prefix, bool addSpace, const QString &in0)
152{
153 QString in = in0;
154 if (in == QString::fromLatin1(str: "\n"))
155 in.chop(n: 1);
156 return poEscapedLines(prefix, addSpace, lines: in.split(sep: QLatin1Char('\n')));
157}
158
159static QString poWrappedEscapedLines(const QString &prefix, bool addSpace, const QString &line)
160{
161 const int maxlen = MAX_LEN - prefix.length() - addSpace;
162 QStringList lines;
163 int off = 0;
164 while (off + maxlen < line.length()) {
165 int idx = line.lastIndexOf(c: QLatin1Char(' '), from: off + maxlen - 1);
166 if (idx < off) {
167#if 0 //def HARD_WRAP_LONG_WORDS
168 // This cannot work without messing up semantics, so do not even try.
169#else
170 idx = line.indexOf(c: QLatin1Char(' '), from: off + maxlen);
171 if (idx < 0)
172 break;
173#endif
174 }
175 lines.append(t: line.mid(position: off, n: idx - off));
176 off = idx + 1;
177 }
178 lines.append(t: line.mid(position: off));
179 return poEscapedLines(prefix, addSpace, lines);
180}
181
182struct PoItem
183{
184public:
185 PoItem()
186 : isPlural(false), isFuzzy(false)
187 {}
188
189
190public:
191 QByteArray id;
192 QByteArray context;
193 QByteArray tscomment;
194 QByteArray oldTscomment;
195 QByteArray lineNumber;
196 QByteArray fileName;
197 QByteArray references;
198 QByteArray translatorComments;
199 QByteArray automaticComments;
200 QByteArray msgId;
201 QByteArray oldMsgId;
202 QList<QByteArray> msgStr;
203 bool isPlural;
204 bool isFuzzy;
205 QHash<QString, QString> extra;
206};
207
208
209static bool isTranslationLine(const QByteArray &line)
210{
211 return line.startsWith(c: "#~ msgstr") || line.startsWith(c: "msgstr");
212}
213
214static QByteArray slurpEscapedString(const QList<QByteArray> &lines, int &l,
215 int offset, const QByteArray &prefix, ConversionData &cd)
216{
217 QByteArray msg;
218 int stoff;
219
220 for (; l < lines.size(); ++l) {
221 const QByteArray &line = lines.at(i: l);
222 if (line.isEmpty() || !line.startsWith(a: prefix))
223 break;
224 while (isspace(line[offset])) // No length check, as string has no trailing spaces.
225 offset++;
226 if (line[offset] != '"')
227 break;
228 offset++;
229 forever {
230 if (offset == line.length())
231 goto premature_eol;
232 uchar c = line[offset++];
233 if (c == '"') {
234 if (offset == line.length())
235 break;
236 while (isspace(line[offset]))
237 offset++;
238 if (line[offset++] != '"') {
239 cd.appendError(error: QString::fromLatin1(
240 str: "PO parsing error: extra characters on line %1.")
241 .arg(a: l + 1));
242 break;
243 }
244 continue;
245 }
246 if (c == '\\') {
247 if (offset == line.length())
248 goto premature_eol;
249 c = line[offset++];
250 switch (c) {
251 case 'r':
252 msg += '\r'; // Maybe just throw it away?
253 break;
254 case 'n':
255 msg += '\n';
256 break;
257 case 't':
258 msg += '\t';
259 break;
260 case 'v':
261 msg += '\v';
262 break;
263 case 'a':
264 msg += '\a';
265 break;
266 case 'b':
267 msg += '\b';
268 break;
269 case 'f':
270 msg += '\f';
271 break;
272 case '"':
273 msg += '"';
274 break;
275 case '\\':
276 msg += '\\';
277 break;
278 case '0':
279 case '1':
280 case '2':
281 case '3':
282 case '4':
283 case '5':
284 case '6':
285 case '7':
286 stoff = offset - 1;
287 while ((c = line[offset]) >= '0' && c <= '7')
288 if (++offset == line.length())
289 goto premature_eol;
290 msg += line.mid(index: stoff, len: offset - stoff).toUInt(ok: 0, base: 8);
291 break;
292 case 'x':
293 stoff = offset;
294 while (isxdigit(line[offset]))
295 if (++offset == line.length())
296 goto premature_eol;
297 msg += line.mid(index: stoff, len: offset - stoff).toUInt(ok: 0, base: 16);
298 break;
299 default:
300 cd.appendError(error: QString::fromLatin1(
301 str: "PO parsing error: invalid escape '\\%1' (line %2).")
302 .arg(a: QChar((uint)c)).arg(a: l + 1));
303 msg += '\\';
304 msg += c;
305 break;
306 }
307 } else {
308 msg += c;
309 }
310 }
311 offset = prefix.size();
312 }
313 --l;
314 return msg;
315
316premature_eol:
317 cd.appendError(error: QString::fromLatin1(
318 str: "PO parsing error: premature end of line %1.").arg(a: l + 1));
319 return QByteArray();
320}
321
322static void slurpComment(QByteArray &msg, const QList<QByteArray> &lines, int & l)
323{
324 int firstLine = l;
325 QByteArray prefix = lines.at(i: l);
326 for (int i = 1; ; i++) {
327 if (prefix.at(i) != ' ') {
328 prefix.truncate(pos: i);
329 break;
330 }
331 }
332 for (; l < lines.size(); ++l) {
333 const QByteArray &line = lines.at(i: l);
334 if (line.startsWith(a: prefix)) {
335 if (l > firstLine)
336 msg += '\n';
337 msg += line.mid(index: prefix.size());
338 } else if (line == "#") {
339 msg += '\n';
340 } else {
341 break;
342 }
343 }
344 --l;
345}
346
347static void splitContext(QByteArray *comment, QByteArray *context)
348{
349 char *data = comment->data();
350 int len = comment->size();
351 int sep = -1, j = 0;
352
353 for (int i = 0; i < len; i++, j++) {
354 if (data[i] == '~' && i + 1 < len)
355 i++;
356 else if (data[i] == '|')
357 sep = j;
358 data[j] = data[i];
359 }
360 if (sep >= 0) {
361 QByteArray tmp = comment->mid(index: sep + 1, len: j - sep - 1);
362 comment->truncate(pos: sep);
363 *context = *comment;
364 *comment = tmp;
365 } else {
366 comment->truncate(pos: j);
367 }
368}
369
370static QString makePoHeader(const QString &str)
371{
372 return QLatin1String("po-header-") + str.toLower().replace(before: QLatin1Char('-'), after: QLatin1Char('_'));
373}
374
375static QByteArray QByteArrayList_join(const QList<QByteArray> &that, char sep)
376{
377 int totalLength = 0;
378 const int size = that.size();
379
380 for (int i = 0; i < size; ++i)
381 totalLength += that.at(i).size();
382
383 if (size > 0)
384 totalLength += size - 1;
385
386 QByteArray res;
387 if (totalLength == 0)
388 return res;
389 res.reserve(asize: totalLength);
390 for (int i = 0; i < that.size(); ++i) {
391 if (i)
392 res += sep;
393 res += that.at(i);
394 }
395 return res;
396}
397
398bool loadPO(Translator &translator, QIODevice &dev, ConversionData &cd)
399{
400 QTextCodec *codec = QTextCodec::codecForName(name: "UTF-8");
401 bool error = false;
402
403 // format of a .po file entry:
404 // white-space
405 // # translator-comments
406 // #. automatic-comments
407 // #: reference...
408 // #, flag...
409 // #~ msgctxt, msgid*, msgstr - used for obsoleted messages
410 // #| msgctxt, msgid* previous untranslated-string - for fuzzy message
411 // #~| msgctxt, msgid* previous untranslated-string - for fuzzy obsoleted messages
412 // msgctx string-context
413 // msgid untranslated-string
414 // -- For singular:
415 // msgstr translated-string
416 // -- For plural:
417 // msgid_plural untranslated-string-plural
418 // msgstr[0] translated-string
419 // ...
420
421 // we need line based lookahead below.
422 QList<QByteArray> lines;
423 while (!dev.atEnd())
424 lines.append(t: dev.readLine().trimmed());
425 lines.append(t: QByteArray());
426
427 int l = 0, lastCmtLine = -1;
428 bool qtContexts = false;
429 PoItem item;
430 for (; l != lines.size(); ++l) {
431 QByteArray line = lines.at(i: l);
432 if (line.isEmpty())
433 continue;
434 if (isTranslationLine(line)) {
435 bool isObsolete = line.startsWith(c: "#~ msgstr");
436 const QByteArray prefix = isObsolete ? "#~ " : "";
437 while (true) {
438 int idx = line.indexOf(c: ' ', from: prefix.length());
439 QByteArray str = slurpEscapedString(lines, l, offset: idx, prefix, cd);
440 item.msgStr.append(t: str);
441 if (l + 1 >= lines.size() || !isTranslationLine(line: lines.at(i: l + 1)))
442 break;
443 ++l;
444 line = lines.at(i: l);
445 }
446 if (item.msgId.isEmpty()) {
447 QHash<QString, QByteArray> extras;
448 QList<QByteArray> hdrOrder;
449 QByteArray pluralForms;
450 foreach (const QByteArray &hdr, item.msgStr.first().split('\n')) {
451 if (hdr.isEmpty())
452 continue;
453 int idx = hdr.indexOf(c: ':');
454 if (idx < 0) {
455 cd.appendError(error: QString::fromLatin1(str: "Unexpected PO header format '%1'")
456 .arg(a: QString::fromLatin1(str: hdr)));
457 error = true;
458 break;
459 }
460 QByteArray hdrName = hdr.left(len: idx).trimmed();
461 QByteArray hdrValue = hdr.mid(index: idx + 1).trimmed();
462 hdrOrder << hdrName;
463 if (hdrName == "X-Language") {
464 translator.setLanguageCode(QString::fromLatin1(str: hdrValue));
465 } else if (hdrName == "X-Source-Language") {
466 translator.setSourceLanguageCode(QString::fromLatin1(str: hdrValue));
467 } else if (hdrName == "X-Qt-Contexts") {
468 qtContexts = (hdrValue == "true");
469 } else if (hdrName == "Plural-Forms") {
470 pluralForms = hdrValue;
471 } else if (hdrName == "MIME-Version") {
472 // just assume it is 1.0
473 } else if (hdrName == "Content-Type") {
474 if (!hdrValue.startsWith(c: "text/plain; charset=")) {
475 cd.appendError(error: QString::fromLatin1(str: "Unexpected Content-Type header '%1'")
476 .arg(a: QString::fromLatin1(str: hdrValue)));
477 error = true;
478 // This will avoid a flood of conversion errors.
479 codec = QTextCodec::codecForName(name: "latin1");
480 } else {
481 QByteArray cod = hdrValue.mid(index: 20);
482 QTextCodec *cdc = QTextCodec::codecForName(name: cod);
483 if (!cdc) {
484 cd.appendError(error: QString::fromLatin1(str: "Unsupported codec '%1'")
485 .arg(a: QString::fromLatin1(str: cod)));
486 error = true;
487 // This will avoid a flood of conversion errors.
488 codec = QTextCodec::codecForName(name: "latin1");
489 } else {
490 codec = cdc;
491 }
492 }
493 } else if (hdrName == "Content-Transfer-Encoding") {
494 if (hdrValue != "8bit") {
495 cd.appendError(error: QString::fromLatin1(str: "Unexpected Content-Transfer-Encoding '%1'")
496 .arg(a: QString::fromLatin1(str: hdrValue)));
497 return false;
498 }
499 } else if (hdrName == "X-Virgin-Header") {
500 // legacy
501 } else {
502 extras[makePoHeader(str: QString::fromLatin1(str: hdrName))] = hdrValue;
503 }
504 }
505 if (!pluralForms.isEmpty()) {
506 if (translator.languageCode().isEmpty()) {
507 extras[makePoHeader(str: QLatin1String("Plural-Forms"))] = pluralForms;
508 } else {
509 // FIXME: have fun with making a consistency check ...
510 }
511 }
512 // Eliminate the field if only headers we added are present in standard order.
513 // Keep in sync with savePO
514 static const char * const dfltHdrs[] = {
515 "MIME-Version", "Content-Type", "Content-Transfer-Encoding",
516 "Plural-Forms", "X-Language", "X-Source-Language", "X-Qt-Contexts"
517 };
518 uint cdh = 0;
519 for (int cho = 0; cho < hdrOrder.length(); cho++) {
520 for (;; cdh++) {
521 if (cdh == sizeof(dfltHdrs)/sizeof(dfltHdrs[0])) {
522 extras[QLatin1String("po-headers")] =
523 QByteArrayList_join(that: hdrOrder, sep: ',');
524 goto doneho;
525 }
526 if (hdrOrder.at(i: cho) == dfltHdrs[cdh]) {
527 cdh++;
528 break;
529 }
530 }
531 }
532 doneho:
533 if (lastCmtLine != -1) {
534 extras[QLatin1String("po-header_comment")] =
535 QByteArrayList_join(that: lines.mid(pos: 0, alength: lastCmtLine + 1), sep: '\n');
536 }
537 for (QHash<QString, QByteArray>::ConstIterator it = extras.constBegin(),
538 end = extras.constEnd();
539 it != end; ++it)
540 translator.setExtra(ba: it.key(), var: codec->toUnicode(it.value()));
541 item = PoItem();
542 continue;
543 }
544 // build translator message
545 TranslatorMessage msg;
546 msg.setContext(codec->toUnicode(item.context));
547 if (!item.references.isEmpty()) {
548 QString xrefs;
549 foreach (const QString &ref,
550 codec->toUnicode(item.references).split(
551 QRegExp(QLatin1String("\\s")), Qt::SkipEmptyParts)) {
552 int pos = ref.indexOf(c: QLatin1Char(':'));
553 int lpos = ref.lastIndexOf(c: QLatin1Char(':'));
554 if (pos != -1 && pos == lpos) {
555 bool ok;
556 int lno = ref.mid(position: pos + 1).toInt(ok: &ok);
557 if (ok) {
558 msg.addReference(fileName: ref.left(n: pos), lineNumber: lno);
559 continue;
560 }
561 }
562 if (!xrefs.isEmpty())
563 xrefs += QLatin1Char(' ');
564 xrefs += ref;
565 }
566 if (!xrefs.isEmpty())
567 item.extra[QLatin1String("po-references")] = xrefs;
568 }
569 msg.setId(codec->toUnicode(item.id));
570 msg.setSourceText(codec->toUnicode(item.msgId));
571 msg.setOldSourceText(codec->toUnicode(item.oldMsgId));
572 msg.setComment(codec->toUnicode(item.tscomment));
573 msg.setOldComment(codec->toUnicode(item.oldTscomment));
574 msg.setExtraComment(codec->toUnicode(item.automaticComments));
575 msg.setTranslatorComment(codec->toUnicode(item.translatorComments));
576 msg.setPlural(item.isPlural || item.msgStr.size() > 1);
577 QStringList translations;
578 foreach (const QByteArray &bstr, item.msgStr) {
579 QString str = codec->toUnicode(bstr);
580 str.replace(before: QChar(Translator::TextVariantSeparator),
581 after: QChar(Translator::BinaryVariantSeparator));
582 translations << str;
583 }
584 msg.setTranslations(translations);
585 bool isFuzzy = item.isFuzzy || (!msg.sourceText().isEmpty() && !msg.isTranslated());
586 if (isObsolete && isFuzzy)
587 msg.setType(TranslatorMessage::Obsolete);
588 else if (isObsolete)
589 msg.setType(TranslatorMessage::Vanished);
590 else if (isFuzzy)
591 msg.setType(TranslatorMessage::Unfinished);
592 else
593 msg.setType(TranslatorMessage::Finished);
594 msg.setExtras(item.extra);
595
596 //qDebug() << "WRITE: " << context;
597 //qDebug() << "SOURCE: " << msg.sourceText();
598 //qDebug() << flags << msg.m_extra;
599 translator.append(msg);
600 item = PoItem();
601 } else if (line.startsWith(c: '#')) {
602 switch (line.size() < 2 ? 0 : line.at(i: 1)) {
603 case ':':
604 item.references += line.mid(index: 3);
605 item.references += '\n';
606 break;
607 case ',': {
608 QStringList flags =
609 QString::fromLatin1(str: line.mid(index: 2)).split(
610 sep: QRegExp(QLatin1String("[, ]")), behavior: Qt::SkipEmptyParts);
611 if (flags.removeOne(t: QLatin1String("fuzzy")))
612 item.isFuzzy = true;
613 flags.removeOne(t: QLatin1String("qt-format"));
614 TranslatorMessage::ExtraData::const_iterator it =
615 item.extra.find(akey: QLatin1String("po-flags"));
616 if (it != item.extra.end())
617 flags.prepend(t: *it);
618 if (!flags.isEmpty())
619 item.extra[QLatin1String("po-flags")] = flags.join(sep: QLatin1String(", "));
620 break;
621 }
622 case 0:
623 item.translatorComments += '\n';
624 break;
625 case ' ':
626 slurpComment(msg&: item.translatorComments, lines, l);
627 break;
628 case '.':
629 if (line.startsWith(c: "#. ts-context ")) { // legacy
630 item.context = line.mid(index: 14);
631 } else if (line.startsWith(c: "#. ts-id ")) {
632 item.id = line.mid(index: 9);
633 } else {
634 item.automaticComments += line.mid(index: 3);
635
636 }
637 break;
638 case '|':
639 if (line.startsWith(c: "#| msgid ")) {
640 item.oldMsgId = slurpEscapedString(lines, l, offset: 9, prefix: "#| ", cd);
641 } else if (line.startsWith(c: "#| msgid_plural ")) {
642 QByteArray extra = slurpEscapedString(lines, l, offset: 16, prefix: "#| ", cd);
643 if (extra != item.oldMsgId)
644 item.extra[QLatin1String("po-old_msgid_plural")] =
645 codec->toUnicode(extra);
646 } else if (line.startsWith(c: "#| msgctxt ")) {
647 item.oldTscomment = slurpEscapedString(lines, l, offset: 11, prefix: "#| ", cd);
648 if (qtContexts)
649 splitContext(comment: &item.oldTscomment, context: &item.context);
650 } else {
651 cd.appendError(error: QString(QLatin1String("PO-format parse error in line %1: '%2'"))
652 .arg(a: l + 1).arg(a: codec->toUnicode(lines[l])));
653 error = true;
654 }
655 break;
656 case '~':
657 if (line.startsWith(c: "#~ msgid ")) {
658 item.msgId = slurpEscapedString(lines, l, offset: 9, prefix: "#~ ", cd);
659 } else if (line.startsWith(c: "#~ msgid_plural ")) {
660 QByteArray extra = slurpEscapedString(lines, l, offset: 16, prefix: "#~ ", cd);
661 if (extra != item.msgId)
662 item.extra[QLatin1String("po-msgid_plural")] =
663 codec->toUnicode(extra);
664 item.isPlural = true;
665 } else if (line.startsWith(c: "#~ msgctxt ")) {
666 item.tscomment = slurpEscapedString(lines, l, offset: 11, prefix: "#~ ", cd);
667 if (qtContexts)
668 splitContext(comment: &item.tscomment, context: &item.context);
669 } else if (line.startsWith(c: "#~| msgid ")) {
670 item.oldMsgId = slurpEscapedString(lines, l, offset: 10, prefix: "#~| ", cd);
671 } else if (line.startsWith(c: "#~| msgid_plural ")) {
672 QByteArray extra = slurpEscapedString(lines, l, offset: 17, prefix: "#~| ", cd);
673 if (extra != item.oldMsgId)
674 item.extra[QLatin1String("po-old_msgid_plural")] =
675 codec->toUnicode(extra);
676 } else if (line.startsWith(c: "#~| msgctxt ")) {
677 item.oldTscomment = slurpEscapedString(lines, l, offset: 12, prefix: "#~| ", cd);
678 if (qtContexts)
679 splitContext(comment: &item.oldTscomment, context: &item.context);
680 } else {
681 cd.appendError(error: QString(QLatin1String("PO-format parse error in line %1: '%2'"))
682 .arg(a: l + 1).arg(a: codec->toUnicode(lines[l])));
683 error = true;
684 }
685 break;
686 default:
687 cd.appendError(error: QString(QLatin1String("PO-format parse error in line %1: '%2'"))
688 .arg(a: l + 1).arg(a: codec->toUnicode(lines[l])));
689 error = true;
690 break;
691 }
692 lastCmtLine = l;
693 } else if (line.startsWith(c: "msgctxt ")) {
694 item.tscomment = slurpEscapedString(lines, l, offset: 8, prefix: QByteArray(), cd);
695 if (qtContexts)
696 splitContext(comment: &item.tscomment, context: &item.context);
697 } else if (line.startsWith(c: "msgid ")) {
698 item.msgId = slurpEscapedString(lines, l, offset: 6, prefix: QByteArray(), cd);
699 } else if (line.startsWith(c: "msgid_plural ")) {
700 QByteArray extra = slurpEscapedString(lines, l, offset: 13, prefix: QByteArray(), cd);
701 if (extra != item.msgId)
702 item.extra[QLatin1String("po-msgid_plural")] = codec->toUnicode(extra);
703 item.isPlural = true;
704 } else {
705 cd.appendError(error: QString(QLatin1String("PO-format error in line %1: '%2'"))
706 .arg(a: l + 1).arg(a: codec->toUnicode(lines[l])));
707 error = true;
708 }
709 }
710 return !error && cd.errors().isEmpty();
711}
712
713static void addPoHeader(Translator::ExtraData &headers, QStringList &hdrOrder,
714 const char *name, const QString &value)
715{
716 QString qName = QLatin1String(name);
717 if (!hdrOrder.contains(str: qName))
718 hdrOrder << qName;
719 headers[makePoHeader(str: qName)] = value;
720}
721
722static QString escapeComment(const QString &in, bool escape)
723{
724 QString out = in;
725 if (escape) {
726 out.replace(c: QLatin1Char('~'), after: QLatin1String("~~"));
727 out.replace(c: QLatin1Char('|'), after: QLatin1String("~|"));
728 }
729 return out;
730}
731
732bool savePO(const Translator &translator, QIODevice &dev, ConversionData &)
733{
734 QString str_format = QLatin1String("-format");
735
736 bool ok = true;
737 QTextStream out(&dev);
738 out.setCodec("UTF-8");
739
740 bool qtContexts = false;
741 foreach (const TranslatorMessage &msg, translator.messages())
742 if (!msg.context().isEmpty()) {
743 qtContexts = true;
744 break;
745 }
746
747 QString cmt = translator.extra(ba: QLatin1String("po-header_comment"));
748 if (!cmt.isEmpty())
749 out << cmt << '\n';
750 out << "msgid \"\"\n";
751 Translator::ExtraData headers = translator.extras();
752 QStringList hdrOrder = translator.extra(ba: QLatin1String("po-headers")).split(
753 sep: QLatin1Char(','), behavior: Qt::SkipEmptyParts);
754 // Keep in sync with loadPO
755 addPoHeader(headers, hdrOrder, name: "MIME-Version", value: QLatin1String("1.0"));
756 addPoHeader(headers, hdrOrder, name: "Content-Type",
757 value: QLatin1String("text/plain; charset=" + out.codec()->name()));
758 addPoHeader(headers, hdrOrder, name: "Content-Transfer-Encoding", value: QLatin1String("8bit"));
759 if (!translator.languageCode().isEmpty()) {
760 QLocale::Language l;
761 QLocale::Country c;
762 Translator::languageAndCountry(languageCode: translator.languageCode(), lang: &l, country: &c);
763 const char *gettextRules;
764 if (getNumerusInfo(language: l, country: c, rules: 0, forms: 0, gettextRules: &gettextRules))
765 addPoHeader(headers, hdrOrder, name: "Plural-Forms", value: QLatin1String(gettextRules));
766 addPoHeader(headers, hdrOrder, name: "X-Language", value: translator.languageCode());
767 }
768 if (!translator.sourceLanguageCode().isEmpty())
769 addPoHeader(headers, hdrOrder, name: "X-Source-Language", value: translator.sourceLanguageCode());
770 if (qtContexts)
771 addPoHeader(headers, hdrOrder, name: "X-Qt-Contexts", value: QLatin1String("true"));
772 QString hdrStr;
773 foreach (const QString &hdr, hdrOrder) {
774 hdrStr += hdr;
775 hdrStr += QLatin1String(": ");
776 hdrStr += headers.value(akey: makePoHeader(str: hdr));
777 hdrStr += QLatin1Char('\n');
778 }
779 out << poEscapedString(prefix: QString(), keyword: QString::fromLatin1(str: "msgstr"), noWrap: true, ba: hdrStr);
780
781 foreach (const TranslatorMessage &msg, translator.messages()) {
782 out << Qt::endl;
783
784 if (!msg.translatorComment().isEmpty())
785 out << poEscapedLines(prefix: QLatin1String("#"), addSpace: true, in0: msg.translatorComment());
786
787 if (!msg.extraComment().isEmpty())
788 out << poEscapedLines(prefix: QLatin1String("#."), addSpace: true, in0: msg.extraComment());
789
790 if (!msg.id().isEmpty())
791 out << QLatin1String("#. ts-id ") << msg.id() << '\n';
792
793 QString xrefs = msg.extra(ba: QLatin1String("po-references"));
794 if (!msg.fileName().isEmpty() || !xrefs.isEmpty()) {
795 QStringList refs;
796 foreach (const TranslatorMessage::Reference &ref, msg.allReferences())
797 refs.append(t: QString(QLatin1String("%2:%1"))
798 .arg(a: ref.lineNumber()).arg(a: ref.fileName()));
799 if (!xrefs.isEmpty())
800 refs << xrefs;
801 out << poWrappedEscapedLines(prefix: QLatin1String("#:"), addSpace: true, line: refs.join(sep: QLatin1Char(' ')));
802 }
803
804 bool noWrap = false;
805 bool skipFormat = false;
806 QStringList flags;
807 if ((msg.type() == TranslatorMessage::Unfinished
808 || msg.type() == TranslatorMessage::Obsolete) && msg.isTranslated())
809 flags.append(t: QLatin1String("fuzzy"));
810 TranslatorMessage::ExtraData::const_iterator itr =
811 msg.extras().find(akey: QLatin1String("po-flags"));
812 if (itr != msg.extras().end()) {
813 QStringList atoms = itr->split(sep: QLatin1String(", "));
814 foreach (const QString &atom, atoms)
815 if (atom.endsWith(s: str_format)) {
816 skipFormat = true;
817 break;
818 }
819 if (atoms.contains(str: QLatin1String("no-wrap")))
820 noWrap = true;
821 flags.append(t: *itr);
822 }
823 if (!skipFormat) {
824 QString source = msg.sourceText();
825 // This is fuzzy logic, as we don't know whether the string is
826 // actually used with QString::arg().
827 for (int off = 0; (off = source.indexOf(c: QLatin1Char('%'), from: off)) >= 0; ) {
828 if (++off >= source.length())
829 break;
830 if (source.at(i: off) == QLatin1Char('n') || source.at(i: off).isDigit()) {
831 flags.append(t: QLatin1String("qt-format"));
832 break;
833 }
834 }
835 }
836 if (!flags.isEmpty())
837 out << "#, " << flags.join(sep: QLatin1String(", ")) << '\n';
838
839 bool isObsolete = (msg.type() == TranslatorMessage::Obsolete
840 || msg.type() == TranslatorMessage::Vanished);
841 QString prefix = QLatin1String(isObsolete ? "#~| " : "#| ");
842 if (!msg.oldComment().isEmpty())
843 out << poEscapedString(prefix, keyword: QLatin1String("msgctxt"), noWrap,
844 ba: escapeComment(in: msg.oldComment(), escape: qtContexts));
845 if (!msg.oldSourceText().isEmpty())
846 out << poEscapedString(prefix, keyword: QLatin1String("msgid"), noWrap, ba: msg.oldSourceText());
847 QString plural = msg.extra(ba: QLatin1String("po-old_msgid_plural"));
848 if (!plural.isEmpty())
849 out << poEscapedString(prefix, keyword: QLatin1String("msgid_plural"), noWrap, ba: plural);
850 prefix = QLatin1String(isObsolete ? "#~ " : "");
851 if (!msg.context().isEmpty())
852 out << poEscapedString(prefix, keyword: QLatin1String("msgctxt"), noWrap,
853 ba: escapeComment(in: msg.context(), escape: true) + QLatin1Char('|')
854 + escapeComment(in: msg.comment(), escape: true));
855 else if (!msg.comment().isEmpty())
856 out << poEscapedString(prefix, keyword: QLatin1String("msgctxt"), noWrap,
857 ba: escapeComment(in: msg.comment(), escape: qtContexts));
858 out << poEscapedString(prefix, keyword: QLatin1String("msgid"), noWrap, ba: msg.sourceText());
859 if (!msg.isPlural()) {
860 QString transl = msg.translation();
861 transl.replace(before: QChar(Translator::BinaryVariantSeparator),
862 after: QChar(Translator::TextVariantSeparator));
863 out << poEscapedString(prefix, keyword: QLatin1String("msgstr"), noWrap, ba: transl);
864 } else {
865 QString plural = msg.extra(ba: QLatin1String("po-msgid_plural"));
866 if (plural.isEmpty())
867 plural = msg.sourceText();
868 out << poEscapedString(prefix, keyword: QLatin1String("msgid_plural"), noWrap, ba: plural);
869 const QStringList &translations = msg.translations();
870 for (int i = 0; i != translations.size(); ++i) {
871 QString str = translations.at(i);
872 str.replace(before: QChar(Translator::BinaryVariantSeparator),
873 after: QChar(Translator::TextVariantSeparator));
874 out << poEscapedString(prefix, keyword: QString::fromLatin1(str: "msgstr[%1]").arg(a: i), noWrap,
875 ba: str);
876 }
877 }
878 }
879 return ok;
880}
881
882static bool savePOT(const Translator &translator, QIODevice &dev, ConversionData &cd)
883{
884 Translator ttor = translator;
885 ttor.dropTranslations();
886 return savePO(translator: ttor, dev, cd);
887}
888
889int initPO()
890{
891 Translator::FileFormat format;
892 format.extension = QLatin1String("po");
893 format.untranslatedDescription = QT_TRANSLATE_NOOP("FMT", "GNU Gettext localization files");
894 format.loader = &loadPO;
895 format.saver = &savePO;
896 format.fileType = Translator::FileFormat::TranslationSource;
897 format.priority = 1;
898 Translator::registerFileFormat(format);
899 format.extension = QLatin1String("pot");
900 format.untranslatedDescription = QT_TRANSLATE_NOOP("FMT", "GNU Gettext localization template files");
901 format.loader = &loadPO;
902 format.saver = &savePOT;
903 format.fileType = Translator::FileFormat::TranslationSource;
904 format.priority = -1;
905 Translator::registerFileFormat(format);
906 return 1;
907}
908
909Q_CONSTRUCTOR_FUNCTION(initPO)
910
911QT_END_NAMESPACE
912

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