Vidalia  0.3.1
ts2po.cpp
Go to the documentation of this file.
1 /*
2 ** This file is part of Vidalia, and is subject to the license terms in the
3 ** LICENSE file, found in the top level directory of this distribution. If you
4 ** did not receive the LICENSE file with this file, you may obtain it from the
5 ** Vidalia source package distributed by the Vidalia Project at
6 ** http://www.torproject.org/projects/vidalia.html. No part of Vidalia,
7 ** including this file, may be copied, modified, propagated, or distributed
8 ** except according to the terms described in the LICENSE file.
9 */
10 
11 #include <QFile>
12 #include <QFileInfo>
13 #include <QDomDocument>
14 #include <QTextStream>
15 #include <QTextCodec>
16 #include <QDateTime>
17 #include <stdlib.h>
18 
19 #include "ts2po_config.h"
20 
21 #define TS_DOCTYPE "TS"
22 #define TS_ELEMENT_CONTEXT "context"
23 #define TS_ELEMENT_NAME "name"
24 #define TS_ELEMENT_MESSAGE "message"
25 #define TS_ELEMENT_SOURCE "source"
26 #define TS_ELEMENT_TRANSLATION "translation"
27 #define TS_ELEMENT_LOCATION "location"
28 #define TS_ATTR_FILENAME "filename"
29 #define TS_ATTR_LINE "line"
30 
31 
32 /** Return the current time (in UTC) in the format YYYY-MM-DD HH:MM+0000. */
33 QString
35 {
36  QDateTime now = QDateTime::currentDateTime().toUTC();
37  return now.toString("yyyy-MM-dd hh:mm+0000");
38 }
39 
40 /** Return a header to be placed at the top of the .po file. The header will
41  * include <b>encoding</b> in the Content-Type header line. */
42 QString
43 create_po_header(const QString &encoding)
44 {
45  QString header;
46  QString tstamp = create_po_timestamp();
47 
48  header.append("msgid \"\"\n");
49  header.append("msgstr \"\"\n");
50  header.append("\"Project-Id-Version: "TS2PO_PROJECT_ID"\\n\"\n");
51  header.append("\"Report-Msgid-Bugs-To: "TS2PO_CONTACT_ADDR"\\n\"\n");
52  header.append(QString("\"POT-Creation-Date: %1\\n\"\n").arg(tstamp));
53  header.append("\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n");
54  header.append("\"Last-Translator: \\n\"\n");
55  header.append("\"Language-Team: "TS2PO_LANGUAGE_TEAM"\\n\"\n");
56  header.append("\"MIME-Version: 1.0\\n\"\n");
57  header.append("\"Content-Type: text/plain; ");
58  header.append(QString("charset=%1\\n\"\n").arg(encoding));
59  header.append("\"Content-Transfer-Encoding: 8bit\\n\"\n");
60  header.append("\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n");
61  header.append("\"X-Generator: Vidalia ts2po "TS2PO_VERSION"\\n\"\n");
62  header.append("\n");
63 
64  return header;
65 }
66 
67 /** Parse the filename from the relative or absolute path given in
68  * <b>filePath</b>. */
69 QString
70 parse_filename(const QString &filePath)
71 {
72  QFileInfo file(filePath);
73  return file.fileName();
74 }
75 
76 /** Convert the messages in <b>context</b> to PO format. The output will be
77  * appended to <b>po</b>. Returns the number of source messages converted on
78  * success, or -1 on error and <b>errorMessage</b> will be set. */
79 int
80 convert_context(const QDomElement &context, QString *po, QString *errorMessage)
81 {
82  QString msgctxt, msgid, msgstr;
83  QString filename, line;
84  QDomElement location, source, translation;
85  int n = 0;
86 
87  Q_ASSERT(po);
88  Q_ASSERT(errorMessage);
89 
90  QDomElement name = context.firstChildElement(TS_ELEMENT_NAME);
91  if (name.isNull()) {
92  *errorMessage = QString("context element with no name (line %1)")
93  .arg(context.lineNumber());
94  return -1;
95  }
96  msgctxt = name.text();
97 
98  QDomElement msg = context.firstChildElement(TS_ELEMENT_MESSAGE);
99  while (!msg.isNull()) {
100  /* Extract the <source> tags */
101  source = msg.firstChildElement(TS_ELEMENT_SOURCE);
102  if (source.isNull()) {
103  *errorMessage = QString("message element with no source string "
104  "(line %1)").arg(msg.lineNumber());
105  return -1;
106  }
107  msgid = source.text().trimmed();
108  msgid.replace("\r", "");
109  msgid.replace("\"", "\\\"");
110  msgid.replace("\n", "\\n\"\n\"");
111 
112  /* Extract the <translation> tags */
113  translation = msg.firstChildElement(TS_ELEMENT_TRANSLATION);
114  msgstr = translation.text().trimmed();
115  msgstr.replace("\r", "");
116  msgstr.replace("\"", "\\\"");
117  msgstr.replace("\n", "\\n\"\n\"");
118 
119  /* Try to extract the <location> tags (optional) */
120  location = msg.firstChildElement(TS_ELEMENT_LOCATION);
121  filename = parse_filename(location.attribute(TS_ATTR_FILENAME));
122  line = location.attribute(TS_ATTR_LINE);
123 
124  /* Format the .po entry for this string */
125  if (!filename.isEmpty() && !line.isEmpty())
126  (*po).append(QString("#: %1:%2\n").arg(filename).arg(line));
127  (*po).append(QString("msgctxt \"%1\"\n").arg(msgctxt));
128  (*po).append(QString("msgid \"%1\"\n").arg(msgid));
129  (*po).append(QString("msgstr \"%1\"\n").arg(msgstr));
130  (*po).append("\n");
131 
132  /* Find the next source message in the current context */
133  msg = msg.nextSiblingElement(TS_ELEMENT_MESSAGE);
134  n++;
135  }
136  return n;
137 }
138 
139 /** Convert the TS-formatted document in <b>ts</b> to a PO-formatted document.
140  * The output will be written to <b>po</b>, including a file header that
141  * specifies <b>encoding</b> as the character set. Returns the number of strings
142  * converted on success, or -1 on error and <b>errorMessage</b> will be set. */
143 int
144 ts2po(const QDomDocument *ts, QString *po, const QString &encoding,
145  QString *errorMessage)
146 {
147  int n_strings = 0;
148  QString context;
149 
150  Q_ASSERT(ts);
151  Q_ASSERT(po);
152  Q_ASSERT(errorMessage);
153 
154  /* Get the document root and check that it's valid */
155  QDomElement root = ts->documentElement();
156  if (root.tagName() != TS_DOCTYPE)
157  return -1;
158 
159  /* Start with the PO header */
160  *po = create_po_header(encoding);
161 
162  /* Iterate through all of the translation contexts and build up the PO file
163  * output. */
164  QDomElement child = root.firstChildElement(TS_ELEMENT_CONTEXT);
165  while (!child.isNull()) {
166  QString context;
167 
168  /* Convert the current .ts context to .po */
169  int n = convert_context(child, &context, errorMessage);
170  if (n < 0)
171  return -1;
172 
173  /* Add it to the output file */
174  (*po).append(context);
175  n_strings += n;
176 
177  /* Move to the next context */
178  child = child.nextSiblingElement(TS_ELEMENT_CONTEXT);
179  }
180  return n_strings;
181 }
182 
183 /** Display application usage and exit. */
184 void
186 {
187  QTextStream error(stderr);
188  error << "usage: ts2po [-q] -i <infile.ts> -o <outfile.po> "
189  "[-c <encoding>]\n";
190  error << " -q (optional) Quiet mode (errors are still displayed)\n";
191  error << " -i <infile.ts> Input .ts file\n";
192  error << " -o <outfile.po> Output .po file\n";
193  error << " -c <encoding> Text encoding (default: utf-8)\n";
194  error.flush();
195  exit(1);
196 }
197 
198 int
199 main(int argc, char *argv[])
200 {
201  QTextStream error(stderr);
202  QString errorMessage;
203  char *infile, *outfile;
204  QTextCodec *codec = QTextCodec::codecForName("utf-8");
205  bool quiet = false;
206 
207  /* Check for the correct number of input parameters. */
208  if (argc < 5 || argc > 8)
210  for (int i = 1; i < argc; i++) {
211  QString arg(argv[i]);
212  if (!arg.compare("-q", Qt::CaseInsensitive))
213  quiet = true;
214  else if (!arg.compare("-i", Qt::CaseInsensitive) && ++i < argc)
215  infile = argv[i];
216  else if (!arg.compare("-o", Qt::CaseInsensitive) && ++i < argc)
217  outfile = argv[i];
218  else if (!arg.compare("-c", Qt::CaseInsensitive) && ++i < argc) {
219  codec = QTextCodec::codecForName(argv[i]);
220  if (!codec) {
221  error << "Invalid text encoding specified.\n";
222  return 1;
223  }
224  } else
226  }
227 
228  /* Read and parse the input .ts file. */
229  QDomDocument ts;
230  QFile tsFile(infile);
231  if (!ts.setContent(&tsFile, true, &errorMessage)) {
232  error << QString("Unable to parse '%1': %2\n").arg(infile)
233  .arg(errorMessage);
234  return 1;
235  }
236 
237  /* Try to open the output .po file for writing. */
238  QFile poFile(outfile);
239  if (!poFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
240  error << QString("Unable to open '%1' for writing: %2\n")
241  .arg(outfile)
242  .arg(tsFile.errorString());
243  return 2;
244  }
245 
246  /* Convert the input .ts file to a .po formatted file. */
247  QString po;
248  int n_strings = ts2po(&ts, &po, QString(codec->name()), &errorMessage);
249  if (n_strings < 0) {
250  error << QString("Unable to convert '%1' to '%2': %3\n").arg(infile)
251  .arg(outfile)
252  .arg(errorMessage);
253  return 3;
254  }
255 
256  /* Write the .po output. */
257  QTextStream out(&poFile);
258  out.setCodec(codec);
259  out << po;
260  poFile.close();
261 
262  if (!quiet) {
263  QTextStream results(stdout);
264  results << QString("Converted %1 strings from %2 to %3.\n").arg(n_strings)
265  .arg(infile)
266  .arg(outfile);
267  }
268  return 0;
269 }
270