7#include <QtCore/QDebug>
9#include <QtCore/QRegularExpression>
10#include <QtCore/QStack>
11#include <QtCore/QString>
12#include <QtCore/QTextStream>
17#define MAGIC_OBSOLETE_REFERENCE "Obsolete_PO_entries"
21using namespace Qt::Literals::StringLiterals;
24
25
38#define COMBINE4CHARS(c1, c2, c3, c4)
39 (int(c1) << 24
| int(c2) << 16
| int(c3) << 8
| int(c4) )
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) ==
'.')
49 extHash |= ((
int)fileName.at(pos) << (8*pass));
69 return "plaintext"_L1;
75 ts << QString().fill(u' ', indent * 2);
109 if (!makePhs || ch < 7 || ch > 0x0d)
110 return QString::fromLatin1(
"&#x%1;").arg(QString::number(ch, 16));
113 QString name = QLatin1String(cm
.mnemonic);
117 return QString::fromLatin1(
"<ph id=\"ph%1\" ctype=\"x-ch-%2\">\\%3</ph>")
118 .arg(++id) .arg(name) .arg(escapechar);
124 int len = str.size();
125 for (
int i = 0; i != len; ++i) {
126 uint c = str.at(i).unicode();
129 result +=
"""_L1;
132 result +=
"&"_L1;
141 result +=
"'"_L1;
144 if (c < 0x20 && c !=
'\r' && c !=
'\n' && c !=
'\t')
145 result += xlNumericEntity(c, makePhs);
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";
171 writeIndent(ts, indent);
172 ts <<
"<context-group purpose=\"location\"><context context-type=\"linenumber\">"
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";
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";
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";
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";
205 if (!msg.translatorComment().isEmpty()) {
206 writeIndent(ts, indent);
207 ts <<
"<note from=\"translator\">"
208 << xlProtect(msg.translatorComment()) <<
"</note>\n";
216 !msg.id().isEmpty() ? xlProtect(msg.id()) :
"_msg"_L1 + QString::number(++msgid);
218 QStringList translns = msg.translations();
220 QStringList sources(msg.sourceText());
222 const auto extrasEnd = extras.cend();
223 if (
const auto it = extras.constFind(QString::fromLatin1(
"po-msgid_plural")); it != extrasEnd)
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());
233 pluralStr = u' ' + QLatin1String(
attribPlural) + QLatin1String(
"=\"yes\"");
235 oldsources.append(*it);
238 auto srcit = sources.cbegin(), srcend = sources.cend(),
239 oldsrcit = oldsources.cbegin(), oldsrcend = oldsources.cend(),
240 transit = translns.cbegin(), transend = translns.cend();
243 while (srcit != srcend || oldsrcit != oldsrcend || transit != transend) {
249 attribs =
" translate=\"no\"";
253 attribs +=
" approved=\"yes\"";
255 && transit != transend && !transit->isEmpty()) {
256 state =
" state=\"needs-review-translation\"";
258 writeIndent(ts, indent);
259 ts <<
"<trans-unit id=\"" << msgidstr;
261 ts <<
"[" << plural++ <<
"]";
262 ts <<
"\"" << attribs <<
">\n";
265 writeIndent(ts, indent);
266 if (srcit != srcend) {
270 ts <<
"<source xml:space=\"preserve\">" << xlProtect(source) <<
"</source>\n";
272 bool puttrans =
false;
274 if (transit != transend) {
275 translation = *transit;
276 translation.replace(Translator::BinaryVariantSeparator, Translator::TextVariantSeparator);
281 if (oldsrcit != oldsrcend && !oldsrcit->isEmpty()) {
282 writeIndent(ts, indent);
283 ts <<
"<alt-trans>\n";
285 writeIndent(ts, indent);
286 ts <<
"<source xml:space=\"preserve\"" << pluralStr <<
'>' << xlProtect(*oldsrcit) <<
"</source>\n";
288 writeIndent(ts, indent);
294 writeIndent(ts, indent);
295 ts <<
"<target xml:space=\"preserve\"" << state <<
">" << xlProtect(translation) <<
"</target>\n";
298 if (oldsrcit != oldsrcend) {
299 if (!oldsrcit->isEmpty()) {
301 writeIndent(ts, indent);
302 ts <<
"</alt-trans>\n";
308 }
while (srcit == srcend && oldsrcit != oldsrcend);
311 writeLineNumber(ts, msg, indent);
312 writeComment(ts, msg, drops, indent);
316 writeIndent(ts, indent);
317 ts <<
"</trans-unit>\n";
324 writeIndent(ts, indent);
326 if (!msg.id().isEmpty())
327 ts <<
" id=\"" << msg.id() <<
"\"";
329 ts <<
" translate=\"no\"";
332 writeLineNumber(ts, msg, indent);
333 writeComment(ts, msg, drops, indent);
335 writeTransUnits(ts, msg, drops, indent);
337 writeIndent(ts, indent);
340 writeTransUnits(ts, msg, drops, indent);
352 QStringView qName,
const QXmlStreamAttributes &atts)
override;
353 bool endElement(QStringView namespaceURI, QStringView localName,
354 QStringView qName)
override;
356 bool fatalError(qint64 line, qint64 column,
const QString &message)
override;
365 XC_context_group_any,
368 XC_context_linenumber,
371 XC_context_old_comment,
374 XC_translator_comment,
376 XC_restype_translation,
377 XC_mtype_seg_translation,
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);
391 QString m_sourceLanguage;
394 QStringList m_sources;
395 QStringList m_oldSources;
397 QString m_oldComment;
398 QString m_extraComment;
399 QString m_translatorComment;
404 QStringList m_translations;
407 QString m_extraFileName;
413 const QString m_URITT;
415 const QString m_URI12;
416 QStack<
int> m_contextStack;
421 m_translator(translator),
434 m_contextStack.push_back(ctx);
440 if (!m_contextStack.isEmpty() && m_contextStack.top() == ctx) {
441 m_contextStack.pop();
449 if (!m_contextStack.isEmpty())
450 return (XliffContext)m_contextStack.top();
457 for (
int i = m_contextStack.size() - 1; i >= 0; --i) {
458 if (m_contextStack.at(i) == ctx)
465 QStringView qName,
const QXmlStreamAttributes &atts)
468 if (namespaceURI == m_URITT)
470 if (namespaceURI != m_URI && namespaceURI != m_URI12) {
471 return fatalError(reader.lineNumber(), reader.columnNumber(),
472 "Unknown namespace in the XLIFF file"_L1);
474 if (localName ==
"xliff"_L1) {
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);
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)
496 pushContext(XC_group);
499 }
else if (localName ==
"trans-unit"_L1) {
500 if (!hasContext(XC_restype_plurals) || m_sources.isEmpty() )
501 if (atts.value(
"translate"_L1) ==
"no"_L1)
503 if (!hasContext(XC_restype_plurals)) {
504 m_id = atts.value(
"id"_L1).toString();
505 if (m_id.startsWith(
"_msg"_L1))
508 if (atts.value(
"approved"_L1) !=
"yes"_L1)
510 pushContext(XC_trans_unit);
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);
524 }
else if (localName ==
"context-group"_L1) {
525 if (atts.value(
"purpose"_L1) ==
"location"_L1)
526 pushContext(XC_context_group);
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);
538 pushContext(XC_context_comment);
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);
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);
553 if (currentContext() != XC_ph && currentContext() != XC_mtype_seg_translation)
562 if (namespaceURI == m_URITT) {
563 if (hasContext(XC_trans_unit) || hasContext(XC_restype_plurals))
564 m_extra[localName.toString()] = accum;
566 m_translator.setExtra(localName.toString(), accum);
569 if (namespaceURI != m_URI && namespaceURI != m_URI12) {
570 return fatalError(reader.lineNumber(), reader.columnNumber(),
571 "Unknown namespace in the XLIFF file"_L1);
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);
583 m_sources.append(accum);
585 }
else if (localName ==
"target"_L1) {
586 if (popContext(XC_restype_translation)) {
587 accum.replace(Translator::TextVariantSeparator, Translator::BinaryVariantSeparator);
588 m_translations.append(accum);
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();
599 popContext(XC_context_group_any);
601 }
else if (localName ==
"context"_L1) {
602 if (popContext(XC_context_linenumber)) {
604 m_lineNumber = accum.trimmed().toInt(&ok);
607 }
else if (popContext(XC_context_filename)) {
608 m_extraFileName = accum;
609 }
else if (popContext(XC_context_comment)) {
611 }
else if (popContext(XC_context_old_comment)) {
612 m_oldComment = accum;
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) {
622 }
else if (localName ==
"trans-unit"_L1) {
623 popContext(XC_trans_unit);
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);
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);
640 }
else if (popContext(XC_restype_context)) {
643 popContext(XC_group);
651 if (currentContext() == XC_ph) {
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()));
661 QString t = ch.toString();
670 m_translator.setLanguageCode(m_language);
671 m_translator.setSourceLanguageCode(m_sourceLanguage);
677 if (m_sources.isEmpty()) {
678 m_cd.appendError(
"XLIFF syntax error: Message without source string."_L1);
681 if (!m_translate && m_refs.size() == 1
688 m_comment, QString(), QString(), -1,
689 m_translations, type, isPlural);
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]);
709 m_oldSources.clear();
710 m_translations.clear();
712 m_oldComment.clear();
713 m_extraComment.clear();
714 m_translatorComment.clear();
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);
733 QXmlStreamReader reader(&dev);
743 QTextStream ts(&dev);
745 QStringList dtgs = cd.dropTags();
746 dtgs <<
"po-(old_)?msgid_plural"_L1;
747 QRegularExpression drops(QRegularExpression::anchoredPattern(dtgs.join(u'|')));
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)
756 QHash<QString, QList<TranslatorMessage> > &file = messageOrder[fn];
758 fileOrder.append(fn);
759 QList<TranslatorMessage> &context = file[msg.context()];
760 if (context.isEmpty())
761 contextOrder[fn].append(msg.context());
765 ts.setFieldAlignment(QTextStream::AlignRight);
766 ts <<
"<?xml version=\"1.0\"";
767 ts <<
" encoding=\"utf-8\"?>\n";
771 writeExtras(ts, indent, translator
.extras(), drops);
772 QString sourceLanguageCode = translator.sourceLanguageCode();
773 if (sourceLanguageCode.isEmpty() || sourceLanguageCode ==
"C"_L1)
774 sourceLanguageCode =
"en"_L1;
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() <<
"\""
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";
796 for (
const TranslatorMessage &msg : std::as_const(messageOrder[fn][ctx]))
797 writeMessage(ts, msg, drops, indent);
799 if (!ctx.isEmpty()) {
801 writeIndent(ts, indent);
807 writeIndent(ts, indent);
808 ts <<
"</body></file>\n";
811 writeIndent(ts, indent);
820 format.extension =
"xlf"_L1;
830Q_CONSTRUCTOR_FUNCTION(initXLIFF)
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
bool endDocument() override
bool endElement(QStringView namespaceURI, QStringView localName, QStringView qName) override
~XLIFFHandler() override=default
bool characters(QStringView ch) override
XLIFFHandler(Translator &translator, ConversionData &cd, QXmlStreamReader &reader)
bool fatalError(qint64 line, qint64 column, const QString &message) override
bool startElement(QStringView namespaceURI, QStringView localName, QStringView qName, const QXmlStreamAttributes &atts) override
Combined button and popup list for selecting options.
static const char * restypeDummy
bool loadXLIFF(Translator &translator, QIODevice &dev, ConversionData &cd)
#define COMBINE4CHARS(c1, c2, c3, c4)
static char charFromEscape(char escape)
static const char * restypePlurals
static const char * XLIFF12namespaceURI
static const char * XLIFF11namespaceURI
static const char * dataTypeUIFile
static const char * contextMsgctxt
static const CharMnemonic charCodeMnemonics[]
#define MAGIC_OBSOLETE_REFERENCE
static void writeIndent(QTextStream &ts, int indent)
static void writeExtras(QTextStream &ts, int indent, const TranslatorMessage::ExtraData &extras, QRegularExpression drops)
static void writeComment(QTextStream &ts, const TranslatorMessage &msg, const QRegularExpression &drops, int indent)
static QString xlNumericEntity(int ch, bool makePhs)
static void writeMessage(QTextStream &ts, const TranslatorMessage &msg, const QRegularExpression &drops, int indent)
bool saveXLIFF(const Translator &translator, QIODevice &dev, ConversionData &cd)
static QString dataType(const TranslatorMessage &m)
static const char * attribPlural
static const char * contextOldMsgctxt
static void writeTransUnits(QTextStream &ts, const TranslatorMessage &msg, const QRegularExpression &drops, int indent)
static const char * TrollTsNamespaceURI
static const char * restypeContext
static QString xlProtect(const QString &str, bool makePhs=true)
static void writeLineNumber(QTextStream &ts, const TranslatorMessage &msg, int indent)