/**************************************************************************** | |
** | |
** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). | |
** All rights reserved. | |
** Contact: Nokia Corporation (qt-info@nokia.com) | |
** | |
** This file is part of the Qt Linguist of the Qt Toolkit. | |
** | |
** $QT_BEGIN_LICENSE:LGPL$ | |
** GNU Lesser General Public License Usage | |
** This file may be used under the terms of the GNU Lesser General Public | |
** License version 2.1 as published by the Free Software Foundation and | |
** appearing in the file LICENSE.LGPL included in the packaging of this | |
** file. Please review the following information to ensure the GNU Lesser | |
** General Public License version 2.1 requirements will be met: | |
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. | |
** | |
** In addition, as a special exception, Nokia gives you certain additional | |
** rights. These rights are described in the Nokia Qt LGPL Exception | |
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. | |
** | |
** GNU General Public License Usage | |
** Alternatively, this file may be used under the terms of the GNU General | |
** Public License version 3.0 as published by the Free Software Foundation | |
** and appearing in the file LICENSE.GPL included in the packaging of this | |
** file. Please review the following information to ensure the GNU General | |
** Public License version 3.0 requirements will be met: | |
** http://www.gnu.org/copyleft/gpl.html. | |
** | |
** Other Usage | |
** Alternatively, this file may be used in accordance with the terms and | |
** conditions contained in a signed written agreement between you and Nokia. | |
** | |
** | |
** | |
** | |
** | |
** $QT_END_LICENSE$ | |
** | |
****************************************************************************/ | |
#include "translator.h" | |
#include <QtCore/QDebug> | |
#include <QtCore/QMap> | |
#include <QtCore/QStack> | |
#include <QtCore/QString> | |
#include <QtCore/QTextCodec> | |
#include <QtCore/QTextStream> | |
#include <QtXml/QXmlAttributes> | |
#include <QtXml/QXmlDefaultHandler> | |
#include <QtXml/QXmlParseException> | |
// 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().toAscii(); | |
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::fromAscii("&#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::fromAscii("<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, const 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::fromAscii("_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) { | |
if (!msg.isPlural()) | |
attribs = " translate=\"no\""; | |
} else if (msg.type() == TranslatorMessage::Finished) { | |
attribs = " approved=\"yes\""; | |
} else if (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) | |
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 QXmlDefaultHandler | |
{ | |
public: | |
XLIFFHandler(Translator &translator, ConversionData &cd); | |
bool startElement(const QString& namespaceURI, const QString &localName, | |
const QString &qName, const QXmlAttributes &atts ); | |
bool endElement(const QString& namespaceURI, const QString &localName, | |
const QString &qName ); | |
bool characters(const QString &ch); | |
bool fatalError(const QXmlParseException &exception); | |
bool endDocument(); | |
private: | |
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; | |
TranslatorMessage::Type m_type; | |
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_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) | |
: m_translator(translator), m_cd(cd), | |
m_type(TranslatorMessage::Finished), | |
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 QString& namespaceURI, | |
const QString &localName, const QString &qName, const QXmlAttributes &atts ) | |
{ | |
Q_UNUSED(qName); | |
if (namespaceURI == m_URITT) | |
goto bail; | |
if (namespaceURI != m_URI && namespaceURI != m_URI12) | |
return false; | |
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")); | |
m_language = atts.value(QLatin1String("target-language")); | |
m_language.replace(QLatin1Char('-'), QLatin1Char('_')); | |
m_sourceLanguage = atts.value(QLatin1String("source-language")); | |
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")); | |
pushContext(XC_restype_context); | |
} else { | |
if (atts.value(QLatin1String("restype")) == QLatin1String(restypePlurals)) { | |
pushContext(XC_restype_plurals); | |
m_id = atts.value(QLatin1String("id")); | |
if (atts.value(QLatin1String("translate")) == QLatin1String("no")) | |
m_type = TranslatorMessage::Obsolete; | |
} 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_type = TranslatorMessage::Obsolete; | |
if (!hasContext(XC_restype_plurals)) { | |
m_id = atts.value(QLatin1String("id")); | |
if (m_id.startsWith(QLatin1String("_msg"))) | |
m_id.clear(); | |
} | |
if (m_type != TranslatorMessage::Obsolete && | |
atts.value(QLatin1String("approved")) != QLatin1String("yes")) | |
m_type = TranslatorMessage::Unfinished; | |
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")) { | |
QString purpose = atts.value(QLatin1String("purpose")); | |
if (purpose == QLatin1String("location")) | |
pushContext(XC_context_group); | |
else | |
pushContext(XC_context_group_any); | |
} else if (currentContext() == XC_context_group && localName == QLatin1String("context")) { | |
QString 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")) { | |
QString 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")); | |
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 QString &namespaceURI, const QString& localName, | |
const QString &qName) | |
{ | |
Q_UNUSED(qName); | |
if (namespaceURI == m_URITT) { | |
if (hasContext(XC_trans_unit) || hasContext(XC_restype_plurals)) | |
m_extra[localName] = accum; | |
else | |
m_translator.setExtra(localName, accum); | |
return true; | |
} | |
if (namespaceURI != m_URI && namespaceURI != m_URI12) | |
return false; | |
//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 false; | |
} | |
} else if (localName == QLatin1String("alt-trans")) { | |
popContext(XC_alt_trans); | |
} else if (localName == QLatin1String("group")) { | |
if (popContext(XC_restype_plurals)) { | |
if (!finalizeMessage(true)) | |
return false; | |
} else if (popContext(XC_restype_context)) { | |
m_context.clear(); | |
} else { | |
popContext(XC_group); | |
} | |
} | |
return true; | |
} | |
bool XLIFFHandler::characters(const QString &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.toAscii())); | |
else | |
accum.append(chr); | |
} | |
} else { | |
QString t = ch; | |
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_type == TranslatorMessage::Obsolete && m_refs.size() == 1 | |
&& m_refs.at(0).fileName() == QLatin1String(MAGIC_OBSOLETE_REFERENCE)) | |
m_refs.clear(); | |
TranslatorMessage msg(m_context, m_sources[0], | |
m_comment, QString(), QString(), -1, | |
m_translations, m_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_type = TranslatorMessage::Finished; | |
return true; | |
} | |
bool XLIFFHandler::fatalError(const QXmlParseException &exception) | |
{ | |
QString msg; | |
msg.sprintf("XML error: Parse error at line %d, column %d (%s).\n", | |
exception.lineNumber(), exception.columnNumber(), | |
exception.message().toLatin1().data() ); | |
m_cd.appendError(msg); | |
return false; | |
} | |
bool loadXLIFF(Translator &translator, QIODevice &dev, ConversionData &cd) | |
{ | |
QXmlInputSource in(&dev); | |
QXmlSimpleReader reader; | |
XLIFFHandler hand(translator, cd); | |
reader.setContentHandler(&hand); | |
reader.setErrorHandler(&hand); | |
return reader.parse(in); | |
} | |
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(QLatin1String("|"))); | |
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.description = QObject::tr("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 |