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 |
|
---|
56 | QT_BEGIN_NAMESPACE
|
---|
57 |
|
---|
58 | /**
|
---|
59 | * Implementation of XLIFF file format for Linguist
|
---|
60 | */
|
---|
61 | //static const char *restypeDomain = "x-gettext-domain";
|
---|
62 | static const char *restypeContext = "x-trolltech-linguist-context";
|
---|
63 | static const char *restypePlurals = "x-gettext-plurals";
|
---|
64 | static const char *restypeDummy = "x-dummy";
|
---|
65 | static const char *dataTypeUIFile = "x-trolltech-designer-ui";
|
---|
66 | static const char *contextMsgctxt = "x-gettext-msgctxt"; // XXX Troll invention, so far.
|
---|
67 | static const char *contextOldMsgctxt = "x-gettext-previous-msgctxt"; // XXX Troll invention, so far.
|
---|
68 | static const char *attribPlural = "trolltech:plural";
|
---|
69 | static const char *XLIFF11namespaceURI = "urn:oasis:names:tc:xliff:document:1.1";
|
---|
70 | static const char *XLIFF12namespaceURI = "urn:oasis:names:tc:xliff:document:1.2";
|
---|
71 | static 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 |
|
---|
76 | static 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 |
|
---|
108 | static void writeIndent(QTextStream &ts, int indent)
|
---|
109 | {
|
---|
110 | ts << QString().fill(QLatin1Char(' '), indent * 2);
|
---|
111 | }
|
---|
112 |
|
---|
113 | struct CharMnemonic
|
---|
114 | {
|
---|
115 | char ch;
|
---|
116 | char escape;
|
---|
117 | const char *mnemonic;
|
---|
118 | };
|
---|
119 |
|
---|
120 | static 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 |
|
---|
130 | static 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 |
|
---|
141 | static 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 |
|
---|
156 | static 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(""");
|
---|
165 | break;
|
---|
166 | case '&':
|
---|
167 | result += QLatin1String("&");
|
---|
168 | break;
|
---|
169 | case '>':
|
---|
170 | result += QLatin1String(">");
|
---|
171 | break;
|
---|
172 | case '<':
|
---|
173 | result += QLatin1String("<");
|
---|
174 | break;
|
---|
175 | case '\'':
|
---|
176 | result += QLatin1String("'");
|
---|
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 |
|
---|
189 | static 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 |
|
---|
202 | static 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 |
|
---|
219 | static 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 |
|
---|
246 | static 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 |
|
---|
350 | static 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 |
|
---|
375 | class XLIFFHandler : public QXmlDefaultHandler
|
---|
376 | {
|
---|
377 | public:
|
---|
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 |
|
---|
389 | private:
|
---|
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 |
|
---|
416 | private:
|
---|
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 |
|
---|
447 | XLIFFHandler::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 |
|
---|
457 | void 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
|
---|
463 | bool 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 |
|
---|
472 | XLIFFHandler::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.
|
---|
480 | bool 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 |
|
---|
489 | bool 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 | }
|
---|
569 | bail:
|
---|
570 | if (currentContext() != XC_ph)
|
---|
571 | accum.clear();
|
---|
572 | return true;
|
---|
573 | }
|
---|
574 |
|
---|
575 | bool 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 |
|
---|
656 | bool 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 |
|
---|
675 | bool XLIFFHandler::endDocument()
|
---|
676 | {
|
---|
677 | m_translator.setLanguageCode(m_language);
|
---|
678 | m_translator.setSourceLanguageCode(m_sourceLanguage);
|
---|
679 | return true;
|
---|
680 | }
|
---|
681 |
|
---|
682 | bool 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 |
|
---|
721 | bool 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 |
|
---|
731 | bool 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 |
|
---|
741 | bool 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 |
|
---|
813 | int 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 |
|
---|
826 | Q_CONSTRUCTOR_FUNCTION(initXLIFF)
|
---|
827 |
|
---|
828 | QT_END_NAMESPACE
|
---|