| /**************************************************************************** |
| ** |
| ** 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 <QtCore/QDebug> |
| #include <QtCore/QMap> |
| #include <QtCore/QRegExp> |
| #include <QtCore/QStack> |
| #include <QtCore/QString> |
| #include <QtCore/QTextCodec> |
| #include <QtCore/QTextStream> |
| |
| // 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("<ph id=\"ph%1\" ctype=\"x-ch-%2\">\\%3</ph>") |
| .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 << "<trolltech:" << it.key() << '>' |
| << protect(it.value()) |
| << "</trolltech:" << it.key() << ">\n"; |
| } |
| } |
| } |
| |
| static void writeLineNumber(QTextStream &ts, const TranslatorMessage &msg, int indent) |
| { |
| if (msg.lineNumber() == -1) |
| return; |
| writeIndent(ts, indent); |
| ts << "<context-group purpose=\"location\"><context context-type=\"linenumber\">" |
| << msg.lineNumber() << "</context></context-group>\n"; |
| foreach (const TranslatorMessage::Reference &ref, msg.extraReferences()) { |
| writeIndent(ts, indent); |
| ts << "<context-group purpose=\"location\">"; |
| if (ref.fileName() != msg.fileName()) |
| ts << "<context context-type=\"sourcefile\">" << ref.fileName() << "</context>"; |
| ts << "<context context-type=\"linenumber\">" << ref.lineNumber() |
| << "</context></context-group>\n"; |
| } |
| } |
| |
| static void writeComment(QTextStream &ts, const TranslatorMessage &msg, const QRegExp &drops, int indent) |
| { |
| if (!msg.comment().isEmpty()) { |
| writeIndent(ts, indent); |
| ts << "<context-group><context context-type=\"" << contextMsgctxt << "\">" |
| << protect(msg.comment(), false) |
| << "</context></context-group>\n"; |
| } |
| if (!msg.oldComment().isEmpty()) { |
| writeIndent(ts, indent); |
| ts << "<context-group><context context-type=\"" << contextOldMsgctxt << "\">" |
| << protect(msg.oldComment(), false) |
| << "</context></context-group>\n"; |
| } |
| writeExtras(ts, indent, msg.extras(), drops); |
| if (!msg.extraComment().isEmpty()) { |
| writeIndent(ts, indent); |
| ts << "<note annotates=\"source\" from=\"developer\">" |
| << protect(msg.extraComment()) << "</note>\n"; |
| } |
| if (!msg.translatorComment().isEmpty()) { |
| writeIndent(ts, indent); |
| ts << "<note from=\"translator\">" |
| << protect(msg.translatorComment()) << "</note>\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<QString, QString>::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 << "<trans-unit id=\"" << msgidstr; |
| if (msg.isPlural()) |
| ts << "[" << plural++ << "]"; |
| ts << "\"" << attribs << ">\n"; |
| ++indent; |
| |
| writeIndent(ts, indent); |
| if (srcit != srcend) { |
| source = *srcit; |
| ++srcit; |
| } // else just repeat last element |
| ts << "<source xml:space=\"preserve\">" << protect(source) << "</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 << "<alt-trans>\n"; |
| ++indent; |
| writeIndent(ts, indent); |
| ts << "<source xml:space=\"preserve\"" << pluralStr << '>' << protect(*oldsrcit) << "</source>\n"; |
| if (!puttrans) { |
| writeIndent(ts, indent); |
| ts << "<target restype=\"" << restypeDummy << "\"/>\n"; |
| } |
| } |
| |
| if (puttrans) { |
| writeIndent(ts, indent); |
| ts << "<target xml:space=\"preserve\"" << state << ">" << protect(translation) << "</target>\n"; |
| } |
| |
| if (oldsrcit != oldsrcend) { |
| if (!oldsrcit->isEmpty()) { |
| --indent; |
| writeIndent(ts, indent); |
| ts << "</alt-trans>\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 << "</trans-unit>\n"; |
| } |
| } |
| |
| static void writeMessage(QTextStream &ts, const TranslatorMessage &msg, const QRegExp &drops, int indent) |
| { |
| if (msg.isPlural()) { |
| writeIndent(ts, indent); |
| ts << "<group restype=\"" << restypePlurals << "\""; |
| if (!msg.id().isEmpty()) |
| ts << " id=\"" << msg.id() << "\""; |
| if (msg.type() == TranslatorMessage::Obsolete || msg.type() == TranslatorMessage::Vanished) |
| ts << " translate=\"no\""; |
| ts << ">\n"; |
| ++indent; |
| writeLineNumber(ts, msg, indent); |
| writeComment(ts, msg, drops, indent); |
| |
| writeTransUnits(ts, msg, drops, indent); |
| --indent; |
| writeIndent(ts, indent); |
| ts << "</group>\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<int> 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 <ph> 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<int>(line), static_cast<int>(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<QString, QHash<QString, QList<TranslatorMessage> > > messageOrder; |
| QHash<QString, QList<QString> > contextOrder; |
| QList<QString> fileOrder; |
| foreach (const TranslatorMessage &msg, translator.messages()) { |
| QString fn = msg.fileName(); |
| if (fn.isEmpty() && msg.type() == TranslatorMessage::Obsolete) |
| fn = QLatin1String(MAGIC_OBSOLETE_REFERENCE); |
| QHash<QString, QList<TranslatorMessage> > &file = messageOrder[fn]; |
| if (file.isEmpty()) |
| fileOrder.append(fn); |
| QList<TranslatorMessage> &context = file[msg.context()]; |
| if (context.isEmpty()) |
| contextOrder[fn].append(msg.context()); |
| context.append(msg); |
| } |
| |
| ts.setFieldAlignment(QTextStream::AlignRight); |
| ts << "<?xml version=\"1.0\""; |
| ts << " encoding=\"utf-8\"?>\n"; |
| ts << "<xliff version=\"1.2\" xmlns=\"" << XLIFF12namespaceURI |
| << "\" xmlns:trolltech=\"" << TrollTsNamespaceURI << "\">\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 << "<file original=\"" << fn << "\"" |
| << " datatype=\"" << dataType(messageOrder[fn].begin()->first()) << "\"" |
| << " source-language=\"" << sourceLanguageCode.toLatin1() << "\"" |
| << " target-language=\"" << languageCode.toLatin1() << "\"" |
| << "><body>\n"; |
| ++indent; |
| |
| foreach (const QString &ctx, contextOrder[fn]) { |
| if (!ctx.isEmpty()) { |
| writeIndent(ts, indent); |
| ts << "<group restype=\"" << restypeContext << "\"" |
| << " resname=\"" << protect(ctx) << "\">\n"; |
| ++indent; |
| } |
| |
| foreach (const TranslatorMessage &msg, messageOrder[fn][ctx]) |
| writeMessage(ts, msg, drops, indent); |
| |
| if (!ctx.isEmpty()) { |
| --indent; |
| writeIndent(ts, indent); |
| ts << "</group>\n"; |
| } |
| } |
| |
| --indent; |
| writeIndent(ts, indent); |
| ts << "</body></file>\n"; |
| } |
| --indent; |
| writeIndent(ts, indent); |
| ts << "</xliff>\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 |