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