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