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