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 
45 QT_BEGIN_NAMESPACE
46 
47 static const int MAX_LEN = 79;
48 
poEscapedString(const QString & prefix,const QString & keyword,bool noWrap,const QString & ba)49 static 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(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, 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(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(QLatin1Char(' '), 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(QLatin1Char(' '), off + maxlen) + 1;
119                             if (!idx)
120                                 break;
121 #endif
122                         }
123                         lines.append(line.mid(off, idx - off));
124                         off = idx;
125                     }
126                     lines.append(line.mid(off));
127                 }
128             }
129         } else if (lines.count() > 1) {
130             lines.prepend(QString());
131         }
132     }
133     return prefix + keyword + QLatin1String(" \"") +
134            lines.join(QLatin1String("\"\n") + prefix + QLatin1Char('"')) +
135            QLatin1String("\"\n");
136 }
137 
poEscapedLines(const QString & prefix,bool addSpace,const QStringList & lines)138 static 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 
poEscapedLines(const QString & prefix,bool addSpace,const QString & in0)151 static QString poEscapedLines(const QString &prefix, bool addSpace, const QString &in0)
152 {
153     QString in = in0;
154     if (in == QString::fromLatin1("\n"))
155         in.chop(1);
156     return poEscapedLines(prefix, addSpace, in.split(QLatin1Char('\n')));
157 }
158 
poWrappedEscapedLines(const QString & prefix,bool addSpace,const QString & line)159 static 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(QLatin1Char(' '), 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(QLatin1Char(' '), off + maxlen);
171             if (idx < 0)
172                 break;
173 #endif
174         }
175         lines.append(line.mid(off, idx - off));
176         off = idx + 1;
177     }
178     lines.append(line.mid(off));
179     return poEscapedLines(prefix, addSpace, lines);
180 }
181 
182 struct PoItem
183 {
184 public:
PoItemPoItem185     PoItem()
186       : isPlural(false), isFuzzy(false)
187     {}
188 
189 
190 public:
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 
isTranslationLine(const QByteArray & line)209 static bool isTranslationLine(const QByteArray &line)
210 {
211     return line.startsWith("#~ msgstr") || line.startsWith("msgstr");
212 }
213 
slurpEscapedString(const QList<QByteArray> & lines,int & l,int offset,const QByteArray & prefix,ConversionData & cd)214 static 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(l);
222         if (line.isEmpty() || !line.startsWith(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(QString::fromLatin1(
240                             "PO parsing error: extra characters on line %1.")
241                             .arg(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(stoff, offset - stoff).toUInt(0, 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(stoff, offset - stoff).toUInt(0, 16);
298                     break;
299                 default:
300                     cd.appendError(QString::fromLatin1(
301                             "PO parsing error: invalid escape '\\%1' (line %2).")
302                             .arg(QChar((uint)c)).arg(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 
316 premature_eol:
317     cd.appendError(QString::fromLatin1(
318             "PO parsing error: premature end of line %1.").arg(l + 1));
319     return QByteArray();
320 }
321 
slurpComment(QByteArray & msg,const QList<QByteArray> & lines,int & l)322 static void slurpComment(QByteArray &msg, const QList<QByteArray> &lines, int & l)
323 {
324     int firstLine = l;
325     QByteArray prefix = lines.at(l);
326     for (int i = 1; ; i++) {
327         if (prefix.at(i) != ' ') {
328             prefix.truncate(i);
329             break;
330         }
331     }
332     for (; l < lines.size(); ++l) {
333         const QByteArray &line = lines.at(l);
334         if (line.startsWith(prefix)) {
335             if (l > firstLine)
336                 msg += '\n';
337             msg += line.mid(prefix.size());
338         } else if (line == "#") {
339             msg += '\n';
340         } else {
341             break;
342         }
343     }
344     --l;
345 }
346 
splitContext(QByteArray * comment,QByteArray * context)347 static 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(sep + 1, j - sep - 1);
362         comment->truncate(sep);
363         *context = *comment;
364         *comment = tmp;
365     } else {
366         comment->truncate(j);
367     }
368 }
369 
makePoHeader(const QString & str)370 static QString makePoHeader(const QString &str)
371 {
372     return QLatin1String("po-header-") + str.toLower().replace(QLatin1Char('-'), QLatin1Char('_'));
373 }
374 
QByteArrayList_join(const QList<QByteArray> & that,char sep)375 static 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(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 
loadPO(Translator & translator,QIODevice & dev,ConversionData & cd)398 bool loadPO(Translator &translator, QIODevice &dev, ConversionData &cd)
399 {
400     QTextCodec *codec = QTextCodec::codecForName("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(dev.readLine().trimmed());
425     lines.append(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(l);
432         if (line.isEmpty())
433            continue;
434         if (isTranslationLine(line)) {
435             bool isObsolete = line.startsWith("#~ msgstr");
436             const QByteArray prefix = isObsolete ? "#~ " : "";
437             while (true) {
438                 int idx = line.indexOf(' ', prefix.length());
439                 QByteArray str = slurpEscapedString(lines, l, idx, prefix, cd);
440                 item.msgStr.append(str);
441                 if (l + 1 >= lines.size() || !isTranslationLine(lines.at(l + 1)))
442                     break;
443                 ++l;
444                 line = lines.at(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(':');
454                     if (idx < 0) {
455                         cd.appendError(QString::fromLatin1("Unexpected PO header format '%1'")
456                             .arg(QString::fromLatin1(hdr)));
457                         error = true;
458                         break;
459                     }
460                     QByteArray hdrName = hdr.left(idx).trimmed();
461                     QByteArray hdrValue = hdr.mid(idx + 1).trimmed();
462                     hdrOrder << hdrName;
463                     if (hdrName == "X-Language") {
464                         translator.setLanguageCode(QString::fromLatin1(hdrValue));
465                     } else if (hdrName == "X-Source-Language") {
466                         translator.setSourceLanguageCode(QString::fromLatin1(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("text/plain; charset=")) {
475                                 cd.appendError(QString::fromLatin1("Unexpected Content-Type header '%1'")
476                                     .arg(QString::fromLatin1(hdrValue)));
477                                 error = true;
478                                 // This will avoid a flood of conversion errors.
479                                 codec = QTextCodec::codecForName("latin1");
480                             } else {
481                                 QByteArray cod = hdrValue.mid(20);
482                                 QTextCodec *cdc = QTextCodec::codecForName(cod);
483                                 if (!cdc) {
484                                     cd.appendError(QString::fromLatin1("Unsupported codec '%1'")
485                                             .arg(QString::fromLatin1(cod)));
486                                     error = true;
487                                     // This will avoid a flood of conversion errors.
488                                     codec = QTextCodec::codecForName("latin1");
489                                 } else {
490                                     codec = cdc;
491                                 }
492                             }
493                     } else if (hdrName == "Content-Transfer-Encoding") {
494                         if (hdrValue != "8bit") {
495                             cd.appendError(QString::fromLatin1("Unexpected Content-Transfer-Encoding '%1'")
496                                 .arg(QString::fromLatin1(hdrValue)));
497                             return false;
498                         }
499                     } else if (hdrName == "X-Virgin-Header") {
500                         // legacy
501                     } else {
502                         extras[makePoHeader(QString::fromLatin1(hdrName))] = hdrValue;
503                     }
504                 }
505                 if (!pluralForms.isEmpty()) {
506                     if (translator.languageCode().isEmpty()) {
507                         extras[makePoHeader(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(hdrOrder, ',');
524                             goto doneho;
525                         }
526                         if (hdrOrder.at(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(lines.mid(0, lastCmtLine + 1), '\n');
536                 }
537                 for (QHash<QString, QByteArray>::ConstIterator it = extras.constBegin(),
538                                                                end = extras.constEnd();
539                      it != end; ++it)
540                     translator.setExtra(it.key(), 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(QLatin1Char(':'));
553                     int lpos = ref.lastIndexOf(QLatin1Char(':'));
554                     if (pos != -1 && pos == lpos) {
555                         bool ok;
556                         int lno = ref.mid(pos + 1).toInt(&ok);
557                         if (ok) {
558                             msg.addReference(ref.left(pos), 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(QChar(Translator::TextVariantSeparator),
581                             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('#')) {
602             switch (line.size() < 2 ? 0 : line.at(1)) {
603                 case ':':
604                     item.references += line.mid(3);
605                     item.references += '\n';
606                     break;
607                 case ',': {
608                     QStringList flags =
609                             QString::fromLatin1(line.mid(2)).split(
610                                     QRegExp(QLatin1String("[, ]")), Qt::SkipEmptyParts);
611                     if (flags.removeOne(QLatin1String("fuzzy")))
612                         item.isFuzzy = true;
613                     flags.removeOne(QLatin1String("qt-format"));
614                     TranslatorMessage::ExtraData::const_iterator it =
615                             item.extra.find(QLatin1String("po-flags"));
616                     if (it != item.extra.end())
617                         flags.prepend(*it);
618                     if (!flags.isEmpty())
619                         item.extra[QLatin1String("po-flags")] = flags.join(QLatin1String(", "));
620                     break;
621                 }
622                 case 0:
623                     item.translatorComments += '\n';
624                     break;
625                 case ' ':
626                     slurpComment(item.translatorComments, lines, l);
627                     break;
628                 case '.':
629                     if (line.startsWith("#. ts-context ")) { // legacy
630                         item.context = line.mid(14);
631                     } else if (line.startsWith("#. ts-id ")) {
632                         item.id = line.mid(9);
633                     } else {
634                         item.automaticComments += line.mid(3);
635 
636                     }
637                     break;
638                 case '|':
639                     if (line.startsWith("#| msgid ")) {
640                         item.oldMsgId = slurpEscapedString(lines, l, 9, "#| ", cd);
641                     } else if (line.startsWith("#| msgid_plural ")) {
642                         QByteArray extra = slurpEscapedString(lines, l, 16, "#| ", cd);
643                         if (extra != item.oldMsgId)
644                             item.extra[QLatin1String("po-old_msgid_plural")] =
645                                     codec->toUnicode(extra);
646                     } else if (line.startsWith("#| msgctxt ")) {
647                         item.oldTscomment = slurpEscapedString(lines, l, 11, "#| ", cd);
648                         if (qtContexts)
649                             splitContext(&item.oldTscomment, &item.context);
650                     } else {
651                         cd.appendError(QString(QLatin1String("PO-format parse error in line %1: '%2'"))
652                             .arg(l + 1).arg(codec->toUnicode(lines[l])));
653                         error = true;
654                     }
655                     break;
656                 case '~':
657                     if (line.startsWith("#~ msgid ")) {
658                         item.msgId = slurpEscapedString(lines, l, 9, "#~ ", cd);
659                     } else if (line.startsWith("#~ msgid_plural ")) {
660                         QByteArray extra = slurpEscapedString(lines, l, 16, "#~ ", 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("#~ msgctxt ")) {
666                         item.tscomment = slurpEscapedString(lines, l, 11, "#~ ", cd);
667                         if (qtContexts)
668                             splitContext(&item.tscomment, &item.context);
669                     } else if (line.startsWith("#~| msgid ")) {
670                         item.oldMsgId = slurpEscapedString(lines, l, 10, "#~| ", cd);
671                     } else if (line.startsWith("#~| msgid_plural ")) {
672                         QByteArray extra = slurpEscapedString(lines, l, 17, "#~| ", cd);
673                         if (extra != item.oldMsgId)
674                             item.extra[QLatin1String("po-old_msgid_plural")] =
675                                     codec->toUnicode(extra);
676                     } else if (line.startsWith("#~| msgctxt ")) {
677                         item.oldTscomment = slurpEscapedString(lines, l, 12, "#~| ", cd);
678                         if (qtContexts)
679                             splitContext(&item.oldTscomment, &item.context);
680                     } else {
681                         cd.appendError(QString(QLatin1String("PO-format parse error in line %1: '%2'"))
682                             .arg(l + 1).arg(codec->toUnicode(lines[l])));
683                         error = true;
684                     }
685                     break;
686                 default:
687                     cd.appendError(QString(QLatin1String("PO-format parse error in line %1: '%2'"))
688                         .arg(l + 1).arg(codec->toUnicode(lines[l])));
689                     error = true;
690                     break;
691             }
692             lastCmtLine = l;
693         } else if (line.startsWith("msgctxt ")) {
694             item.tscomment = slurpEscapedString(lines, l, 8, QByteArray(), cd);
695             if (qtContexts)
696                 splitContext(&item.tscomment, &item.context);
697         } else if (line.startsWith("msgid ")) {
698             item.msgId = slurpEscapedString(lines, l, 6, QByteArray(), cd);
699         } else if (line.startsWith("msgid_plural ")) {
700             QByteArray extra = slurpEscapedString(lines, l, 13, 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(QString(QLatin1String("PO-format error in line %1: '%2'"))
706                 .arg(l + 1).arg(codec->toUnicode(lines[l])));
707             error = true;
708         }
709     }
710     return !error && cd.errors().isEmpty();
711 }
712 
addPoHeader(Translator::ExtraData & headers,QStringList & hdrOrder,const char * name,const QString & value)713 static void addPoHeader(Translator::ExtraData &headers, QStringList &hdrOrder,
714                         const char *name, const QString &value)
715 {
716     QString qName = QLatin1String(name);
717     if (!hdrOrder.contains(qName))
718         hdrOrder << qName;
719     headers[makePoHeader(qName)] = value;
720 }
721 
escapeComment(const QString & in,bool escape)722 static QString escapeComment(const QString &in, bool escape)
723 {
724     QString out = in;
725     if (escape) {
726         out.replace(QLatin1Char('~'), QLatin1String("~~"));
727         out.replace(QLatin1Char('|'), QLatin1String("~|"));
728     }
729     return out;
730 }
731 
savePO(const Translator & translator,QIODevice & dev,ConversionData &)732 bool 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(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(QLatin1String("po-headers")).split(
753             QLatin1Char(','), Qt::SkipEmptyParts);
754     // Keep in sync with loadPO
755     addPoHeader(headers, hdrOrder, "MIME-Version", QLatin1String("1.0"));
756     addPoHeader(headers, hdrOrder, "Content-Type",
757                 QLatin1String("text/plain; charset=" + out.codec()->name()));
758     addPoHeader(headers, hdrOrder, "Content-Transfer-Encoding", QLatin1String("8bit"));
759     if (!translator.languageCode().isEmpty()) {
760         QLocale::Language l;
761         QLocale::Country c;
762         Translator::languageAndCountry(translator.languageCode(), &l, &c);
763         const char *gettextRules;
764         if (getNumerusInfo(l, c, 0, 0, &gettextRules))
765             addPoHeader(headers, hdrOrder, "Plural-Forms", QLatin1String(gettextRules));
766         addPoHeader(headers, hdrOrder, "X-Language", translator.languageCode());
767     }
768     if (!translator.sourceLanguageCode().isEmpty())
769         addPoHeader(headers, hdrOrder, "X-Source-Language", translator.sourceLanguageCode());
770     if (qtContexts)
771         addPoHeader(headers, hdrOrder, "X-Qt-Contexts", QLatin1String("true"));
772     QString hdrStr;
773     foreach (const QString &hdr, hdrOrder) {
774         hdrStr += hdr;
775         hdrStr += QLatin1String(": ");
776         hdrStr += headers.value(makePoHeader(hdr));
777         hdrStr += QLatin1Char('\n');
778     }
779     out << poEscapedString(QString(), QString::fromLatin1("msgstr"), true, hdrStr);
780 
781     foreach (const TranslatorMessage &msg, translator.messages()) {
782         out << Qt::endl;
783 
784         if (!msg.translatorComment().isEmpty())
785             out << poEscapedLines(QLatin1String("#"), true, msg.translatorComment());
786 
787         if (!msg.extraComment().isEmpty())
788             out << poEscapedLines(QLatin1String("#."), true, msg.extraComment());
789 
790         if (!msg.id().isEmpty())
791             out << QLatin1String("#. ts-id ") << msg.id() << '\n';
792 
793         QString xrefs = msg.extra(QLatin1String("po-references"));
794         if (!msg.fileName().isEmpty() || !xrefs.isEmpty()) {
795             QStringList refs;
796             foreach (const TranslatorMessage::Reference &ref, msg.allReferences())
797                 refs.append(QString(QLatin1String("%2:%1"))
798                                     .arg(ref.lineNumber()).arg(ref.fileName()));
799             if (!xrefs.isEmpty())
800                 refs << xrefs;
801             out << poWrappedEscapedLines(QLatin1String("#:"), true, refs.join(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(QLatin1String("fuzzy"));
810         TranslatorMessage::ExtraData::const_iterator itr =
811                 msg.extras().find(QLatin1String("po-flags"));
812         if (itr != msg.extras().end()) {
813             QStringList atoms = itr->split(QLatin1String(", "));
814             foreach (const QString &atom, atoms)
815                 if (atom.endsWith(str_format)) {
816                     skipFormat = true;
817                     break;
818                 }
819             if (atoms.contains(QLatin1String("no-wrap")))
820                 noWrap = true;
821             flags.append(*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(QLatin1Char('%'), off)) >= 0; ) {
828                 if (++off >= source.length())
829                     break;
830                 if (source.at(off) == QLatin1Char('n') || source.at(off).isDigit()) {
831                     flags.append(QLatin1String("qt-format"));
832                     break;
833                 }
834             }
835         }
836         if (!flags.isEmpty())
837             out << "#, " << flags.join(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, QLatin1String("msgctxt"), noWrap,
844                                    escapeComment(msg.oldComment(), qtContexts));
845         if (!msg.oldSourceText().isEmpty())
846             out << poEscapedString(prefix, QLatin1String("msgid"), noWrap, msg.oldSourceText());
847         QString plural = msg.extra(QLatin1String("po-old_msgid_plural"));
848         if (!plural.isEmpty())
849             out << poEscapedString(prefix, QLatin1String("msgid_plural"), noWrap, plural);
850         prefix = QLatin1String(isObsolete ? "#~ " : "");
851         if (!msg.context().isEmpty())
852             out << poEscapedString(prefix, QLatin1String("msgctxt"), noWrap,
853                                    escapeComment(msg.context(), true) + QLatin1Char('|')
854                                    + escapeComment(msg.comment(), true));
855         else if (!msg.comment().isEmpty())
856             out << poEscapedString(prefix, QLatin1String("msgctxt"), noWrap,
857                                    escapeComment(msg.comment(), qtContexts));
858         out << poEscapedString(prefix, QLatin1String("msgid"), noWrap, msg.sourceText());
859         if (!msg.isPlural()) {
860             QString transl = msg.translation();
861             transl.replace(QChar(Translator::BinaryVariantSeparator),
862                            QChar(Translator::TextVariantSeparator));
863             out << poEscapedString(prefix, QLatin1String("msgstr"), noWrap, transl);
864         } else {
865             QString plural = msg.extra(QLatin1String("po-msgid_plural"));
866             if (plural.isEmpty())
867                 plural = msg.sourceText();
868             out << poEscapedString(prefix, QLatin1String("msgid_plural"), noWrap, 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(QChar(Translator::BinaryVariantSeparator),
873                             QChar(Translator::TextVariantSeparator));
874                 out << poEscapedString(prefix, QString::fromLatin1("msgstr[%1]").arg(i), noWrap,
875                                        str);
876             }
877         }
878     }
879     return ok;
880 }
881 
savePOT(const Translator & translator,QIODevice & dev,ConversionData & cd)882 static bool savePOT(const Translator &translator, QIODevice &dev, ConversionData &cd)
883 {
884     Translator ttor = translator;
885     ttor.dropTranslations();
886     return savePO(ttor, dev, cd);
887 }
888 
initPO()889 int 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 
909 Q_CONSTRUCTOR_FUNCTION(initPO)
910 
911 QT_END_NAMESPACE
912