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#include "xmlparser.h"
6
7#include <QtCore/QDebug>
8#include <QtCore/QMap>
9#include <QtCore/QRegularExpression>
10#include <QtCore/QStack>
11#include <QtCore/QString>
12#include <QtCore/QTextStream>
13
14// The string value is historical and reflects the main purpose: Keeping
15// obsolete entries separate from the magic file message (which both have
16// no location information, but typically reside at opposite ends of the file).
17#define MAGIC_OBSOLETE_REFERENCE "Obsolete_PO_entries"
18
19QT_BEGIN_NAMESPACE
20
21/**
22 * Implementation of XLIFF file format for Linguist
23 */
24//static const char *restypeDomain = "x-gettext-domain";
25static const char *restypeContext = "x-trolltech-linguist-context";
26static const char *restypePlurals = "x-gettext-plurals";
27static const char *restypeDummy = "x-dummy";
28static const char *dataTypeUIFile = "x-trolltech-designer-ui";
29static const char *contextMsgctxt = "x-gettext-msgctxt"; // XXX Troll invention, so far.
30static const char *contextOldMsgctxt = "x-gettext-previous-msgctxt"; // XXX Troll invention, so far.
31static const char *attribPlural = "trolltech:plural";
32static const char *XLIFF11namespaceURI = "urn:oasis:names:tc:xliff:document:1.1";
33static const char *XLIFF12namespaceURI = "urn:oasis:names:tc:xliff:document:1.2";
34static const char *TrollTsNamespaceURI = "urn:trolltech:names:ts:document:1.0";
35
36#define COMBINE4CHARS(c1, c2, c3, c4) \
37 (int(c1) << 24 | int(c2) << 16 | int(c3) << 8 | int(c4) )
38
39static QString dataType(const TranslatorMessage &m)
40{
41 QByteArray fileName = m.fileName().toLatin1();
42 unsigned int extHash = 0;
43 int pos = fileName.size() - 1;
44 for (int pass = 0; pass < 4 && pos >=0; ++pass, --pos) {
45 if (fileName.at(i: pos) == '.')
46 break;
47 extHash |= ((int)fileName.at(i: pos) << (8*pass));
48 }
49
50 switch (extHash) {
51 case COMBINE4CHARS(0,'c','p','p'):
52 case COMBINE4CHARS(0,'c','x','x'):
53 case COMBINE4CHARS(0,'c','+','+'):
54 case COMBINE4CHARS(0,'h','p','p'):
55 case COMBINE4CHARS(0,'h','x','x'):
56 case COMBINE4CHARS(0,'h','+','+'):
57 return QLatin1String("cpp");
58 case COMBINE4CHARS(0, 0 , 0 ,'c'):
59 case COMBINE4CHARS(0, 0 , 0 ,'h'):
60 case COMBINE4CHARS(0, 0 ,'c','c'):
61 case COMBINE4CHARS(0, 0 ,'c','h'):
62 case COMBINE4CHARS(0, 0 ,'h','h'):
63 return QLatin1String("c");
64 case COMBINE4CHARS(0, 0 ,'u','i'):
65 return QLatin1String(dataTypeUIFile); //### form?
66 default:
67 return QLatin1String("plaintext"); // we give up
68 }
69}
70
71static void writeIndent(QTextStream &ts, int indent)
72{
73 ts << QString().fill(c: QLatin1Char(' '), size: indent * 2);
74}
75
76struct CharMnemonic
77{
78 char ch;
79 char escape;
80 const char *mnemonic;
81};
82
83static const CharMnemonic charCodeMnemonics[] = {
84 {.ch: 0x07, .escape: 'a', .mnemonic: "bel"},
85 {.ch: 0x08, .escape: 'b', .mnemonic: "bs"},
86 {.ch: 0x09, .escape: 't', .mnemonic: "tab"},
87 {.ch: 0x0a, .escape: 'n', .mnemonic: "lf"},
88 {.ch: 0x0b, .escape: 'v', .mnemonic: "vt"},
89 {.ch: 0x0c, .escape: 'f', .mnemonic: "ff"},
90 {.ch: 0x0d, .escape: 'r', .mnemonic: "cr"}
91};
92
93static char charFromEscape(char escape)
94{
95 for (uint i = 0; i < sizeof(charCodeMnemonics)/sizeof(CharMnemonic); ++i) {
96 CharMnemonic cm = charCodeMnemonics[i];
97 if (cm.escape == escape)
98 return cm.ch;
99 }
100 Q_ASSERT(0);
101 return escape;
102}
103
104static QString xlNumericEntity(int ch, bool makePhs)
105{
106 // ### This needs to be reviewed, to reflect the updated XLIFF-PO spec.
107 if (!makePhs || ch < 7 || ch > 0x0d)
108 return QString::fromLatin1(ba: "&#x%1;").arg(a: QString::number(ch, base: 16));
109
110 CharMnemonic cm = charCodeMnemonics[int(ch) - 7];
111 QString name = QLatin1String(cm.mnemonic);
112 char escapechar = cm.escape;
113
114 static int id = 0;
115 return QString::fromLatin1(ba: "<ph id=\"ph%1\" ctype=\"x-ch-%2\">\\%3</ph>")
116 .arg(a: ++id) .arg(a: name) .arg(a: escapechar);
117}
118
119static QString xlProtect(const QString &str, bool makePhs = true)
120{
121 QString result;
122 int len = str.size();
123 for (int i = 0; i != len; ++i) {
124 uint c = str.at(i).unicode();
125 switch (c) {
126 case '\"':
127 result += QLatin1String("&quot;");
128 break;
129 case '&':
130 result += QLatin1String("&amp;");
131 break;
132 case '>':
133 result += QLatin1String("&gt;");
134 break;
135 case '<':
136 result += QLatin1String("&lt;");
137 break;
138 case '\'':
139 result += QLatin1String("&apos;");
140 break;
141 default:
142 if (c < 0x20 && c != '\r' && c != '\n' && c != '\t')
143 result += xlNumericEntity(ch: c, makePhs);
144 else // this also covers surrogates
145 result += QChar(c);
146 }
147 }
148 return result;
149}
150
151
152static void writeExtras(QTextStream &ts, int indent,
153 const TranslatorMessage::ExtraData &extras, QRegularExpression drops)
154{
155 for (auto it = extras.cbegin(), end = extras.cend(); it != end; ++it) {
156 if (!drops.match(subject: it.key()).hasMatch()) {
157 writeIndent(ts, indent);
158 ts << "<trolltech:" << it.key() << '>'
159 << xlProtect(str: it.value())
160 << "</trolltech:" << it.key() << ">\n";
161 }
162 }
163}
164
165static void writeLineNumber(QTextStream &ts, const TranslatorMessage &msg, int indent)
166{
167 if (msg.lineNumber() == -1)
168 return;
169 writeIndent(ts, indent);
170 ts << "<context-group purpose=\"location\"><context context-type=\"linenumber\">"
171 << msg.lineNumber() << "</context></context-group>\n";
172 const auto refs = msg.extraReferences();
173 for (const TranslatorMessage::Reference &ref : refs) {
174 writeIndent(ts, indent);
175 ts << "<context-group purpose=\"location\">";
176 if (ref.fileName() != msg.fileName())
177 ts << "<context context-type=\"sourcefile\">" << ref.fileName() << "</context>";
178 ts << "<context context-type=\"linenumber\">" << ref.lineNumber()
179 << "</context></context-group>\n";
180 }
181}
182
183static void writeComment(QTextStream &ts, const TranslatorMessage &msg, const QRegularExpression &drops, int indent)
184{
185 if (!msg.comment().isEmpty()) {
186 writeIndent(ts, indent);
187 ts << "<context-group><context context-type=\"" << contextMsgctxt << "\">"
188 << xlProtect(str: msg.comment(), makePhs: false)
189 << "</context></context-group>\n";
190 }
191 if (!msg.oldComment().isEmpty()) {
192 writeIndent(ts, indent);
193 ts << "<context-group><context context-type=\"" << contextOldMsgctxt << "\">"
194 << xlProtect(str: msg.oldComment(), makePhs: false)
195 << "</context></context-group>\n";
196 }
197 writeExtras(ts, indent, extras: msg.extras(), drops);
198 if (!msg.extraComment().isEmpty()) {
199 writeIndent(ts, indent);
200 ts << "<note annotates=\"source\" from=\"developer\">"
201 << xlProtect(str: msg.extraComment()) << "</note>\n";
202 }
203 if (!msg.translatorComment().isEmpty()) {
204 writeIndent(ts, indent);
205 ts << "<note from=\"translator\">"
206 << xlProtect(str: msg.translatorComment()) << "</note>\n";
207 }
208}
209
210static void writeTransUnits(QTextStream &ts, const TranslatorMessage &msg, const QRegularExpression &drops, int indent)
211{
212 static int msgid;
213 QString msgidstr = !msg.id().isEmpty() ? msg.id() : QString::fromLatin1(ba: "_msg%1").arg(a: ++msgid);
214
215 QStringList translns = msg.translations();
216 QString pluralStr;
217 QStringList sources(msg.sourceText());
218 const auto &extras = msg.extras();
219 const auto extrasEnd = extras.cend();
220 if (const auto it = extras.constFind(key: QString::fromLatin1(ba: "po-msgid_plural")); it != extrasEnd)
221 sources.append(t: *it);
222 QStringList oldsources;
223 if (!msg.oldSourceText().isEmpty())
224 oldsources.append(t: msg.oldSourceText());
225 if (const auto it = extras.constFind(key: QString::fromLatin1(ba: "po-old_msgid_plural")); it != extrasEnd) {
226 if (oldsources.isEmpty()) {
227 if (sources.size() == 2)
228 oldsources.append(t: QString());
229 else
230 pluralStr = QLatin1Char(' ') + QLatin1String(attribPlural) + QLatin1String("=\"yes\"");
231 }
232 oldsources.append(t: *it);
233 }
234
235 auto srcit = sources.cbegin(), srcend = sources.cend(),
236 oldsrcit = oldsources.cbegin(), oldsrcend = oldsources.cend(),
237 transit = translns.cbegin(), transend = translns.cend();
238 int plural = 0;
239 QString source;
240 while (srcit != srcend || oldsrcit != oldsrcend || transit != transend) {
241 QByteArray attribs;
242 QByteArray state;
243 if ((msg.type() == TranslatorMessage::Obsolete
244 || msg.type() == TranslatorMessage::Vanished)
245 && !msg.isPlural()) {
246 attribs = " translate=\"no\"";
247 }
248 if (msg.type() == TranslatorMessage::Finished
249 || msg.type() == TranslatorMessage::Vanished) {
250 attribs += " approved=\"yes\"";
251 } else if (msg.type() == TranslatorMessage::Unfinished
252 && transit != transend && !transit->isEmpty()) {
253 state = " state=\"needs-review-translation\"";
254 }
255 writeIndent(ts, indent);
256 ts << "<trans-unit id=\"" << msgidstr;
257 if (msg.isPlural())
258 ts << "[" << plural++ << "]";
259 ts << "\"" << attribs << ">\n";
260 ++indent;
261
262 writeIndent(ts, indent);
263 if (srcit != srcend) {
264 source = *srcit;
265 ++srcit;
266 } // else just repeat last element
267 ts << "<source xml:space=\"preserve\">" << xlProtect(str: source) << "</source>\n";
268
269 bool puttrans = false;
270 QString translation;
271 if (transit != transend) {
272 translation = *transit;
273 translation.replace(before: QChar(Translator::BinaryVariantSeparator),
274 after: QChar(Translator::TextVariantSeparator));
275 ++transit;
276 puttrans = true;
277 }
278 do {
279 if (oldsrcit != oldsrcend && !oldsrcit->isEmpty()) {
280 writeIndent(ts, indent);
281 ts << "<alt-trans>\n";
282 ++indent;
283 writeIndent(ts, indent);
284 ts << "<source xml:space=\"preserve\"" << pluralStr << '>' << xlProtect(str: *oldsrcit) << "</source>\n";
285 if (!puttrans) {
286 writeIndent(ts, indent);
287 ts << "<target restype=\"" << restypeDummy << "\"/>\n";
288 }
289 }
290
291 if (puttrans) {
292 writeIndent(ts, indent);
293 ts << "<target xml:space=\"preserve\"" << state << ">" << xlProtect(str: translation) << "</target>\n";
294 }
295
296 if (oldsrcit != oldsrcend) {
297 if (!oldsrcit->isEmpty()) {
298 --indent;
299 writeIndent(ts, indent);
300 ts << "</alt-trans>\n";
301 }
302 ++oldsrcit;
303 }
304
305 puttrans = false;
306 } while (srcit == srcend && oldsrcit != oldsrcend);
307
308 if (!msg.isPlural()) {
309 writeLineNumber(ts, msg, indent);
310 writeComment(ts, msg, drops, indent);
311 }
312
313 --indent;
314 writeIndent(ts, indent);
315 ts << "</trans-unit>\n";
316 }
317}
318
319static void writeMessage(QTextStream &ts, const TranslatorMessage &msg, const QRegularExpression &drops, int indent)
320{
321 if (msg.isPlural()) {
322 writeIndent(ts, indent);
323 ts << "<group restype=\"" << restypePlurals << "\"";
324 if (!msg.id().isEmpty())
325 ts << " id=\"" << msg.id() << "\"";
326 if (msg.type() == TranslatorMessage::Obsolete || msg.type() == TranslatorMessage::Vanished)
327 ts << " translate=\"no\"";
328 ts << ">\n";
329 ++indent;
330 writeLineNumber(ts, msg, indent);
331 writeComment(ts, msg, drops, indent);
332
333 writeTransUnits(ts, msg, drops, indent);
334 --indent;
335 writeIndent(ts, indent);
336 ts << "</group>\n";
337 } else {
338 writeTransUnits(ts, msg, drops, indent);
339 }
340}
341
342class XLIFFHandler : public XmlParser
343{
344public:
345 XLIFFHandler(Translator &translator, ConversionData &cd, QXmlStreamReader &reader);
346 ~XLIFFHandler() override = default;
347
348private:
349 bool startElement(QStringView namespaceURI, QStringView localName,
350 QStringView qName, const QXmlStreamAttributes &atts) override;
351 bool endElement(QStringView namespaceURI, QStringView localName,
352 QStringView qName) override;
353 bool characters(QStringView ch) override;
354 bool fatalError(qint64 line, qint64 column, const QString &message) override;
355
356 bool endDocument() override;
357
358 enum XliffContext {
359 XC_xliff,
360 XC_group,
361 XC_trans_unit,
362 XC_context_group,
363 XC_context_group_any,
364 XC_context,
365 XC_context_filename,
366 XC_context_linenumber,
367 XC_context_context,
368 XC_context_comment,
369 XC_context_old_comment,
370 XC_ph,
371 XC_extra_comment,
372 XC_translator_comment,
373 XC_restype_context,
374 XC_restype_translation,
375 XC_mtype_seg_translation,
376 XC_restype_plurals,
377 XC_alt_trans
378 };
379 void pushContext(XliffContext ctx);
380 bool popContext(XliffContext ctx);
381 XliffContext currentContext() const;
382 bool hasContext(XliffContext ctx) const;
383 bool finalizeMessage(bool isPlural);
384
385private:
386 Translator &m_translator;
387 ConversionData &m_cd;
388 QString m_language;
389 QString m_sourceLanguage;
390 QString m_context;
391 QString m_id;
392 QStringList m_sources;
393 QStringList m_oldSources;
394 QString m_comment;
395 QString m_oldComment;
396 QString m_extraComment;
397 QString m_translatorComment;
398 bool m_translate;
399 bool m_approved;
400 bool m_isPlural;
401 bool m_hadAlt;
402 QStringList m_translations;
403 QString m_fileName;
404 int m_lineNumber;
405 QString m_extraFileName;
406 TranslatorMessage::References m_refs;
407 TranslatorMessage::ExtraData m_extra;
408
409 QString accum;
410 QString m_ctype;
411 const QString m_URITT; // convenience and efficiency
412 const QString m_URI; // ...
413 const QString m_URI12; // ...
414 QStack<int> m_contextStack;
415};
416
417XLIFFHandler::XLIFFHandler(Translator &translator, ConversionData &cd, QXmlStreamReader &reader)
418 : XmlParser(reader, true),
419 m_translator(translator),
420 m_cd(cd),
421 m_translate(true),
422 m_approved(true),
423 m_lineNumber(-1),
424 m_URITT(QLatin1String(TrollTsNamespaceURI)),
425 m_URI(QLatin1String(XLIFF11namespaceURI)),
426 m_URI12(QLatin1String(XLIFF12namespaceURI))
427{}
428
429
430void XLIFFHandler::pushContext(XliffContext ctx)
431{
432 m_contextStack.push_back(t: ctx);
433}
434
435// Only pops it off if the top of the stack contains ctx
436bool XLIFFHandler::popContext(XliffContext ctx)
437{
438 if (!m_contextStack.isEmpty() && m_contextStack.top() == ctx) {
439 m_contextStack.pop();
440 return true;
441 }
442 return false;
443}
444
445XLIFFHandler::XliffContext XLIFFHandler::currentContext() const
446{
447 if (!m_contextStack.isEmpty())
448 return (XliffContext)m_contextStack.top();
449 return XC_xliff;
450}
451
452// traverses to the top to check all of the parent contexes.
453bool XLIFFHandler::hasContext(XliffContext ctx) const
454{
455 for (int i = m_contextStack.size() - 1; i >= 0; --i) {
456 if (m_contextStack.at(i) == ctx)
457 return true;
458 }
459 return false;
460}
461
462bool XLIFFHandler::startElement(QStringView namespaceURI, QStringView localName,
463 QStringView qName, const QXmlStreamAttributes &atts)
464{
465 Q_UNUSED(qName);
466 if (namespaceURI == m_URITT)
467 goto bail;
468 if (namespaceURI != m_URI && namespaceURI != m_URI12) {
469 return fatalError(line: reader.lineNumber(), column: reader.columnNumber(),
470 message: QLatin1String("Unknown namespace in the XLIFF file"));
471 }
472 if (localName == QLatin1String("xliff")) {
473 // make sure that the stack is not empty during parsing
474 pushContext(ctx: XC_xliff);
475 } else if (localName == QLatin1String("file")) {
476 m_fileName = atts.value(qualifiedName: QLatin1String("original")).toString();
477 m_language = atts.value(qualifiedName: QLatin1String("target-language")).toString();
478 m_language.replace(before: QLatin1Char('-'), after: QLatin1Char('_'));
479 m_sourceLanguage = atts.value(qualifiedName: QLatin1String("source-language")).toString();
480 m_sourceLanguage.replace(before: QLatin1Char('-'), after: QLatin1Char('_'));
481 if (m_sourceLanguage == QLatin1String("en"))
482 m_sourceLanguage.clear();
483 } else if (localName == QLatin1String("group")) {
484 if (atts.value(qualifiedName: QLatin1String("restype")) == QLatin1String(restypeContext)) {
485 m_context = atts.value(qualifiedName: QLatin1String("resname")).toString();
486 pushContext(ctx: XC_restype_context);
487 } else {
488 if (atts.value(qualifiedName: QLatin1String("restype")) == QLatin1String(restypePlurals)) {
489 pushContext(ctx: XC_restype_plurals);
490 m_id = atts.value(qualifiedName: QLatin1String("id")).toString();
491 if (atts.value(qualifiedName: QLatin1String("translate")) == QLatin1String("no"))
492 m_translate = false;
493 } else {
494 pushContext(ctx: XC_group);
495 }
496 }
497 } else if (localName == QLatin1String("trans-unit")) {
498 if (!hasContext(ctx: XC_restype_plurals) || m_sources.isEmpty() /* who knows ... */)
499 if (atts.value(qualifiedName: QLatin1String("translate")) == QLatin1String("no"))
500 m_translate = false;
501 if (!hasContext(ctx: XC_restype_plurals)) {
502 m_id = atts.value(qualifiedName: QLatin1String("id")).toString();
503 if (m_id.startsWith(s: QLatin1String("_msg")))
504 m_id.clear();
505 }
506 if (atts.value(qualifiedName: QLatin1String("approved")) != QLatin1String("yes"))
507 m_approved = false;
508 pushContext(ctx: XC_trans_unit);
509 m_hadAlt = false;
510 } else if (localName == QLatin1String("alt-trans")) {
511 pushContext(ctx: XC_alt_trans);
512 } else if (localName == QLatin1String("source")) {
513 m_isPlural = atts.value(qualifiedName: QLatin1String(attribPlural)) == QLatin1String("yes");
514 } else if (localName == QLatin1String("target")) {
515 if (atts.value(qualifiedName: QLatin1String("restype")) != QLatin1String(restypeDummy))
516 pushContext(ctx: XC_restype_translation);
517 } else if (localName == QLatin1String("mrk")) {
518 if (atts.value(qualifiedName: QLatin1String("mtype")) == QLatin1String("seg")) {
519 if (currentContext() == XC_restype_translation)
520 pushContext(ctx: XC_mtype_seg_translation);
521 }
522 } else if (localName == QLatin1String("context-group")) {
523 if (atts.value(qualifiedName: QLatin1String("purpose")) == QLatin1String("location"))
524 pushContext(ctx: XC_context_group);
525 else
526 pushContext(ctx: XC_context_group_any);
527 } else if (currentContext() == XC_context_group && localName == QLatin1String("context")) {
528 const auto ctxtype = atts.value(qualifiedName: QLatin1String("context-type"));
529 if (ctxtype == QLatin1String("linenumber"))
530 pushContext(ctx: XC_context_linenumber);
531 else if (ctxtype == QLatin1String("sourcefile"))
532 pushContext(ctx: XC_context_filename);
533 } else if (currentContext() == XC_context_group_any && localName == QLatin1String("context")) {
534 const auto ctxtype = atts.value(qualifiedName: QLatin1String("context-type"));
535 if (ctxtype == QLatin1String(contextMsgctxt))
536 pushContext(ctx: XC_context_comment);
537 else if (ctxtype == QLatin1String(contextOldMsgctxt))
538 pushContext(ctx: XC_context_old_comment);
539 } else if (localName == QLatin1String("note")) {
540 if (atts.value(qualifiedName: QLatin1String("annotates")) == QLatin1String("source") &&
541 atts.value(qualifiedName: QLatin1String("from")) == QLatin1String("developer"))
542 pushContext(ctx: XC_extra_comment);
543 else
544 pushContext(ctx: XC_translator_comment);
545 } else if (localName == QLatin1String("ph")) {
546 QString ctype = atts.value(qualifiedName: QLatin1String("ctype")).toString();
547 if (ctype.startsWith(s: QLatin1String("x-ch-")))
548 m_ctype = ctype.mid(position: 5);
549 pushContext(ctx: XC_ph);
550 }
551bail:
552 if (currentContext() != XC_ph && currentContext() != XC_mtype_seg_translation)
553 accum.clear();
554 return true;
555}
556
557bool XLIFFHandler::endElement(QStringView namespaceURI, QStringView localName,
558 QStringView qName)
559{
560 Q_UNUSED(qName);
561 if (namespaceURI == m_URITT) {
562 if (hasContext(ctx: XC_trans_unit) || hasContext(ctx: XC_restype_plurals))
563 m_extra[localName.toString()] = accum;
564 else
565 m_translator.setExtra(ba: localName.toString(), var: accum);
566 return true;
567 }
568 if (namespaceURI != m_URI && namespaceURI != m_URI12) {
569 return fatalError(line: reader.lineNumber(), column: reader.columnNumber(),
570 message: QLatin1String("Unknown namespace in the XLIFF file"));
571 }
572 //qDebug() << "URI:" << namespaceURI << "QNAME:" << qName;
573 if (localName == QLatin1String("xliff")) {
574 popContext(ctx: XC_xliff);
575 } else if (localName == QLatin1String("source")) {
576 if (hasContext(ctx: XC_alt_trans)) {
577 if (m_isPlural && m_oldSources.isEmpty())
578 m_oldSources.append(t: QString());
579 m_oldSources.append(t: accum);
580 m_hadAlt = true;
581 } else {
582 m_sources.append(t: accum);
583 }
584 } else if (localName == QLatin1String("target")) {
585 if (popContext(ctx: XC_restype_translation)) {
586 accum.replace(before: QChar(Translator::TextVariantSeparator),
587 after: QChar(Translator::BinaryVariantSeparator));
588 m_translations.append(t: accum);
589 }
590 } else if (localName == QLatin1String("mrk")) {
591 popContext(ctx: XC_mtype_seg_translation);
592 } else if (localName == QLatin1String("context-group")) {
593 if (popContext(ctx: XC_context_group)) {
594 m_refs.append(t: TranslatorMessage::Reference(
595 m_extraFileName.isEmpty() ? m_fileName : m_extraFileName, m_lineNumber));
596 m_extraFileName.clear();
597 m_lineNumber = -1;
598 } else {
599 popContext(ctx: XC_context_group_any);
600 }
601 } else if (localName == QLatin1String("context")) {
602 if (popContext(ctx: XC_context_linenumber)) {
603 bool ok;
604 m_lineNumber = accum.trimmed().toInt(ok: &ok);
605 if (!ok)
606 m_lineNumber = -1;
607 } else if (popContext(ctx: XC_context_filename)) {
608 m_extraFileName = accum;
609 } else if (popContext(ctx: XC_context_comment)) {
610 m_comment = accum;
611 } else if (popContext(ctx: XC_context_old_comment)) {
612 m_oldComment = accum;
613 }
614 } else if (localName == QLatin1String("note")) {
615 if (popContext(ctx: XC_extra_comment))
616 m_extraComment = accum;
617 else if (popContext(ctx: XC_translator_comment))
618 m_translatorComment = accum;
619 } else if (localName == QLatin1String("ph")) {
620 m_ctype.clear();
621 popContext(ctx: XC_ph);
622 } else if (localName == QLatin1String("trans-unit")) {
623 popContext(ctx: XC_trans_unit);
624 if (!m_hadAlt)
625 m_oldSources.append(t: QString());
626 if (!hasContext(ctx: XC_restype_plurals)) {
627 if (!finalizeMessage(isPlural: false)) {
628 return fatalError(line: reader.lineNumber(), column: reader.columnNumber(),
629 message: QLatin1String("Element processing failed"));
630 }
631 }
632 } else if (localName == QLatin1String("alt-trans")) {
633 popContext(ctx: XC_alt_trans);
634 } else if (localName == QLatin1String("group")) {
635 if (popContext(ctx: XC_restype_plurals)) {
636 if (!finalizeMessage(isPlural: true)) {
637 return fatalError(line: reader.lineNumber(), column: reader.columnNumber(),
638 message: QLatin1String("Element processing failed"));
639 }
640 } else if (popContext(ctx: XC_restype_context)) {
641 m_context.clear();
642 } else {
643 popContext(ctx: XC_group);
644 }
645 }
646 return true;
647}
648
649bool XLIFFHandler::characters(QStringView ch)
650{
651 if (currentContext() == XC_ph) {
652 // handle the content of <ph> elements
653 for (int i = 0; i < ch.size(); ++i) {
654 QChar chr = ch.at(n: i);
655 if (accum.endsWith(c: QLatin1Char('\\')))
656 accum[accum.size() - 1] = QLatin1Char(charFromEscape(escape: chr.toLatin1()));
657 else
658 accum.append(c: chr);
659 }
660 } else {
661 QString t = ch.toString();
662 t.replace(before: QLatin1String("\r"), after: QLatin1String(""));
663 accum.append(s: t);
664 }
665 return true;
666}
667
668bool XLIFFHandler::endDocument()
669{
670 m_translator.setLanguageCode(m_language);
671 m_translator.setSourceLanguageCode(m_sourceLanguage);
672 return true;
673}
674
675bool XLIFFHandler::finalizeMessage(bool isPlural)
676{
677 if (m_sources.isEmpty()) {
678 m_cd.appendError(error: QLatin1String("XLIFF syntax error: Message without source string."));
679 return false;
680 }
681 if (!m_translate && m_refs.size() == 1
682 && m_refs.at(i: 0).fileName() == QLatin1String(MAGIC_OBSOLETE_REFERENCE))
683 m_refs.clear();
684 TranslatorMessage::Type type
685 = m_translate ? (m_approved ? TranslatorMessage::Finished : TranslatorMessage::Unfinished)
686 : (m_approved ? TranslatorMessage::Vanished : TranslatorMessage::Obsolete);
687 TranslatorMessage msg(m_context, m_sources[0],
688 m_comment, QString(), QString(), -1,
689 m_translations, type, isPlural);
690 msg.setId(m_id);
691 msg.setReferences(m_refs);
692 msg.setOldComment(m_oldComment);
693 msg.setExtraComment(m_extraComment);
694 msg.setTranslatorComment(m_translatorComment);
695 msg.setFileName(m_fileName);
696 if (m_sources.size() > 1 && m_sources[1] != m_sources[0])
697 m_extra.insert(key: QLatin1String("po-msgid_plural"), value: m_sources[1]);
698 if (!m_oldSources.isEmpty()) {
699 if (!m_oldSources[0].isEmpty())
700 msg.setOldSourceText(m_oldSources[0]);
701 if (m_oldSources.size() > 1 && m_oldSources[1] != m_oldSources[0])
702 m_extra.insert(key: QLatin1String("po-old_msgid_plural"), value: m_oldSources[1]);
703 }
704 msg.setExtras(m_extra);
705 m_translator.append(msg);
706
707 m_id.clear();
708 m_sources.clear();
709 m_oldSources.clear();
710 m_translations.clear();
711 m_comment.clear();
712 m_oldComment.clear();
713 m_extraComment.clear();
714 m_translatorComment.clear();
715 m_extra.clear();
716 m_refs.clear();
717 m_translate = true;
718 m_approved = true;
719 return true;
720}
721
722bool XLIFFHandler::fatalError(qint64 line, qint64 column, const QString &message)
723{
724 QString msg = QString::asprintf(format: "XML error: Parse error at line %d, column %d (%s).\n",
725 static_cast<int>(line), static_cast<int>(column),
726 message.toLatin1().data());
727 m_cd.appendError(error: msg);
728 return false;
729}
730
731bool loadXLIFF(Translator &translator, QIODevice &dev, ConversionData &cd)
732{
733 QXmlStreamReader reader(&dev);
734 XLIFFHandler hand(translator, cd, reader);
735 return hand.parse();
736}
737
738bool saveXLIFF(const Translator &translator, QIODevice &dev, ConversionData &cd)
739{
740 bool ok = true;
741 int indent = 0;
742
743 QTextStream ts(&dev);
744
745 QStringList dtgs = cd.dropTags();
746 dtgs << QLatin1String("po-(old_)?msgid_plural");
747 QRegularExpression drops(QRegularExpression::anchoredPattern(expression: dtgs.join(sep: QLatin1Char('|'))));
748
749 QHash<QString, QHash<QString, QList<TranslatorMessage> > > messageOrder;
750 QHash<QString, QList<QString> > contextOrder;
751 QList<QString> fileOrder;
752 for (const TranslatorMessage &msg : translator.messages()) {
753 QString fn = msg.fileName();
754 if (fn.isEmpty() && msg.type() == TranslatorMessage::Obsolete)
755 fn = QLatin1String(MAGIC_OBSOLETE_REFERENCE);
756 QHash<QString, QList<TranslatorMessage> > &file = messageOrder[fn];
757 if (file.isEmpty())
758 fileOrder.append(t: fn);
759 QList<TranslatorMessage> &context = file[msg.context()];
760 if (context.isEmpty())
761 contextOrder[fn].append(t: msg.context());
762 context.append(t: msg);
763 }
764
765 ts.setFieldAlignment(QTextStream::AlignRight);
766 ts << "<?xml version=\"1.0\"";
767 ts << " encoding=\"utf-8\"?>\n";
768 ts << "<xliff version=\"1.2\" xmlns=\"" << XLIFF12namespaceURI
769 << "\" xmlns:trolltech=\"" << TrollTsNamespaceURI << "\">\n";
770 ++indent;
771 writeExtras(ts, indent, extras: translator.extras(), drops);
772 QString sourceLanguageCode = translator.sourceLanguageCode();
773 if (sourceLanguageCode.isEmpty() || sourceLanguageCode == QLatin1String("C"))
774 sourceLanguageCode = QLatin1String("en");
775 else
776 sourceLanguageCode.replace(before: QLatin1Char('_'), after: QLatin1Char('-'));
777 QString languageCode = translator.languageCode();
778 languageCode.replace(before: QLatin1Char('_'), after: QLatin1Char('-'));
779 for (const QString &fn : std::as_const(t&: fileOrder)) {
780 writeIndent(ts, indent);
781 ts << "<file original=\"" << fn << "\""
782 << " datatype=\"" << dataType(m: messageOrder[fn].cbegin()->first()) << "\""
783 << " source-language=\"" << sourceLanguageCode.toLatin1() << "\""
784 << " target-language=\"" << languageCode.toLatin1() << "\""
785 << "><body>\n";
786 ++indent;
787
788 for (const QString &ctx : std::as_const(t&: contextOrder[fn])) {
789 if (!ctx.isEmpty()) {
790 writeIndent(ts, indent);
791 ts << "<group restype=\"" << restypeContext << "\""
792 << " resname=\"" << xlProtect(str: ctx) << "\">\n";
793 ++indent;
794 }
795
796 for (const TranslatorMessage &msg : std::as_const(t&: messageOrder[fn][ctx]))
797 writeMessage(ts, msg, drops, indent);
798
799 if (!ctx.isEmpty()) {
800 --indent;
801 writeIndent(ts, indent);
802 ts << "</group>\n";
803 }
804 }
805
806 --indent;
807 writeIndent(ts, indent);
808 ts << "</body></file>\n";
809 }
810 --indent;
811 writeIndent(ts, indent);
812 ts << "</xliff>\n";
813
814 return ok;
815}
816
817int initXLIFF()
818{
819 Translator::FileFormat format;
820 format.extension = QLatin1String("xlf");
821 format.untranslatedDescription = QT_TRANSLATE_NOOP("FMT", "XLIFF localization files");
822 format.fileType = Translator::FileFormat::TranslationSource;
823 format.priority = 1;
824 format.loader = &loadXLIFF;
825 format.saver = &saveXLIFF;
826 Translator::registerFileFormat(format);
827 return 1;
828}
829
830Q_CONSTRUCTOR_FUNCTION(initXLIFF)
831
832QT_END_NAMESPACE
833

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

source code of qttools/src/linguist/shared/xliff.cpp