/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the Qt Linguist of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "translator.h" #include "xmlparser.h" #include #include #include #include #include #include #include // The string value is historical and reflects the main purpose: Keeping // obsolete entries separate from the magic file message (which both have // no location information, but typically reside at opposite ends of the file). #define MAGIC_OBSOLETE_REFERENCE "Obsolete_PO_entries" QT_BEGIN_NAMESPACE /** * Implementation of XLIFF file format for Linguist */ //static const char *restypeDomain = "x-gettext-domain"; static const char *restypeContext = "x-trolltech-linguist-context"; static const char *restypePlurals = "x-gettext-plurals"; static const char *restypeDummy = "x-dummy"; static const char *dataTypeUIFile = "x-trolltech-designer-ui"; static const char *contextMsgctxt = "x-gettext-msgctxt"; // XXX Troll invention, so far. static const char *contextOldMsgctxt = "x-gettext-previous-msgctxt"; // XXX Troll invention, so far. static const char *attribPlural = "trolltech:plural"; static const char *XLIFF11namespaceURI = "urn:oasis:names:tc:xliff:document:1.1"; static const char *XLIFF12namespaceURI = "urn:oasis:names:tc:xliff:document:1.2"; static const char *TrollTsNamespaceURI = "urn:trolltech:names:ts:document:1.0"; #define COMBINE4CHARS(c1, c2, c3, c4) \ (int(c1) << 24 | int(c2) << 16 | int(c3) << 8 | int(c4) ) static QString dataType(const TranslatorMessage &m) { QByteArray fileName = m.fileName().toLatin1(); unsigned int extHash = 0; int pos = fileName.count() - 1; for (int pass = 0; pass < 4 && pos >=0; ++pass, --pos) { if (fileName.at(pos) == '.') break; extHash |= ((int)fileName.at(pos) << (8*pass)); } switch (extHash) { case COMBINE4CHARS(0,'c','p','p'): case COMBINE4CHARS(0,'c','x','x'): case COMBINE4CHARS(0,'c','+','+'): case COMBINE4CHARS(0,'h','p','p'): case COMBINE4CHARS(0,'h','x','x'): case COMBINE4CHARS(0,'h','+','+'): return QLatin1String("cpp"); case COMBINE4CHARS(0, 0 , 0 ,'c'): case COMBINE4CHARS(0, 0 , 0 ,'h'): case COMBINE4CHARS(0, 0 ,'c','c'): case COMBINE4CHARS(0, 0 ,'c','h'): case COMBINE4CHARS(0, 0 ,'h','h'): return QLatin1String("c"); case COMBINE4CHARS(0, 0 ,'u','i'): return QLatin1String(dataTypeUIFile); //### form? default: return QLatin1String("plaintext"); // we give up } } static void writeIndent(QTextStream &ts, int indent) { ts << QString().fill(QLatin1Char(' '), indent * 2); } struct CharMnemonic { char ch; char escape; const char *mnemonic; }; static const CharMnemonic charCodeMnemonics[] = { {0x07, 'a', "bel"}, {0x08, 'b', "bs"}, {0x09, 't', "tab"}, {0x0a, 'n', "lf"}, {0x0b, 'v', "vt"}, {0x0c, 'f', "ff"}, {0x0d, 'r', "cr"} }; static char charFromEscape(char escape) { for (uint i = 0; i < sizeof(charCodeMnemonics)/sizeof(CharMnemonic); ++i) { CharMnemonic cm = charCodeMnemonics[i]; if (cm.escape == escape) return cm.ch; } Q_ASSERT(0); return escape; } static QString numericEntity(int ch, bool makePhs) { // ### This needs to be reviewed, to reflect the updated XLIFF-PO spec. if (!makePhs || ch < 7 || ch > 0x0d) return QString::fromLatin1("&#x%1;").arg(QString::number(ch, 16)); CharMnemonic cm = charCodeMnemonics[int(ch) - 7]; QString name = QLatin1String(cm.mnemonic); char escapechar = cm.escape; static int id = 0; return QString::fromLatin1("\\%3") .arg(++id) .arg(name) .arg(escapechar); } static QString protect(const QString &str, bool makePhs = true) { QString result; int len = str.size(); for (int i = 0; i != len; ++i) { uint c = str.at(i).unicode(); switch (c) { case '\"': result += QLatin1String("""); break; case '&': result += QLatin1String("&"); break; case '>': result += QLatin1String(">"); break; case '<': result += QLatin1String("<"); break; case '\'': result += QLatin1String("'"); break; default: if (c < 0x20 && c != '\r' && c != '\n' && c != '\t') result += numericEntity(c, makePhs); else // this also covers surrogates result += QChar(c); } } return result; } static void writeExtras(QTextStream &ts, int indent, const TranslatorMessage::ExtraData &extras, QRegExp drops) { for (Translator::ExtraData::ConstIterator it = extras.begin(); it != extras.end(); ++it) { if (!drops.exactMatch(it.key())) { writeIndent(ts, indent); ts << "' << protect(it.value()) << "\n"; } } } static void writeLineNumber(QTextStream &ts, const TranslatorMessage &msg, int indent) { if (msg.lineNumber() == -1) return; writeIndent(ts, indent); ts << "" << msg.lineNumber() << "\n"; foreach (const TranslatorMessage::Reference &ref, msg.extraReferences()) { writeIndent(ts, indent); ts << ""; if (ref.fileName() != msg.fileName()) ts << "" << ref.fileName() << ""; ts << "" << ref.lineNumber() << "\n"; } } static void writeComment(QTextStream &ts, const TranslatorMessage &msg, const QRegExp &drops, int indent) { if (!msg.comment().isEmpty()) { writeIndent(ts, indent); ts << "" << protect(msg.comment(), false) << "\n"; } if (!msg.oldComment().isEmpty()) { writeIndent(ts, indent); ts << "" << protect(msg.oldComment(), false) << "\n"; } writeExtras(ts, indent, msg.extras(), drops); if (!msg.extraComment().isEmpty()) { writeIndent(ts, indent); ts << "" << protect(msg.extraComment()) << "\n"; } if (!msg.translatorComment().isEmpty()) { writeIndent(ts, indent); ts << "" << protect(msg.translatorComment()) << "\n"; } } static void writeTransUnits(QTextStream &ts, const TranslatorMessage &msg, const QRegExp &drops, int indent) { static int msgid; QString msgidstr = !msg.id().isEmpty() ? msg.id() : QString::fromLatin1("_msg%1").arg(++msgid); QStringList translns = msg.translations(); QHash::const_iterator it; QString pluralStr; QStringList sources(msg.sourceText()); if ((it = msg.extras().find(QString::fromLatin1("po-msgid_plural"))) != msg.extras().end()) sources.append(*it); QStringList oldsources; if (!msg.oldSourceText().isEmpty()) oldsources.append(msg.oldSourceText()); if ((it = msg.extras().find(QString::fromLatin1("po-old_msgid_plural"))) != msg.extras().end()) { if (oldsources.isEmpty()) { if (sources.count() == 2) oldsources.append(QString()); else pluralStr = QLatin1Char(' ') + QLatin1String(attribPlural) + QLatin1String("=\"yes\""); } oldsources.append(*it); } QStringList::const_iterator srcit = sources.begin(), srcend = sources.end(), oldsrcit = oldsources.begin(), oldsrcend = oldsources.end(), transit = translns.begin(), transend = translns.end(); int plural = 0; QString source; while (srcit != srcend || oldsrcit != oldsrcend || transit != transend) { QByteArray attribs; QByteArray state; if ((msg.type() == TranslatorMessage::Obsolete || msg.type() == TranslatorMessage::Vanished) && !msg.isPlural()) { attribs = " translate=\"no\""; } if (msg.type() == TranslatorMessage::Finished || msg.type() == TranslatorMessage::Vanished) { attribs += " approved=\"yes\""; } else if (msg.type() == TranslatorMessage::Unfinished && transit != transend && !transit->isEmpty()) { state = " state=\"needs-review-translation\""; } writeIndent(ts, indent); ts << "\n"; ++indent; writeIndent(ts, indent); if (srcit != srcend) { source = *srcit; ++srcit; } // else just repeat last element ts << "" << protect(source) << "\n"; bool puttrans = false; QString translation; if (transit != transend) { translation = *transit; translation.replace(QChar(Translator::BinaryVariantSeparator), QChar(Translator::TextVariantSeparator)); ++transit; puttrans = true; } do { if (oldsrcit != oldsrcend && !oldsrcit->isEmpty()) { writeIndent(ts, indent); ts << "\n"; ++indent; writeIndent(ts, indent); ts << "' << protect(*oldsrcit) << "\n"; if (!puttrans) { writeIndent(ts, indent); ts << "\n"; } } if (puttrans) { writeIndent(ts, indent); ts << "" << protect(translation) << "\n"; } if (oldsrcit != oldsrcend) { if (!oldsrcit->isEmpty()) { --indent; writeIndent(ts, indent); ts << "\n"; } ++oldsrcit; } puttrans = false; } while (srcit == srcend && oldsrcit != oldsrcend); if (!msg.isPlural()) { writeLineNumber(ts, msg, indent); writeComment(ts, msg, drops, indent); } --indent; writeIndent(ts, indent); ts << "\n"; } } static void writeMessage(QTextStream &ts, const TranslatorMessage &msg, const QRegExp &drops, int indent) { if (msg.isPlural()) { writeIndent(ts, indent); ts << "\n"; ++indent; writeLineNumber(ts, msg, indent); writeComment(ts, msg, drops, indent); writeTransUnits(ts, msg, drops, indent); --indent; writeIndent(ts, indent); ts << "\n"; } else { writeTransUnits(ts, msg, drops, indent); } } class XLIFFHandler : public XmlParser { public: XLIFFHandler(Translator &translator, ConversionData &cd, QXmlStreamReader &reader); ~XLIFFHandler() override = default; private: bool startElement(const QStringRef &namespaceURI, const QStringRef &localName, const QStringRef &qName, const QXmlStreamAttributes &atts) override; bool endElement(const QStringRef &namespaceURI, const QStringRef &localName, const QStringRef &qName) override; bool characters(const QStringRef &ch) override; bool fatalError(qint64 line, qint64 column, const QString &message) override; bool endDocument() override; enum XliffContext { XC_xliff, XC_group, XC_trans_unit, XC_context_group, XC_context_group_any, XC_context, XC_context_filename, XC_context_linenumber, XC_context_context, XC_context_comment, XC_context_old_comment, XC_ph, XC_extra_comment, XC_translator_comment, XC_restype_context, XC_restype_translation, XC_restype_plurals, XC_alt_trans }; void pushContext(XliffContext ctx); bool popContext(XliffContext ctx); XliffContext currentContext() const; bool hasContext(XliffContext ctx) const; bool finalizeMessage(bool isPlural); private: Translator &m_translator; ConversionData &m_cd; QString m_language; QString m_sourceLanguage; QString m_context; QString m_id; QStringList m_sources; QStringList m_oldSources; QString m_comment; QString m_oldComment; QString m_extraComment; QString m_translatorComment; bool m_translate; bool m_approved; bool m_isPlural; bool m_hadAlt; QStringList m_translations; QString m_fileName; int m_lineNumber; QString m_extraFileName; TranslatorMessage::References m_refs; TranslatorMessage::ExtraData m_extra; QString accum; QString m_ctype; const QString m_URITT; // convenience and efficiency const QString m_URI; // ... const QString m_URI12; // ... QStack m_contextStack; }; XLIFFHandler::XLIFFHandler(Translator &translator, ConversionData &cd, QXmlStreamReader &reader) : XmlParser(reader, true), m_translator(translator), m_cd(cd), m_translate(true), m_approved(true), m_lineNumber(-1), m_URITT(QLatin1String(TrollTsNamespaceURI)), m_URI(QLatin1String(XLIFF11namespaceURI)), m_URI12(QLatin1String(XLIFF12namespaceURI)) {} void XLIFFHandler::pushContext(XliffContext ctx) { m_contextStack.push_back(ctx); } // Only pops it off if the top of the stack contains ctx bool XLIFFHandler::popContext(XliffContext ctx) { if (!m_contextStack.isEmpty() && m_contextStack.top() == ctx) { m_contextStack.pop(); return true; } return false; } XLIFFHandler::XliffContext XLIFFHandler::currentContext() const { if (!m_contextStack.isEmpty()) return (XliffContext)m_contextStack.top(); return XC_xliff; } // traverses to the top to check all of the parent contexes. bool XLIFFHandler::hasContext(XliffContext ctx) const { for (int i = m_contextStack.count() - 1; i >= 0; --i) { if (m_contextStack.at(i) == ctx) return true; } return false; } bool XLIFFHandler::startElement(const QStringRef &namespaceURI, const QStringRef &localName, const QStringRef &qName, const QXmlStreamAttributes &atts) { Q_UNUSED(qName); if (namespaceURI == m_URITT) goto bail; if (namespaceURI != m_URI && namespaceURI != m_URI12) { return fatalError(reader.lineNumber(), reader.columnNumber(), QLatin1String("Unknown namespace in the XLIFF file")); } if (localName == QLatin1String("xliff")) { // make sure that the stack is not empty during parsing pushContext(XC_xliff); } else if (localName == QLatin1String("file")) { m_fileName = atts.value(QLatin1String("original")).toString(); m_language = atts.value(QLatin1String("target-language")).toString(); m_language.replace(QLatin1Char('-'), QLatin1Char('_')); m_sourceLanguage = atts.value(QLatin1String("source-language")).toString(); m_sourceLanguage.replace(QLatin1Char('-'), QLatin1Char('_')); if (m_sourceLanguage == QLatin1String("en")) m_sourceLanguage.clear(); } else if (localName == QLatin1String("group")) { if (atts.value(QLatin1String("restype")) == QLatin1String(restypeContext)) { m_context = atts.value(QLatin1String("resname")).toString(); pushContext(XC_restype_context); } else { if (atts.value(QLatin1String("restype")) == QLatin1String(restypePlurals)) { pushContext(XC_restype_plurals); m_id = atts.value(QLatin1String("id")).toString(); if (atts.value(QLatin1String("translate")) == QLatin1String("no")) m_translate = false; } else { pushContext(XC_group); } } } else if (localName == QLatin1String("trans-unit")) { if (!hasContext(XC_restype_plurals) || m_sources.isEmpty() /* who knows ... */) if (atts.value(QLatin1String("translate")) == QLatin1String("no")) m_translate = false; if (!hasContext(XC_restype_plurals)) { m_id = atts.value(QLatin1String("id")).toString(); if (m_id.startsWith(QLatin1String("_msg"))) m_id.clear(); } if (atts.value(QLatin1String("approved")) != QLatin1String("yes")) m_approved = false; pushContext(XC_trans_unit); m_hadAlt = false; } else if (localName == QLatin1String("alt-trans")) { pushContext(XC_alt_trans); } else if (localName == QLatin1String("source")) { m_isPlural = atts.value(QLatin1String(attribPlural)) == QLatin1String("yes"); } else if (localName == QLatin1String("target")) { if (atts.value(QLatin1String("restype")) != QLatin1String(restypeDummy)) pushContext(XC_restype_translation); } else if (localName == QLatin1String("context-group")) { if (atts.value(QLatin1String("purpose")) == QLatin1String("location")) pushContext(XC_context_group); else pushContext(XC_context_group_any); } else if (currentContext() == XC_context_group && localName == QLatin1String("context")) { const auto ctxtype = atts.value(QLatin1String("context-type")); if (ctxtype == QLatin1String("linenumber")) pushContext(XC_context_linenumber); else if (ctxtype == QLatin1String("sourcefile")) pushContext(XC_context_filename); } else if (currentContext() == XC_context_group_any && localName == QLatin1String("context")) { const auto ctxtype = atts.value(QLatin1String("context-type")); if (ctxtype == QLatin1String(contextMsgctxt)) pushContext(XC_context_comment); else if (ctxtype == QLatin1String(contextOldMsgctxt)) pushContext(XC_context_old_comment); } else if (localName == QLatin1String("note")) { if (atts.value(QLatin1String("annotates")) == QLatin1String("source") && atts.value(QLatin1String("from")) == QLatin1String("developer")) pushContext(XC_extra_comment); else pushContext(XC_translator_comment); } else if (localName == QLatin1String("ph")) { QString ctype = atts.value(QLatin1String("ctype")).toString(); if (ctype.startsWith(QLatin1String("x-ch-"))) m_ctype = ctype.mid(5); pushContext(XC_ph); } bail: if (currentContext() != XC_ph) accum.clear(); return true; } bool XLIFFHandler::endElement(const QStringRef &namespaceURI, const QStringRef &localName, const QStringRef &qName) { Q_UNUSED(qName); if (namespaceURI == m_URITT) { if (hasContext(XC_trans_unit) || hasContext(XC_restype_plurals)) m_extra[localName.toString()] = accum; else m_translator.setExtra(localName.toString(), accum); return true; } if (namespaceURI != m_URI && namespaceURI != m_URI12) { return fatalError(reader.lineNumber(), reader.columnNumber(), QLatin1String("Unknown namespace in the XLIFF file")); } //qDebug() << "URI:" << namespaceURI << "QNAME:" << qName; if (localName == QLatin1String("xliff")) { popContext(XC_xliff); } else if (localName == QLatin1String("source")) { if (hasContext(XC_alt_trans)) { if (m_isPlural && m_oldSources.isEmpty()) m_oldSources.append(QString()); m_oldSources.append(accum); m_hadAlt = true; } else { m_sources.append(accum); } } else if (localName == QLatin1String("target")) { if (popContext(XC_restype_translation)) { accum.replace(QChar(Translator::TextVariantSeparator), QChar(Translator::BinaryVariantSeparator)); m_translations.append(accum); } } else if (localName == QLatin1String("context-group")) { if (popContext(XC_context_group)) { m_refs.append(TranslatorMessage::Reference( m_extraFileName.isEmpty() ? m_fileName : m_extraFileName, m_lineNumber)); m_extraFileName.clear(); m_lineNumber = -1; } else { popContext(XC_context_group_any); } } else if (localName == QLatin1String("context")) { if (popContext(XC_context_linenumber)) { bool ok; m_lineNumber = accum.trimmed().toInt(&ok); if (!ok) m_lineNumber = -1; } else if (popContext(XC_context_filename)) { m_extraFileName = accum; } else if (popContext(XC_context_comment)) { m_comment = accum; } else if (popContext(XC_context_old_comment)) { m_oldComment = accum; } } else if (localName == QLatin1String("note")) { if (popContext(XC_extra_comment)) m_extraComment = accum; else if (popContext(XC_translator_comment)) m_translatorComment = accum; } else if (localName == QLatin1String("ph")) { m_ctype.clear(); popContext(XC_ph); } else if (localName == QLatin1String("trans-unit")) { popContext(XC_trans_unit); if (!m_hadAlt) m_oldSources.append(QString()); if (!hasContext(XC_restype_plurals)) { if (!finalizeMessage(false)) { return fatalError(reader.lineNumber(), reader.columnNumber(), QLatin1String("Element processing failed")); } } } else if (localName == QLatin1String("alt-trans")) { popContext(XC_alt_trans); } else if (localName == QLatin1String("group")) { if (popContext(XC_restype_plurals)) { if (!finalizeMessage(true)) { return fatalError(reader.lineNumber(), reader.columnNumber(), QLatin1String("Element processing failed")); } } else if (popContext(XC_restype_context)) { m_context.clear(); } else { popContext(XC_group); } } return true; } bool XLIFFHandler::characters(const QStringRef &ch) { if (currentContext() == XC_ph) { // handle the content of elements for (int i = 0; i < ch.count(); ++i) { QChar chr = ch.at(i); if (accum.endsWith(QLatin1Char('\\'))) accum[accum.size() - 1] = QLatin1Char(charFromEscape(chr.toLatin1())); else accum.append(chr); } } else { QString t = ch.toString(); t.replace(QLatin1String("\r"), QLatin1String("")); accum.append(t); } return true; } bool XLIFFHandler::endDocument() { m_translator.setLanguageCode(m_language); m_translator.setSourceLanguageCode(m_sourceLanguage); return true; } bool XLIFFHandler::finalizeMessage(bool isPlural) { if (m_sources.isEmpty()) { m_cd.appendError(QLatin1String("XLIFF syntax error: Message without source string.")); return false; } if (!m_translate && m_refs.size() == 1 && m_refs.at(0).fileName() == QLatin1String(MAGIC_OBSOLETE_REFERENCE)) m_refs.clear(); TranslatorMessage::Type type = m_translate ? (m_approved ? TranslatorMessage::Finished : TranslatorMessage::Unfinished) : (m_approved ? TranslatorMessage::Vanished : TranslatorMessage::Obsolete); TranslatorMessage msg(m_context, m_sources[0], m_comment, QString(), QString(), -1, m_translations, type, isPlural); msg.setId(m_id); msg.setReferences(m_refs); msg.setOldComment(m_oldComment); msg.setExtraComment(m_extraComment); msg.setTranslatorComment(m_translatorComment); if (m_sources.count() > 1 && m_sources[1] != m_sources[0]) m_extra.insert(QLatin1String("po-msgid_plural"), m_sources[1]); if (!m_oldSources.isEmpty()) { if (!m_oldSources[0].isEmpty()) msg.setOldSourceText(m_oldSources[0]); if (m_oldSources.count() > 1 && m_oldSources[1] != m_oldSources[0]) m_extra.insert(QLatin1String("po-old_msgid_plural"), m_oldSources[1]); } msg.setExtras(m_extra); m_translator.append(msg); m_id.clear(); m_sources.clear(); m_oldSources.clear(); m_translations.clear(); m_comment.clear(); m_oldComment.clear(); m_extraComment.clear(); m_translatorComment.clear(); m_extra.clear(); m_refs.clear(); m_translate = true; m_approved = true; return true; } bool XLIFFHandler::fatalError(qint64 line, qint64 column, const QString &message) { QString msg = QString::asprintf("XML error: Parse error at line %d, column %d (%s).\n", static_cast(line), static_cast(column), message.toLatin1().data()); m_cd.appendError(msg); return false; } bool loadXLIFF(Translator &translator, QIODevice &dev, ConversionData &cd) { QXmlStreamReader reader(&dev); XLIFFHandler hand(translator, cd, reader); return hand.parse(); } bool saveXLIFF(const Translator &translator, QIODevice &dev, ConversionData &cd) { bool ok = true; int indent = 0; QTextStream ts(&dev); ts.setCodec(QTextCodec::codecForName("UTF-8")); QStringList dtgs = cd.dropTags(); dtgs << QLatin1String("po-(old_)?msgid_plural"); QRegExp drops(dtgs.join(QLatin1Char('|'))); QHash > > messageOrder; QHash > contextOrder; QList fileOrder; foreach (const TranslatorMessage &msg, translator.messages()) { QString fn = msg.fileName(); if (fn.isEmpty() && msg.type() == TranslatorMessage::Obsolete) fn = QLatin1String(MAGIC_OBSOLETE_REFERENCE); QHash > &file = messageOrder[fn]; if (file.isEmpty()) fileOrder.append(fn); QList &context = file[msg.context()]; if (context.isEmpty()) contextOrder[fn].append(msg.context()); context.append(msg); } ts.setFieldAlignment(QTextStream::AlignRight); ts << "\n"; ts << "\n"; ++indent; writeExtras(ts, indent, translator.extras(), drops); QString sourceLanguageCode = translator.sourceLanguageCode(); if (sourceLanguageCode.isEmpty() || sourceLanguageCode == QLatin1String("C")) sourceLanguageCode = QLatin1String("en"); else sourceLanguageCode.replace(QLatin1Char('_'), QLatin1Char('-')); QString languageCode = translator.languageCode(); languageCode.replace(QLatin1Char('_'), QLatin1Char('-')); foreach (const QString &fn, fileOrder) { writeIndent(ts, indent); ts << "first()) << "\"" << " source-language=\"" << sourceLanguageCode.toLatin1() << "\"" << " target-language=\"" << languageCode.toLatin1() << "\"" << ">\n"; ++indent; foreach (const QString &ctx, contextOrder[fn]) { if (!ctx.isEmpty()) { writeIndent(ts, indent); ts << "\n"; ++indent; } foreach (const TranslatorMessage &msg, messageOrder[fn][ctx]) writeMessage(ts, msg, drops, indent); if (!ctx.isEmpty()) { --indent; writeIndent(ts, indent); ts << "\n"; } } --indent; writeIndent(ts, indent); ts << "\n"; } --indent; writeIndent(ts, indent); ts << "\n"; return ok; } int initXLIFF() { Translator::FileFormat format; format.extension = QLatin1String("xlf"); format.untranslatedDescription = QT_TRANSLATE_NOOP("FMT", "XLIFF localization files"); format.fileType = Translator::FileFormat::TranslationSource; format.priority = 1; format.loader = &loadXLIFF; format.saver = &saveXLIFF; Translator::registerFileFormat(format); return 1; } Q_CONSTRUCTOR_FUNCTION(initXLIFF) QT_END_NAMESPACE