Vidalia 0.2.12

po2ts.cpp

Go to the documentation of this file.
00001 /*
00002 **  This file is part of Vidalia, and is subject to the license terms in the
00003 **  LICENSE file, found in the top level directory of this distribution. If you
00004 **  did not receive the LICENSE file with this file, you may obtain it from the
00005 **  Vidalia source package distributed by the Vidalia Project at
00006 **  http://www.vidalia-project.net/. No part of Vidalia, including this file,
00007 **  may be copied, modified, propagated, or distributed except according to the
00008 **  terms described in the LICENSE file.
00009 */
00010 
00011 #include <QHash>
00012 #include <QFile>
00013 #include <QDomDocument>
00014 #include <QTextStream>
00015 #include <QTextCodec>
00016 #include <stdlib.h>
00017 
00018 #define TS_DOCTYPE                    "TS"
00019 #define TS_ELEMENT_ROOT               "TS"
00020 #define TS_ELEMENT_CONTEXT            "context"
00021 #define TS_ELEMENT_NAME               "name"
00022 #define TS_ELEMENT_MESSAGE            "message"
00023 #define TS_ELEMENT_SOURCE             "source"
00024 #define TS_ELEMENT_TRANSLATION        "translation"
00025 #define TS_ATTR_TRANSLATION_TYPE      "type"
00026 #define TS_ATTR_VERSION               "version"
00027 
00028 
00029 /** Create a new context element with the name <b>contextName</b>. */
00030 QDomElement
00031 new_context_element(QDomDocument *ts, const QString &contextName)
00032 {
00033   QDomElement context, name;
00034  
00035   /* Create a <name> element */
00036   name = ts->createElement(TS_ELEMENT_NAME);
00037   name.appendChild(ts->createTextNode(contextName));
00038 
00039   /* Create a <context> element and add the <name> element as a child */
00040   context = ts->createElement(TS_ELEMENT_CONTEXT);
00041   context.appendChild(name);
00042   return context;
00043 }
00044 
00045 /** Create a new message element using the source string <b>msgid</b> and the
00046  * translation <b>msgstr</b>. */
00047 QDomElement
00048 new_message_element(QDomDocument *ts,
00049                     const QString &msgid, const QString &msgstr)
00050 {
00051   QDomElement message, source, translation;
00052 
00053   /* Create and set the <source> element */
00054   source = ts->createElement(TS_ELEMENT_SOURCE);
00055   source.appendChild(ts->createTextNode(msgid));
00056 
00057   /* Create and set the <translation> element */
00058   translation = ts->createElement(TS_ELEMENT_TRANSLATION);
00059   if (!msgstr.isEmpty())
00060     translation.appendChild(ts->createTextNode(msgstr));
00061   else
00062     translation.setAttribute(TS_ATTR_TRANSLATION_TYPE, "unfinished");
00063 
00064   /* Create a <message> element and add the <source> and <translation>
00065    * elements as children */
00066   message = ts->createElement(TS_ELEMENT_MESSAGE);
00067   message.appendChild(source);
00068   message.appendChild(translation);
00069 
00070   return message;
00071 }
00072 
00073 /** Create a new TS document of the appropriate doctype and with a TS root
00074  * element. */
00075 QDomDocument
00076 new_ts_document()
00077 {
00078   QDomDocument ts(TS_DOCTYPE);
00079   
00080   QDomElement root = ts.createElement(TS_ELEMENT_ROOT);
00081   root.setAttribute(TS_ATTR_VERSION, "1.1");
00082   ts.appendChild(root);
00083 
00084   return ts;
00085 }
00086 
00087 /** Parse the context name from <b>str</b>, where the context name is of the
00088  * form DQUOTE ContextName DQUOTE. */
00089 QString
00090 parse_message_context(const QString &str)
00091 {
00092   QString out = str.trimmed();
00093   out = out.replace("\"", "");
00094   return out;
00095 }
00096 
00097 /** Parse the context name from <b>str</b>, where <b>str</b> is of the
00098  * form ContextName#Number. This is the format used by translate-toolkit. */
00099 QString
00100 parse_message_context_lame(const QString &str)
00101 {
00102   if (str.contains("#"))
00103     return str.section("#", 0, 0);
00104   return QString();
00105 }
00106 
00107 /** Parse the PO-formatted message string from <b>msg</b>. If <b>msg</b> is a
00108  * multiline string, the extra double quotes will be replaced with newlines
00109  * appropriately. */
00110 QString
00111 parse_message_string(const QString &msg)
00112 {
00113   QString out = msg.trimmed(); 
00114   
00115   out.replace("\"\n\"", "");
00116   if (out.startsWith("\""))
00117     out = out.remove(0, 1);
00118   if (out.endsWith("\""))
00119     out.chop(1);
00120   out.replace("\\\"", "\"");
00121   return out;
00122 }
00123 
00124 /** Read and return the next non-empty line from <b>stream</b>. */
00125 QString
00126 read_next_line(QTextStream *stream)
00127 {
00128   stream->skipWhiteSpace();
00129   return stream->readLine().append("\n");
00130 }
00131 
00132 /** Skip past the header portion of the PO file and any leading whitespace. 
00133  * The next line read from <b>po</b> will be the first non-header line in the
00134  * document. */
00135 void
00136 skip_po_header(QTextStream *po)
00137 {
00138   QString line;
00139   /* Skip any leading whitespace before the header */
00140   po->skipWhiteSpace();
00141   /* Read to the first empty line */
00142   line = po->readLine();
00143   while (!po->atEnd() && !line.isEmpty())
00144     line = po->readLine();
00145 }
00146 
00147 /** Convert <b>po</b> from the PO format to a TS-formatted XML document.
00148  * <b>ts</b> will be set to the resulting TS document. Return the number of
00149  * converted strings on success, or -1 on error and <b>errorMessage</b> will
00150  * be set. */
00151 int
00152 po2ts(QTextStream *po, QDomDocument *ts, QString *errorMessage)
00153 {
00154   QString line;
00155   QString msgctxt, msgid, msgstr;
00156   QHash<QString,QDomElement> contextElements;
00157   QDomElement contextElement, msgElement, transElement;
00158   int n_strings = 0;
00159 
00160   Q_ASSERT(po);
00161   Q_ASSERT(ts);
00162   Q_ASSERT(errorMessage);
00163 
00164   *ts = new_ts_document();
00165   
00166   skip_po_header(po);
00167   line = read_next_line(po);
00168   while (!po->atEnd()) {
00169     /* Ignore all "#" lines except "#:" */
00170     while (line.startsWith("#")) {
00171       if (line.startsWith("#:")) {
00172         /* Context was specified with the stupid overloaded "#:" syntax.*/
00173         msgctxt = line.section(" ", 1);
00174         msgctxt = parse_message_context_lame(msgctxt);
00175       }
00176       line = read_next_line(po);
00177     }
00178 
00179     /* A context specified on a "msgctxt" line takes precedence over a context
00180      * specified using the overload "#:" notation. */
00181     if (line.startsWith("msgctxt ")) {    
00182       msgctxt = line.section(" ", 1);
00183       msgctxt = parse_message_context(msgctxt);
00184       line = read_next_line(po);
00185     }
00186     
00187     /* Parse the (possibly multiline) message source string */
00188     if (!line.startsWith("msgid ")) {
00189       *errorMessage = "expected 'msgid' line";
00190       return -1;
00191     }
00192     msgid = line.section(" ", 1);
00193     
00194     line = read_next_line(po);
00195     while (line.startsWith("\"")) {
00196       msgid.append(line);
00197       line = read_next_line(po);
00198     }
00199     msgid = parse_message_string(msgid);
00200 
00201     /* Parse the (possibly multiline) translated string */
00202     if (!line.startsWith("msgstr ")) {
00203       *errorMessage = "expected 'msgstr' line";
00204       return -1;
00205     }
00206     msgstr = line.section(" ", 1);
00207     
00208     line = read_next_line(po);
00209     while (line.startsWith("\"")) {
00210       msgstr.append(line);
00211       line = read_next_line(po);
00212     }
00213     msgstr = parse_message_string(msgstr);
00214 
00215     /* Add the message and translation to the .ts document */
00216     if (contextElements.contains(msgctxt)) {
00217       contextElement = contextElements.value(msgctxt);
00218     } else {
00219       contextElement = new_context_element(ts, msgctxt);
00220       ts->documentElement().appendChild(contextElement);
00221       contextElements.insert(msgctxt, contextElement);
00222     }
00223     contextElement.appendChild(new_message_element(ts, msgid, msgstr)); 
00224     
00225     n_strings++;
00226   }
00227   return n_strings;
00228 }
00229 
00230 /** Display application usage and exit. */
00231 void
00232 print_usage_and_exit()
00233 {
00234   QTextStream error(stderr);
00235   error << "usage: po2ts [-q] -i <infile.po> -o <outfile.ts> "
00236            "[-c <encoding>]\n";
00237   error << "  -q (optional)   Quiet mode (errors are still displayed)\n";
00238   error << "  -i <infile.po>  Input .po file\n";
00239   error << "  -o <outfile.ts> Output .ts file\n";
00240   error << "  -c <encoding>   Text encoding (default: utf-8)\n";
00241   error.flush();
00242   exit(1);
00243 }
00244 
00245 int
00246 main(int argc, char *argv[])
00247 {
00248   QTextStream error(stderr);
00249   QString errorMessage;
00250   char *infile, *outfile;
00251   QTextCodec *codec = QTextCodec::codecForName("utf-8");
00252   bool quiet = false;
00253 
00254   /* Check for the correct number of input parameters. */
00255   if (argc < 5 || argc > 8)
00256     print_usage_and_exit();
00257   for (int i = 1; i < argc; i++) {
00258     QString arg(argv[i]);
00259     if (!arg.compare("-q", Qt::CaseInsensitive))
00260       quiet = true;
00261     else if (!arg.compare("-i", Qt::CaseInsensitive) && ++i < argc)
00262       infile = argv[i];
00263     else if (!arg.compare("-o", Qt::CaseInsensitive) && ++i < argc)
00264       outfile = argv[i];
00265     else if (!arg.compare("-c", Qt::CaseInsensitive) && ++i < argc) {
00266       codec = QTextCodec::codecForName(argv[i]);
00267       if (!codec) {
00268         error << "Invalid text encoding specified\n";
00269         return 1;
00270       }
00271     } else
00272       print_usage_and_exit(); 
00273   }
00274 
00275   /* Open the input PO file for reading. */
00276   QFile poFile(infile);
00277   if (!poFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
00278     error << QString("Unable to open '%1' for reading: %2\n").arg(infile)
00279                                                 .arg(poFile.errorString());
00280     return 2;
00281   }
00282 
00283   QDomDocument ts;
00284   QTextStream po(&poFile);
00285   po.setCodec(codec);
00286   int n_strings = po2ts(&po, &ts, &errorMessage);
00287   if (n_strings < 0) {
00288     error << QString("Unable to convert '%1': %2\n").arg(infile)
00289                                                     .arg(errorMessage);
00290     return 3;
00291   }
00292 
00293   /* Open the TS file for writing. */
00294   QFile tsFile(outfile);
00295   if (!tsFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
00296     error << QString("Unable to open '%1' for writing: %2\n").arg(outfile)
00297                                                 .arg(tsFile.errorString());
00298     return 4;
00299   }
00300 
00301   /* Write the .ts output. */
00302   QTextStream out(&tsFile);
00303   out.setCodec(codec);
00304   out << QString("<?xml version=\"1.0\" encoding=\"%1\"?>\n")
00305                                                   .arg(QString(codec->name()));
00306   out << ts.toString(4);
00307 
00308   if (!quiet) {
00309     QTextStream results(stdout);
00310     results << QString("Converted %1 strings from %2 to %3.\n").arg(n_strings)
00311                                                                .arg(infile)
00312                                                                .arg(outfile);
00313   }
00314   return 0;
00315 }
00316