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

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