source: trunk/tools/linguist/shared/xliff.cpp@ 385

Last change on this file since 385 was 2, checked in by Dmitry A. Kuminov, 16 years ago

Initially imported qt-all-opensource-src-4.5.1 from Trolltech.

File size: 28.8 KB
Line 
1/****************************************************************************
2**
3** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies).
4** Contact: Qt Software Information (qt-info@nokia.com)
5**
6** This file is part of the Qt Linguist of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial Usage
10** Licensees holding valid Qt Commercial licenses may use this file in
11** accordance with the Qt Commercial License Agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and Nokia.
14**
15** GNU Lesser General Public License Usage
16** Alternatively, this file may be used under the terms of the GNU Lesser
17** General Public License version 2.1 as published by the Free Software
18** Foundation and appearing in the file LICENSE.LGPL included in the
19** packaging of this file. Please review the following information to
20** ensure the GNU Lesser General Public License version 2.1 requirements
21** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
22**
23** In addition, as a special exception, Nokia gives you certain
24** additional rights. These rights are described in the Nokia Qt LGPL
25** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this
26** package.
27**
28** GNU General Public License Usage
29** Alternatively, this file may be used under the terms of the GNU
30** General Public License version 3.0 as published by the Free Software
31** Foundation and appearing in the file LICENSE.GPL included in the
32** packaging of this file. Please review the following information to
33** ensure the GNU General Public License version 3.0 requirements will be
34** met: http://www.gnu.org/copyleft/gpl.html.
35**
36** If you are unsure which license is appropriate for your use, please
37** contact the sales department at qt-sales@nokia.com.
38** $QT_END_LICENSE$
39**
40****************************************************************************/
41
42#include "translator.h"
43
44#include <QtCore/QDebug>
45#include <QtCore/QMap>
46#include <QtCore/QStack>
47#include <QtCore/QString>
48#include <QtCore/QTextCodec>
49#include <QtCore/QTextStream>
50
51#include <QtXml/QXmlAttributes>
52#include <QtXml/QXmlDefaultHandler>
53#include <QtXml/QXmlParseException>
54
55
56QT_BEGIN_NAMESPACE
57
58/**
59 * Implementation of XLIFF file format for Linguist
60 */
61//static const char *restypeDomain = "x-gettext-domain";
62static const char *restypeContext = "x-trolltech-linguist-context";
63static const char *restypePlurals = "x-gettext-plurals";
64static const char *restypeDummy = "x-dummy";
65static const char *dataTypeUIFile = "x-trolltech-designer-ui";
66static const char *contextMsgctxt = "x-gettext-msgctxt"; // XXX Troll invention, so far.
67static const char *contextOldMsgctxt = "x-gettext-previous-msgctxt"; // XXX Troll invention, so far.
68static const char *attribPlural = "trolltech:plural";
69static const char *XLIFF11namespaceURI = "urn:oasis:names:tc:xliff:document:1.1";
70static const char *XLIFF12namespaceURI = "urn:oasis:names:tc:xliff:document:1.2";
71static const char *TrollTsNamespaceURI = "urn:trolltech:names:ts:document:1.0";
72
73#define COMBINE4CHARS(c1, c2, c3, c4) \
74 (int(c1) << 24 | int(c2) << 16 | int(c3) << 8 | int(c4) )
75
76static QString dataType(const TranslatorMessage &m)
77{
78 QByteArray fileName = m.fileName().toAscii();
79 unsigned int extHash = 0;
80 int pos = fileName.count() - 1;
81 for (int pass = 0; pass < 4 && pos >=0; ++pass, --pos) {
82 if (fileName.at(pos) == '.')
83 break;
84 extHash |= ((int)fileName.at(pos) << (8*pass));
85 }
86
87 switch (extHash) {
88 case COMBINE4CHARS(0,'c','p','p'):
89 case COMBINE4CHARS(0,'c','x','x'):
90 case COMBINE4CHARS(0,'c','+','+'):
91 case COMBINE4CHARS(0,'h','p','p'):
92 case COMBINE4CHARS(0,'h','x','x'):
93 case COMBINE4CHARS(0,'h','+','+'):
94 return QLatin1String("cpp");
95 case COMBINE4CHARS(0, 0 , 0 ,'c'):
96 case COMBINE4CHARS(0, 0 , 0 ,'h'):
97 case COMBINE4CHARS(0, 0 ,'c','c'):
98 case COMBINE4CHARS(0, 0 ,'c','h'):
99 case COMBINE4CHARS(0, 0 ,'h','h'):
100 return QLatin1String("c");
101 case COMBINE4CHARS(0, 0 ,'u','i'):
102 return QLatin1String(dataTypeUIFile); //### form?
103 default:
104 return QLatin1String("plaintext"); // we give up
105 }
106}
107
108static void writeIndent(QTextStream &ts, int indent)
109{
110 ts << QString().fill(QLatin1Char(' '), indent * 2);
111}
112
113struct CharMnemonic
114{
115 char ch;
116 char escape;
117 const char *mnemonic;
118};
119
120static const CharMnemonic charCodeMnemonics[] = {
121 {0x07, 'a', "bel"},
122 {0x08, 'b', "bs"},
123 {0x09, 't', "tab"},
124 {0x0a, 'n', "lf"},
125 {0x0b, 'v', "vt"},
126 {0x0c, 'f', "ff"},
127 {0x0d, 'r', "cr"}
128};
129
130static char charFromEscape(char escape)
131{
132 for (uint i = 0; i < sizeof(charCodeMnemonics)/sizeof(CharMnemonic); ++i) {
133 CharMnemonic cm = charCodeMnemonics[i];
134 if (cm.escape == escape)
135 return cm.ch;
136 }
137 Q_ASSERT(0);
138 return escape;
139}
140
141static QString numericEntity(int ch, bool makePhs)
142{
143 // ### This needs to be reviewed, to reflect the updated XLIFF-PO spec.
144 if (!makePhs || ch < 7 || ch > 0x0d)
145 return QString::fromAscii("&#x%1;").arg(QString::number(ch, 16));
146
147 CharMnemonic cm = charCodeMnemonics[int(ch) - 7];
148 QString name = QLatin1String(cm.mnemonic);
149 char escapechar = cm.escape;
150
151 static int id = 0;
152 return QString::fromAscii("<ph id=\"ph%1\" ctype=\"x-ch-%2\">\\%3</ph>")
153 .arg(++id) .arg(name) .arg(escapechar);
154}
155
156static QString protect(const QString &str, bool makePhs = true)
157{
158 QString result;
159 int len = str.size();
160 for (int i = 0; i != len; ++i) {
161 uint c = str.at(i).unicode();
162 switch (c) {
163 case '\"':
164 result += QLatin1String("&quot;");
165 break;
166 case '&':
167 result += QLatin1String("&amp;");
168 break;
169 case '>':
170 result += QLatin1String("&gt;");
171 break;
172 case '<':
173 result += QLatin1String("&lt;");
174 break;
175 case '\'':
176 result += QLatin1String("&apos;");
177 break;
178 default:
179 if (c < 0x20 && c != '\r' && c != '\n' && c != '\t')
180 result += numericEntity(c, makePhs);
181 else // this also covers surrogates
182 result += QChar(c);
183 }
184 }
185 return result;
186}
187
188
189static void writeExtras(QTextStream &ts, int indent,
190 const TranslatorMessage::ExtraData &extras, const QRegExp &drops)
191{
192 for (Translator::ExtraData::ConstIterator it = extras.begin(); it != extras.end(); ++it) {
193 if (!drops.exactMatch(it.key())) {
194 writeIndent(ts, indent);
195 ts << "<trolltech:" << it.key() << '>'
196 << protect(it.value())
197 << "</trolltech:" << it.key() << ">\n";
198 }
199 }
200}
201
202static void writeLineNumber(QTextStream &ts, const TranslatorMessage &msg, int indent)
203{
204 if (msg.lineNumber() == -1)
205 return;
206 writeIndent(ts, indent);
207 ts << "<context-group purpose=\"location\"><context context-type=\"linenumber\">"
208 << msg.lineNumber() << "</context></context-group>\n";
209 foreach (const TranslatorMessage::Reference &ref, msg.extraReferences()) {
210 writeIndent(ts, indent);
211 ts << "<context-group purpose=\"location\">";
212 if (ref.fileName() != msg.fileName())
213 ts << "<context context-type=\"sourcefile\">" << ref.fileName() << "</context>";
214 ts << "<context context-type=\"linenumber\">" << ref.lineNumber()
215 << "</context></context-group>\n";
216 }
217}
218
219static void writeComment(QTextStream &ts, const TranslatorMessage &msg, const QRegExp &drops, int indent)
220{
221 if (!msg.comment().isEmpty()) {
222 writeIndent(ts, indent);
223 ts << "<context-group><context context-type=\"" << contextMsgctxt << "\">"
224 << protect(msg.comment(), false)
225 << "</context></context-group>\n";
226 }
227 if (!msg.oldComment().isEmpty()) {
228 writeIndent(ts, indent);
229 ts << "<context-group><context context-type=\"" << contextOldMsgctxt << "\">"
230 << protect(msg.oldComment(), false)
231 << "</context></context-group>\n";
232 }
233 writeExtras(ts, indent, msg.extras(), drops);
234 if (!msg.extraComment().isEmpty()) {
235 writeIndent(ts, indent);
236 ts << "<note annotates=\"source\" from=\"developer\">"
237 << protect(msg.extraComment()) << "</note>\n";
238 }
239 if (!msg.translatorComment().isEmpty()) {
240 writeIndent(ts, indent);
241 ts << "<note from=\"translator\">"
242 << protect(msg.translatorComment()) << "</note>\n";
243 }
244}
245
246static void writeTransUnits(QTextStream &ts, const TranslatorMessage &msg, const QRegExp &drops, int indent,
247 const Translator &translator, ConversionData &cd, bool *ok)
248{
249 static int msgid;
250 QString msgidstr = !msg.id().isEmpty() ? msg.id() : QString::fromAscii("_msg%1").arg(++msgid);
251
252 QStringList translns = translator.normalizedTranslations(msg, cd, ok);
253 QHash<QString, QString>::const_iterator it;
254 QString pluralStr;
255 QStringList sources(msg.sourceText());
256 if ((it = msg.extras().find(QString::fromLatin1("po-msgid_plural"))) != msg.extras().end())
257 sources.append(*it);
258 QStringList oldsources;
259 if (!msg.oldSourceText().isEmpty())
260 oldsources.append(msg.oldSourceText());
261 if ((it = msg.extras().find(QString::fromLatin1("po-old_msgid_plural"))) != msg.extras().end()) {
262 if (oldsources.isEmpty()) {
263 if (sources.count() == 2)
264 oldsources.append(QString());
265 else
266 pluralStr = QLatin1Char(' ') + QLatin1String(attribPlural) + QLatin1String("=\"yes\"");
267 }
268 oldsources.append(*it);
269 }
270
271 QStringList::const_iterator
272 srcit = sources.begin(), srcend = sources.end(),
273 oldsrcit = oldsources.begin(), oldsrcend = oldsources.end(),
274 transit = translns.begin(), transend = translns.end();
275 int plural = 0;
276 QString source;
277 while (srcit != srcend || oldsrcit != oldsrcend || transit != transend) {
278 QByteArray attribs;
279 QByteArray state;
280 if (msg.type() == TranslatorMessage::Obsolete) {
281 if (!msg.isPlural())
282 attribs = " translate=\"no\"";
283 } else if (msg.type() == TranslatorMessage::Finished) {
284 attribs = " approved=\"yes\"";
285 } else if (transit != transend && !transit->isEmpty()) {
286 state = " state=\"needs-review-translation\"";
287 }
288 writeIndent(ts, indent);
289 ts << "<trans-unit id=\"" << msgidstr;
290 if (msg.isPlural())
291 ts << "[" << plural++ << "]";
292 ts << "\"" << attribs << ">\n";
293 ++indent;
294
295 writeIndent(ts, indent);
296 if (srcit != srcend) {
297 source = *srcit;
298 ++srcit;
299 } // else just repeat last element
300 ts << "<source xml:space=\"preserve\">" << protect(source) << "</source>\n";
301
302 bool puttrans = false;
303 QString translation;
304 if (transit != transend) {
305 translation = *transit;
306 ++transit;
307 puttrans = true;
308 }
309 do {
310 if (oldsrcit != oldsrcend && !oldsrcit->isEmpty()) {
311 writeIndent(ts, indent);
312 ts << "<alt-trans>\n";
313 ++indent;
314 writeIndent(ts, indent);
315 ts << "<source xml:space=\"preserve\"" << pluralStr << '>' << protect(*oldsrcit) << "</source>\n";
316 if (!puttrans) {
317 writeIndent(ts, indent);
318 ts << "<target restype=\"" << restypeDummy << "\"/>\n";
319 }
320 }
321
322 if (puttrans) {
323 writeIndent(ts, indent);
324 ts << "<target xml:space=\"preserve\"" << state << ">" << protect(translation) << "</target>\n";
325 }
326
327 if (oldsrcit != oldsrcend) {
328 if (!oldsrcit->isEmpty()) {
329 --indent;
330 writeIndent(ts, indent);
331 ts << "</alt-trans>\n";
332 }
333 ++oldsrcit;
334 }
335
336 puttrans = false;
337 } while (srcit == srcend && oldsrcit != oldsrcend);
338
339 if (!msg.isPlural()) {
340 writeLineNumber(ts, msg, indent);
341 writeComment(ts, msg, drops, indent);
342 }
343
344 --indent;
345 writeIndent(ts, indent);
346 ts << "</trans-unit>\n";
347 }
348}
349
350static void writeMessage(QTextStream &ts, const TranslatorMessage &msg, const QRegExp &drops, int indent,
351 const Translator &translator, ConversionData &cd, bool *ok)
352{
353 if (msg.isPlural()) {
354 writeIndent(ts, indent);
355 ts << "<group restype=\"" << restypePlurals << "\"";
356 if (!msg.id().isEmpty())
357 ts << " id=\"" << msg.id() << "\"";
358 if (msg.type() == TranslatorMessage::Obsolete)
359 ts << " translate=\"no\"";
360 ts << ">\n";
361 ++indent;
362 writeLineNumber(ts, msg, indent);
363 writeComment(ts, msg, drops, indent);
364
365 writeTransUnits(ts, msg, drops, indent, translator, cd, ok);
366 --indent;
367 writeIndent(ts, indent);
368 ts << "</group>\n";
369 } else {
370 writeTransUnits(ts, msg, drops, indent, translator, cd, ok);
371 }
372}
373
374
375class XLIFFHandler : public QXmlDefaultHandler
376{
377public:
378 XLIFFHandler(Translator &translator, ConversionData &cd);
379
380 bool startElement(const QString& namespaceURI, const QString &localName,
381 const QString &qName, const QXmlAttributes &atts );
382 bool endElement(const QString& namespaceURI, const QString &localName,
383 const QString &qName );
384 bool characters(const QString &ch);
385 bool fatalError(const QXmlParseException &exception);
386
387 bool endDocument();
388
389private:
390 enum XliffContext {
391 XC_xliff,
392 XC_group,
393 XC_trans_unit,
394 XC_context_group,
395 XC_context_group_any,
396 XC_context,
397 XC_context_filename,
398 XC_context_linenumber,
399 XC_context_context,
400 XC_context_comment,
401 XC_context_old_comment,
402 XC_ph,
403 XC_extra_comment,
404 XC_translator_comment,
405 XC_restype_context,
406 XC_restype_translation,
407 XC_restype_plurals,
408 XC_alt_trans
409 };
410 void pushContext(XliffContext ctx);
411 bool popContext(XliffContext ctx);
412 XliffContext currentContext() const;
413 bool hasContext(XliffContext ctx) const;
414 bool finalizeMessage(bool isPlural);
415
416private:
417 Translator &m_translator;
418 ConversionData &m_cd;
419 TranslatorMessage::Type m_type;
420 QString m_language;
421 QString m_sourceLanguage;
422 QString m_context;
423 QString m_id;
424 QStringList m_sources;
425 QStringList m_oldSources;
426 QString m_comment;
427 QString m_oldComment;
428 QString m_extraComment;
429 QString m_translatorComment;
430 bool m_isPlural;
431 bool m_hadAlt;
432 QStringList m_translations;
433 QString m_fileName;
434 int m_lineNumber;
435 QString m_extraFileName;
436 TranslatorMessage::References m_refs;
437 TranslatorMessage::ExtraData m_extra;
438
439 QString accum;
440 QString m_ctype;
441 const QString m_URITT; // convenience and efficiency
442 const QString m_URI; // ...
443 const QString m_URI12; // ...
444 QStack<int> m_contextStack;
445};
446
447XLIFFHandler::XLIFFHandler(Translator &translator, ConversionData &cd)
448 : m_translator(translator), m_cd(cd),
449 m_type(TranslatorMessage::Finished),
450 m_lineNumber(-1),
451 m_URITT(QLatin1String(TrollTsNamespaceURI)),
452 m_URI(QLatin1String(XLIFF11namespaceURI)),
453 m_URI12(QLatin1String(XLIFF12namespaceURI))
454{}
455
456
457void XLIFFHandler::pushContext(XliffContext ctx)
458{
459 m_contextStack.push_back(ctx);
460}
461
462// Only pops it off if the top of the stack contains ctx
463bool XLIFFHandler::popContext(XliffContext ctx)
464{
465 if (!m_contextStack.isEmpty() && m_contextStack.top() == ctx) {
466 m_contextStack.pop();
467 return true;
468 }
469 return false;
470}
471
472XLIFFHandler::XliffContext XLIFFHandler::currentContext() const
473{
474 if (!m_contextStack.isEmpty())
475 return (XliffContext)m_contextStack.top();
476 return XC_xliff;
477}
478
479// traverses to the top to check all of the parent contexes.
480bool XLIFFHandler::hasContext(XliffContext ctx) const
481{
482 for (int i = m_contextStack.count() - 1; i >= 0; --i) {
483 if (m_contextStack.at(i) == ctx)
484 return true;
485 }
486 return false;
487}
488
489bool XLIFFHandler::startElement(const QString& namespaceURI,
490 const QString &localName, const QString &qName, const QXmlAttributes &atts )
491{
492 Q_UNUSED(qName);
493 if (namespaceURI == m_URITT)
494 goto bail;
495 if (namespaceURI != m_URI && namespaceURI != m_URI12)
496 return false;
497 if (localName == QLatin1String("xliff")) {
498 // make sure that the stack is not empty during parsing
499 pushContext(XC_xliff);
500 } else if (localName == QLatin1String("file")) {
501 m_fileName = atts.value(QLatin1String("original"));
502 m_language = atts.value(QLatin1String("target-language"));
503 m_sourceLanguage = atts.value(QLatin1String("source-language"));
504 } else if (localName == QLatin1String("group")) {
505 if (atts.value(QLatin1String("restype")) == QLatin1String(restypeContext)) {
506 m_context = atts.value(QLatin1String("resname"));
507 pushContext(XC_restype_context);
508 } else {
509 if (atts.value(QLatin1String("restype")) == QLatin1String(restypePlurals)) {
510 pushContext(XC_restype_plurals);
511 m_id = atts.value(QLatin1String("id"));
512 if (atts.value(QLatin1String("translate")) == QLatin1String("no"))
513 m_type = TranslatorMessage::Obsolete;
514 } else {
515 pushContext(XC_group);
516 }
517 }
518 } else if (localName == QLatin1String("trans-unit")) {
519 if (!hasContext(XC_restype_plurals) || m_sources.isEmpty() /* who knows ... */)
520 if (atts.value(QLatin1String("translate")) == QLatin1String("no"))
521 m_type = TranslatorMessage::Obsolete;
522 if (!hasContext(XC_restype_plurals)) {
523 m_id = atts.value(QLatin1String("id"));
524 if (m_id.startsWith(QLatin1String("_msg")))
525 m_id.clear();
526 }
527 if (m_type != TranslatorMessage::Obsolete &&
528 atts.value(QLatin1String("approved")) != QLatin1String("yes"))
529 m_type = TranslatorMessage::Unfinished;
530 pushContext(XC_trans_unit);
531 m_hadAlt = false;
532 } else if (localName == QLatin1String("alt-trans")) {
533 pushContext(XC_alt_trans);
534 } else if (localName == QLatin1String("source")) {
535 m_isPlural = atts.value(QLatin1String(attribPlural)) == QLatin1String("yes");
536 } else if (localName == QLatin1String("target")) {
537 if (atts.value(QLatin1String("restype")) != QLatin1String(restypeDummy))
538 pushContext(XC_restype_translation);
539 } else if (localName == QLatin1String("context-group")) {
540 QString purpose = atts.value(QLatin1String("purpose"));
541 if (purpose == QLatin1String("location"))
542 pushContext(XC_context_group);
543 else
544 pushContext(XC_context_group_any);
545 } else if (currentContext() == XC_context_group && localName == QLatin1String("context")) {
546 QString ctxtype = atts.value(QLatin1String("context-type"));
547 if (ctxtype == QLatin1String("linenumber"))
548 pushContext(XC_context_linenumber);
549 else if (ctxtype == QLatin1String("sourcefile"))
550 pushContext(XC_context_filename);
551 } else if (currentContext() == XC_context_group_any && localName == QLatin1String("context")) {
552 QString ctxtype = atts.value(QLatin1String("context-type"));
553 if (ctxtype == QLatin1String(contextMsgctxt))
554 pushContext(XC_context_comment);
555 else if (ctxtype == QLatin1String(contextOldMsgctxt))
556 pushContext(XC_context_old_comment);
557 } else if (localName == QLatin1String("note")) {
558 if (atts.value(QLatin1String("annotates")) == QLatin1String("source") &&
559 atts.value(QLatin1String("from")) == QLatin1String("developer"))
560 pushContext(XC_extra_comment);
561 else
562 pushContext(XC_translator_comment);
563 } else if (localName == QLatin1String("ph")) {
564 QString ctype = atts.value(QLatin1String("ctype"));
565 if (ctype.startsWith(QLatin1String("x-ch-")))
566 m_ctype = ctype.mid(5);
567 pushContext(XC_ph);
568 }
569bail:
570 if (currentContext() != XC_ph)
571 accum.clear();
572 return true;
573}
574
575bool XLIFFHandler::endElement(const QString &namespaceURI, const QString& localName,
576 const QString &qName)
577{
578 Q_UNUSED(qName);
579 if (namespaceURI == m_URITT) {
580 if (hasContext(XC_trans_unit) || hasContext(XC_restype_plurals))
581 m_extra[localName] = accum;
582 else
583 m_translator.setExtra(localName, accum);
584 return true;
585 }
586 if (namespaceURI != m_URI && namespaceURI != m_URI12)
587 return false;
588 //qDebug() << "URI:" << namespaceURI << "QNAME:" << qName;
589 if (localName == QLatin1String("xliff")) {
590 popContext(XC_xliff);
591 } else if (localName == QLatin1String("source")) {
592 if (hasContext(XC_alt_trans)) {
593 if (m_isPlural && m_oldSources.isEmpty())
594 m_oldSources.append(QString());
595 m_oldSources.append(accum);
596 m_hadAlt = true;
597 } else {
598 m_sources.append(accum);
599 }
600 } else if (localName == QLatin1String("target")) {
601 if (popContext(XC_restype_translation))
602 m_translations.append(accum);
603 } else if (localName == QLatin1String("context-group")) {
604 if (popContext(XC_context_group)) {
605 m_refs.append(TranslatorMessage::Reference(
606 m_extraFileName.isEmpty() ? m_fileName : m_extraFileName, m_lineNumber));
607 m_extraFileName.clear();
608 m_lineNumber = -1;
609 } else {
610 popContext(XC_context_group_any);
611 }
612 } else if (localName == QLatin1String("context")) {
613 if (popContext(XC_context_linenumber)) {
614 bool ok;
615 m_lineNumber = accum.trimmed().toInt(&ok);
616 if (!ok)
617 m_lineNumber = -1;
618 } else if (popContext(XC_context_filename)) {
619 m_extraFileName = accum;
620 } else if (popContext(XC_context_comment)) {
621 m_comment = accum;
622 } else if (popContext(XC_context_old_comment)) {
623 m_oldComment = accum;
624 }
625 } else if (localName == QLatin1String("note")) {
626 if (popContext(XC_extra_comment))
627 m_extraComment = accum;
628 else if (popContext(XC_translator_comment))
629 m_translatorComment = accum;
630 } else if (localName == QLatin1String("ph")) {
631 m_ctype.clear();
632 popContext(XC_ph);
633 } else if (localName == QLatin1String("trans-unit")) {
634 popContext(XC_trans_unit);
635 if (!m_hadAlt)
636 m_oldSources.append(QString());
637 if (!hasContext(XC_restype_plurals)) {
638 if (!finalizeMessage(false))
639 return false;
640 }
641 } else if (localName == QLatin1String("alt-trans")) {
642 popContext(XC_alt_trans);
643 } else if (localName == QLatin1String("group")) {
644 if (popContext(XC_restype_plurals)) {
645 if (!finalizeMessage(true))
646 return false;
647 } else if (popContext(XC_restype_context)) {
648 m_context.clear();
649 } else {
650 popContext(XC_group);
651 }
652 }
653 return true;
654}
655
656bool XLIFFHandler::characters(const QString &ch)
657{
658 if (currentContext() == XC_ph) {
659 // handle the content of <ph> elements
660 for (int i = 0; i < ch.count(); ++i) {
661 QChar chr = ch.at(i);
662 if (accum.endsWith(QLatin1Char('\\')))
663 accum[accum.size() - 1] = QLatin1Char(charFromEscape(chr.toAscii()));
664 else
665 accum.append(chr);
666 }
667 } else {
668 QString t = ch;
669 t.replace(QLatin1String("\r"), QLatin1String(""));
670 accum.append(t);
671 }
672 return true;
673}
674
675bool XLIFFHandler::endDocument()
676{
677 m_translator.setLanguageCode(m_language);
678 m_translator.setSourceLanguageCode(m_sourceLanguage);
679 return true;
680}
681
682bool XLIFFHandler::finalizeMessage(bool isPlural)
683{
684 if (m_sources.isEmpty()) {
685 m_cd.appendError(QLatin1String("XLIFF syntax error: Message without source string."));
686 return false;
687 }
688 TranslatorMessage msg(m_context, m_sources[0],
689 m_comment, QString(), QString(), -1,
690 m_translations, m_type, isPlural);
691 msg.setId(m_id);
692 msg.setReferences(m_refs);
693 msg.setOldComment(m_oldComment);
694 msg.setExtraComment(m_extraComment);
695 msg.setTranslatorComment(m_translatorComment);
696 if (m_sources.count() > 1 && m_sources[1] != m_sources[0])
697 m_extra.insert(QLatin1String("po-msgid_plural"), m_sources[1]);
698 if (!m_oldSources.isEmpty()) {
699 if (!m_oldSources[0].isEmpty())
700 msg.setOldSourceText(m_oldSources[0]);
701 if (m_oldSources.count() > 1 && m_oldSources[1] != m_oldSources[0])
702 m_extra.insert(QLatin1String("po-old_msgid_plural"), m_oldSources[1]);
703 }
704 msg.setExtras(m_extra);
705 m_translator.append(msg);
706
707 m_id.clear();
708 m_sources.clear();
709 m_oldSources.clear();
710 m_translations.clear();
711 m_comment.clear();
712 m_oldComment.clear();
713 m_extraComment.clear();
714 m_translatorComment.clear();
715 m_extra.clear();
716 m_refs.clear();
717 m_type = TranslatorMessage::Finished;
718 return true;
719}
720
721bool XLIFFHandler::fatalError(const QXmlParseException &exception)
722{
723 QString msg;
724 msg.sprintf("XML error: Parse error at line %d, column %d (%s).\n",
725 exception.lineNumber(), exception.columnNumber(),
726 exception.message().toLatin1().data() );
727 m_cd.appendError(msg);
728 return false;
729}
730
731bool loadXLIFF(Translator &translator, QIODevice &dev, ConversionData &cd)
732{
733 QXmlInputSource in(&dev);
734 QXmlSimpleReader reader;
735 XLIFFHandler hand(translator, cd);
736 reader.setContentHandler(&hand);
737 reader.setErrorHandler(&hand);
738 return reader.parse(in);
739}
740
741bool saveXLIFF(const Translator &translator, QIODevice &dev, ConversionData &cd)
742{
743 bool ok = true;
744 int indent = 0;
745
746 QTextStream ts(&dev);
747 ts.setCodec(QTextCodec::codecForName("UTF-8"));
748
749 QStringList dtgs = cd.dropTags();
750 dtgs << QLatin1String("po-(old_)?msgid_plural");
751 QRegExp drops(dtgs.join(QLatin1String("|")));
752
753 QHash<QString, QHash<QString, QList<TranslatorMessage> > > messageOrder;
754 QHash<QString, QList<QString> > contextOrder;
755 QList<QString> fileOrder;
756 foreach (const TranslatorMessage &msg, translator.messages()) {
757 QHash<QString, QList<TranslatorMessage> > &file = messageOrder[msg.fileName()];
758 if (file.isEmpty())
759 fileOrder.append(msg.fileName());
760 QList<TranslatorMessage> &context = file[msg.context()];
761 if (context.isEmpty())
762 contextOrder[msg.fileName()].append(msg.context());
763 context.append(msg);
764 }
765
766 ts.setFieldAlignment(QTextStream::AlignRight);
767 ts << "<?xml version=\"1.0\"";
768 ts << " encoding=\"utf-8\"?>\n";
769 ts << "<xliff version=\"1.2\" xmlns=\"" << XLIFF12namespaceURI
770 << "\" xmlns:trolltech=\"" << TrollTsNamespaceURI << "\">\n";
771 ++indent;
772 writeExtras(ts, indent, translator.extras(), drops);
773 foreach (const QString &fn, fileOrder) {
774 writeIndent(ts, indent);
775 ts << "<file original=\"" << fn << "\""
776 << " datatype=\"" << dataType(messageOrder[fn].begin()->first()) << "\""
777 << " source-language=\""
778 << (translator.sourceLanguageCode().isEmpty() ?
779 QByteArray("en") : translator.sourceLanguageCode().toLatin1()) << "\""
780 << " target-language=\"" << translator.languageCode() << "\""
781 << "><body>\n";
782 ++indent;
783
784 foreach (const QString &ctx, contextOrder[fn]) {
785 if (!ctx.isEmpty()) {
786 writeIndent(ts, indent);
787 ts << "<group restype=\"" << restypeContext << "\""
788 << " resname=\"" << protect(ctx) << "\">\n";
789 ++indent;
790 }
791
792 foreach (const TranslatorMessage &msg, messageOrder[fn][ctx])
793 writeMessage(ts, msg, drops, indent, translator, cd, &ok);
794
795 if (!ctx.isEmpty()) {
796 --indent;
797 writeIndent(ts, indent);
798 ts << "</group>\n";
799 }
800 }
801
802 --indent;
803 writeIndent(ts, indent);
804 ts << "</body></file>\n";
805 }
806 --indent;
807 writeIndent(ts, indent);
808 ts << "</xliff>\n";
809
810 return ok;
811}
812
813int initXLIFF()
814{
815 Translator::FileFormat format;
816 format.extension = QLatin1String("xlf");
817 format.description = QObject::tr("XLIFF localization files");
818 format.fileType = Translator::FileFormat::TranslationSource;
819 format.priority = 1;
820 format.loader = &loadXLIFF;
821 format.saver = &saveXLIFF;
822 Translator::registerFileFormat(format);
823 return 1;
824}
825
826Q_CONSTRUCTOR_FUNCTION(initXLIFF)
827
828QT_END_NAMESPACE
Note: See TracBrowser for help on using the repository browser.