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_restype_plurals,
376 XC_alt_trans
377 };
378 void pushContext(XliffContext ctx);
379 bool popContext(XliffContext ctx);
380 XliffContext currentContext() const;
381 bool hasContext(XliffContext ctx) const;
382 bool finalizeMessage(bool isPlural);
383
384private:
385 Translator &m_translator;
386 ConversionData &m_cd;
387 QString m_language;
388 QString m_sourceLanguage;
389 QString m_context;
390 QString m_id;
391 QStringList m_sources;
392 QStringList m_oldSources;
393 QString m_comment;
394 QString m_oldComment;
395 QString m_extraComment;
396 QString m_translatorComment;
397 bool m_translate;
398 bool m_approved;
399 bool m_isPlural;
400 bool m_hadAlt;
401 QStringList m_translations;
402 QString m_fileName;
403 int m_lineNumber;
404 QString m_extraFileName;
405 TranslatorMessage::References m_refs;
406 TranslatorMessage::ExtraData m_extra;
407
408 QString accum;
409 QString m_ctype;
410 const QString m_URITT; // convenience and efficiency
411 const QString m_URI; // ...
412 const QString m_URI12; // ...
413 QStack<int> m_contextStack;
414};
415
416XLIFFHandler::XLIFFHandler(Translator &translator, ConversionData &cd, QXmlStreamReader &reader)
417 : XmlParser(reader, true),
418 m_translator(translator),
419 m_cd(cd),
420 m_translate(true),
421 m_approved(true),
422 m_lineNumber(-1),
423 m_URITT(QLatin1String(TrollTsNamespaceURI)),
424 m_URI(QLatin1String(XLIFF11namespaceURI)),
425 m_URI12(QLatin1String(XLIFF12namespaceURI))
426{}
427
428
429void XLIFFHandler::pushContext(XliffContext ctx)
430{
431 m_contextStack.push_back(t: ctx);
432}
433
434// Only pops it off if the top of the stack contains ctx
435bool XLIFFHandler::popContext(XliffContext ctx)
436{
437 if (!m_contextStack.isEmpty() && m_contextStack.top() == ctx) {
438 m_contextStack.pop();
439 return true;
440 }
441 return false;
442}
443
444XLIFFHandler::XliffContext XLIFFHandler::currentContext() const
445{
446 if (!m_contextStack.isEmpty())
447 return (XliffContext)m_contextStack.top();
448 return XC_xliff;
449}
450
451// traverses to the top to check all of the parent contexes.
452bool XLIFFHandler::hasContext(XliffContext ctx) const
453{
454 for (int i = m_contextStack.size() - 1; i >= 0; --i) {
455 if (m_contextStack.at(i) == ctx)
456 return true;
457 }
458 return false;
459}
460
461bool XLIFFHandler::startElement(QStringView namespaceURI, QStringView localName,
462 QStringView qName, const QXmlStreamAttributes &atts)
463{
464 Q_UNUSED(qName);
465 if (namespaceURI == m_URITT)
466 goto bail;
467 if (namespaceURI != m_URI && namespaceURI != m_URI12) {
468 return fatalError(line: reader.lineNumber(), column: reader.columnNumber(),
469 message: QLatin1String("Unknown namespace in the XLIFF file"));
470 }
471 if (localName == QLatin1String("xliff")) {
472 // make sure that the stack is not empty during parsing
473 pushContext(ctx: XC_xliff);
474 } else if (localName == QLatin1String("file")) {
475 m_fileName = atts.value(qualifiedName: QLatin1String("original")).toString();
476 m_language = atts.value(qualifiedName: QLatin1String("target-language")).toString();
477 m_language.replace(before: QLatin1Char('-'), after: QLatin1Char('_'));
478 m_sourceLanguage = atts.value(qualifiedName: QLatin1String("source-language")).toString();
479 m_sourceLanguage.replace(before: QLatin1Char('-'), after: QLatin1Char('_'));
480 if (m_sourceLanguage == QLatin1String("en"))
481 m_sourceLanguage.clear();
482 } else if (localName == QLatin1String("group")) {
483 if (atts.value(qualifiedName: QLatin1String("restype")) == QLatin1String(restypeContext)) {
484 m_context = atts.value(qualifiedName: QLatin1String("resname")).toString();
485 pushContext(ctx: XC_restype_context);
486 } else {
487 if (atts.value(qualifiedName: QLatin1String("restype")) == QLatin1String(restypePlurals)) {
488 pushContext(ctx: XC_restype_plurals);
489 m_id = atts.value(qualifiedName: QLatin1String("id")).toString();
490 if (atts.value(qualifiedName: QLatin1String("translate")) == QLatin1String("no"))
491 m_translate = false;
492 } else {
493 pushContext(ctx: XC_group);
494 }
495 }
496 } else if (localName == QLatin1String("trans-unit")) {
497 if (!hasContext(ctx: XC_restype_plurals) || m_sources.isEmpty() /* who knows ... */)
498 if (atts.value(qualifiedName: QLatin1String("translate")) == QLatin1String("no"))
499 m_translate = false;
500 if (!hasContext(ctx: XC_restype_plurals)) {
501 m_id = atts.value(qualifiedName: QLatin1String("id")).toString();
502 if (m_id.startsWith(s: QLatin1String("_msg")))
503 m_id.clear();
504 }
505 if (atts.value(qualifiedName: QLatin1String("approved")) != QLatin1String("yes"))
506 m_approved = false;
507 pushContext(ctx: XC_trans_unit);
508 m_hadAlt = false;
509 } else if (localName == QLatin1String("alt-trans")) {
510 pushContext(ctx: XC_alt_trans);
511 } else if (localName == QLatin1String("source")) {
512 m_isPlural = atts.value(qualifiedName: QLatin1String(attribPlural)) == QLatin1String("yes");
513 } else if (localName == QLatin1String("target")) {
514 if (atts.value(qualifiedName: QLatin1String("restype")) != QLatin1String(restypeDummy))
515 pushContext(ctx: XC_restype_translation);
516 } else if (localName == QLatin1String("context-group")) {
517 if (atts.value(qualifiedName: QLatin1String("purpose")) == QLatin1String("location"))
518 pushContext(ctx: XC_context_group);
519 else
520 pushContext(ctx: XC_context_group_any);
521 } else if (currentContext() == XC_context_group && localName == QLatin1String("context")) {
522 const auto ctxtype = atts.value(qualifiedName: QLatin1String("context-type"));
523 if (ctxtype == QLatin1String("linenumber"))
524 pushContext(ctx: XC_context_linenumber);
525 else if (ctxtype == QLatin1String("sourcefile"))
526 pushContext(ctx: XC_context_filename);
527 } else if (currentContext() == XC_context_group_any && localName == QLatin1String("context")) {
528 const auto ctxtype = atts.value(qualifiedName: QLatin1String("context-type"));
529 if (ctxtype == QLatin1String(contextMsgctxt))
530 pushContext(ctx: XC_context_comment);
531 else if (ctxtype == QLatin1String(contextOldMsgctxt))
532 pushContext(ctx: XC_context_old_comment);
533 } else if (localName == QLatin1String("note")) {
534 if (atts.value(qualifiedName: QLatin1String("annotates")) == QLatin1String("source") &&
535 atts.value(qualifiedName: QLatin1String("from")) == QLatin1String("developer"))
536 pushContext(ctx: XC_extra_comment);
537 else
538 pushContext(ctx: XC_translator_comment);
539 } else if (localName == QLatin1String("ph")) {
540 QString ctype = atts.value(qualifiedName: QLatin1String("ctype")).toString();
541 if (ctype.startsWith(s: QLatin1String("x-ch-")))
542 m_ctype = ctype.mid(position: 5);
543 pushContext(ctx: XC_ph);
544 }
545bail:
546 if (currentContext() != XC_ph)
547 accum.clear();
548 return true;
549}
550
551bool XLIFFHandler::endElement(QStringView namespaceURI, QStringView localName,
552 QStringView qName)
553{
554 Q_UNUSED(qName);
555 if (namespaceURI == m_URITT) {
556 if (hasContext(ctx: XC_trans_unit) || hasContext(ctx: XC_restype_plurals))
557 m_extra[localName.toString()] = accum;
558 else
559 m_translator.setExtra(ba: localName.toString(), var: accum);
560 return true;
561 }
562 if (namespaceURI != m_URI && namespaceURI != m_URI12) {
563 return fatalError(line: reader.lineNumber(), column: reader.columnNumber(),
564 message: QLatin1String("Unknown namespace in the XLIFF file"));
565 }
566 //qDebug() << "URI:" << namespaceURI << "QNAME:" << qName;
567 if (localName == QLatin1String("xliff")) {
568 popContext(ctx: XC_xliff);
569 } else if (localName == QLatin1String("source")) {
570 if (hasContext(ctx: XC_alt_trans)) {
571 if (m_isPlural && m_oldSources.isEmpty())
572 m_oldSources.append(t: QString());
573 m_oldSources.append(t: accum);
574 m_hadAlt = true;
575 } else {
576 m_sources.append(t: accum);
577 }
578 } else if (localName == QLatin1String("target")) {
579 if (popContext(ctx: XC_restype_translation)) {
580 accum.replace(before: QChar(Translator::TextVariantSeparator),
581 after: QChar(Translator::BinaryVariantSeparator));
582 m_translations.append(t: accum);
583 }
584 } else if (localName == QLatin1String("context-group")) {
585 if (popContext(ctx: XC_context_group)) {
586 m_refs.append(t: TranslatorMessage::Reference(
587 m_extraFileName.isEmpty() ? m_fileName : m_extraFileName, m_lineNumber));
588 m_extraFileName.clear();
589 m_lineNumber = -1;
590 } else {
591 popContext(ctx: XC_context_group_any);
592 }
593 } else if (localName == QLatin1String("context")) {
594 if (popContext(ctx: XC_context_linenumber)) {
595 bool ok;
596 m_lineNumber = accum.trimmed().toInt(ok: &ok);
597 if (!ok)
598 m_lineNumber = -1;
599 } else if (popContext(ctx: XC_context_filename)) {
600 m_extraFileName = accum;
601 } else if (popContext(ctx: XC_context_comment)) {
602 m_comment = accum;
603 } else if (popContext(ctx: XC_context_old_comment)) {
604 m_oldComment = accum;
605 }
606 } else if (localName == QLatin1String("note")) {
607 if (popContext(ctx: XC_extra_comment))
608 m_extraComment = accum;
609 else if (popContext(ctx: XC_translator_comment))
610 m_translatorComment = accum;
611 } else if (localName == QLatin1String("ph")) {
612 m_ctype.clear();
613 popContext(ctx: XC_ph);
614 } else if (localName == QLatin1String("trans-unit")) {
615 popContext(ctx: XC_trans_unit);
616 if (!m_hadAlt)
617 m_oldSources.append(t: QString());
618 if (!hasContext(ctx: XC_restype_plurals)) {
619 if (!finalizeMessage(isPlural: false)) {
620 return fatalError(line: reader.lineNumber(), column: reader.columnNumber(),
621 message: QLatin1String("Element processing failed"));
622 }
623 }
624 } else if (localName == QLatin1String("alt-trans")) {
625 popContext(ctx: XC_alt_trans);
626 } else if (localName == QLatin1String("group")) {
627 if (popContext(ctx: XC_restype_plurals)) {
628 if (!finalizeMessage(isPlural: true)) {
629 return fatalError(line: reader.lineNumber(), column: reader.columnNumber(),
630 message: QLatin1String("Element processing failed"));
631 }
632 } else if (popContext(ctx: XC_restype_context)) {
633 m_context.clear();
634 } else {
635 popContext(ctx: XC_group);
636 }
637 }
638 return true;
639}
640
641bool XLIFFHandler::characters(QStringView ch)
642{
643 if (currentContext() == XC_ph) {
644 // handle the content of <ph> elements
645 for (int i = 0; i < ch.size(); ++i) {
646 QChar chr = ch.at(n: i);
647 if (accum.endsWith(c: QLatin1Char('\\')))
648 accum[accum.size() - 1] = QLatin1Char(charFromEscape(escape: chr.toLatin1()));
649 else
650 accum.append(c: chr);
651 }
652 } else {
653 QString t = ch.toString();
654 t.replace(before: QLatin1String("\r"), after: QLatin1String(""));
655 accum.append(s: t);
656 }
657 return true;
658}
659
660bool XLIFFHandler::endDocument()
661{
662 m_translator.setLanguageCode(m_language);
663 m_translator.setSourceLanguageCode(m_sourceLanguage);
664 return true;
665}
666
667bool XLIFFHandler::finalizeMessage(bool isPlural)
668{
669 if (m_sources.isEmpty()) {
670 m_cd.appendError(error: QLatin1String("XLIFF syntax error: Message without source string."));
671 return false;
672 }
673 if (!m_translate && m_refs.size() == 1
674 && m_refs.at(i: 0).fileName() == QLatin1String(MAGIC_OBSOLETE_REFERENCE))
675 m_refs.clear();
676 TranslatorMessage::Type type
677 = m_translate ? (m_approved ? TranslatorMessage::Finished : TranslatorMessage::Unfinished)
678 : (m_approved ? TranslatorMessage::Vanished : TranslatorMessage::Obsolete);
679 TranslatorMessage msg(m_context, m_sources[0],
680 m_comment, QString(), QString(), -1,
681 m_translations, type, isPlural);
682 msg.setId(m_id);
683 msg.setReferences(m_refs);
684 msg.setOldComment(m_oldComment);
685 msg.setExtraComment(m_extraComment);
686 msg.setTranslatorComment(m_translatorComment);
687 if (m_sources.size() > 1 && m_sources[1] != m_sources[0])
688 m_extra.insert(key: QLatin1String("po-msgid_plural"), value: m_sources[1]);
689 if (!m_oldSources.isEmpty()) {
690 if (!m_oldSources[0].isEmpty())
691 msg.setOldSourceText(m_oldSources[0]);
692 if (m_oldSources.size() > 1 && m_oldSources[1] != m_oldSources[0])
693 m_extra.insert(key: QLatin1String("po-old_msgid_plural"), value: m_oldSources[1]);
694 }
695 msg.setExtras(m_extra);
696 m_translator.append(msg);
697
698 m_id.clear();
699 m_sources.clear();
700 m_oldSources.clear();
701 m_translations.clear();
702 m_comment.clear();
703 m_oldComment.clear();
704 m_extraComment.clear();
705 m_translatorComment.clear();
706 m_extra.clear();
707 m_refs.clear();
708 m_translate = true;
709 m_approved = true;
710 return true;
711}
712
713bool XLIFFHandler::fatalError(qint64 line, qint64 column, const QString &message)
714{
715 QString msg = QString::asprintf(format: "XML error: Parse error at line %d, column %d (%s).\n",
716 static_cast<int>(line), static_cast<int>(column),
717 message.toLatin1().data());
718 m_cd.appendError(error: msg);
719 return false;
720}
721
722bool loadXLIFF(Translator &translator, QIODevice &dev, ConversionData &cd)
723{
724 QXmlStreamReader reader(&dev);
725 XLIFFHandler hand(translator, cd, reader);
726 return hand.parse();
727}
728
729bool saveXLIFF(const Translator &translator, QIODevice &dev, ConversionData &cd)
730{
731 bool ok = true;
732 int indent = 0;
733
734 QTextStream ts(&dev);
735
736 QStringList dtgs = cd.dropTags();
737 dtgs << QLatin1String("po-(old_)?msgid_plural");
738 QRegularExpression drops(QRegularExpression::anchoredPattern(expression: dtgs.join(sep: QLatin1Char('|'))));
739
740 QHash<QString, QHash<QString, QList<TranslatorMessage> > > messageOrder;
741 QHash<QString, QList<QString> > contextOrder;
742 QList<QString> fileOrder;
743 for (const TranslatorMessage &msg : translator.messages()) {
744 QString fn = msg.fileName();
745 if (fn.isEmpty() && msg.type() == TranslatorMessage::Obsolete)
746 fn = QLatin1String(MAGIC_OBSOLETE_REFERENCE);
747 QHash<QString, QList<TranslatorMessage> > &file = messageOrder[fn];
748 if (file.isEmpty())
749 fileOrder.append(t: fn);
750 QList<TranslatorMessage> &context = file[msg.context()];
751 if (context.isEmpty())
752 contextOrder[fn].append(t: msg.context());
753 context.append(t: msg);
754 }
755
756 ts.setFieldAlignment(QTextStream::AlignRight);
757 ts << "<?xml version=\"1.0\"";
758 ts << " encoding=\"utf-8\"?>\n";
759 ts << "<xliff version=\"1.2\" xmlns=\"" << XLIFF12namespaceURI
760 << "\" xmlns:trolltech=\"" << TrollTsNamespaceURI << "\">\n";
761 ++indent;
762 writeExtras(ts, indent, extras: translator.extras(), drops);
763 QString sourceLanguageCode = translator.sourceLanguageCode();
764 if (sourceLanguageCode.isEmpty() || sourceLanguageCode == QLatin1String("C"))
765 sourceLanguageCode = QLatin1String("en");
766 else
767 sourceLanguageCode.replace(before: QLatin1Char('_'), after: QLatin1Char('-'));
768 QString languageCode = translator.languageCode();
769 languageCode.replace(before: QLatin1Char('_'), after: QLatin1Char('-'));
770 for (const QString &fn : std::as_const(t&: fileOrder)) {
771 writeIndent(ts, indent);
772 ts << "<file original=\"" << fn << "\""
773 << " datatype=\"" << dataType(m: messageOrder[fn].cbegin()->first()) << "\""
774 << " source-language=\"" << sourceLanguageCode.toLatin1() << "\""
775 << " target-language=\"" << languageCode.toLatin1() << "\""
776 << "><body>\n";
777 ++indent;
778
779 for (const QString &ctx : std::as_const(t&: contextOrder[fn])) {
780 if (!ctx.isEmpty()) {
781 writeIndent(ts, indent);
782 ts << "<group restype=\"" << restypeContext << "\""
783 << " resname=\"" << xlProtect(str: ctx) << "\">\n";
784 ++indent;
785 }
786
787 for (const TranslatorMessage &msg : std::as_const(t&: messageOrder[fn][ctx]))
788 writeMessage(ts, msg, drops, indent);
789
790 if (!ctx.isEmpty()) {
791 --indent;
792 writeIndent(ts, indent);
793 ts << "</group>\n";
794 }
795 }
796
797 --indent;
798 writeIndent(ts, indent);
799 ts << "</body></file>\n";
800 }
801 --indent;
802 writeIndent(ts, indent);
803 ts << "</xliff>\n";
804
805 return ok;
806}
807
808int initXLIFF()
809{
810 Translator::FileFormat format;
811 format.extension = QLatin1String("xlf");
812 format.untranslatedDescription = QT_TRANSLATE_NOOP("FMT", "XLIFF localization files");
813 format.fileType = Translator::FileFormat::TranslationSource;
814 format.priority = 1;
815 format.loader = &loadXLIFF;
816 format.saver = &saveXLIFF;
817 Translator::registerFileFormat(format);
818 return 1;
819}
820
821Q_CONSTRUCTOR_FUNCTION(initXLIFF)
822
823QT_END_NAMESPACE
824

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