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