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