Vidalia 0.2.12
|
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 <QTextStream> 00014 #include <QTextCodec> 00015 #include <QDateTime> 00016 #include <QStringList> 00017 #include <stdlib.h> 00018 00019 #include "nsh2po_config.h" 00020 00021 00022 /** Parse the context name from <b>str</b>, where the context name is of the 00023 * form DQUOTE ContextName DQUOTE. */ 00024 QString 00025 parse_message_context(const QString &str) 00026 { 00027 QString out = str.trimmed(); 00028 out = out.replace("\"", ""); 00029 return out; 00030 } 00031 00032 /** Parse the context name from <b>str</b>, where <b>str</b> is of the 00033 * form ContextName#Number. This is the format used by translate-toolkit. */ 00034 QString 00035 parse_message_context_lame(const QString &str) 00036 { 00037 if (str.contains("#")) 00038 return str.section("#", 0, 0); 00039 return QString(); 00040 } 00041 00042 /** Parse the PO-formatted message string from <b>msg</b>. */ 00043 QString 00044 parse_message_string(const QString &msg) 00045 { 00046 QString out = msg.trimmed(); 00047 00048 if (out.startsWith("\"")) 00049 out = out.remove(0, 1); 00050 if (out.endsWith("\"")) 00051 out.chop(1); 00052 out.replace("\\\"", "\""); 00053 out.replace("\\r\\n", "\\n"); 00054 return out; 00055 } 00056 00057 /** Parse the NSIS-formatted LangString message from <b>msg</b>. */ 00058 QString 00059 parse_nsh_langstring(const QString &msg) 00060 { 00061 QString out = msg.trimmed(); 00062 00063 if (out.startsWith("\"")) 00064 out = out.remove(0, 1); 00065 if (out.endsWith("\"")) 00066 out.chop(1); 00067 out.replace("$\\n", "\\n"); 00068 out.replace("$\\r", ""); 00069 out.replace("\\r", ""); 00070 return out; 00071 } 00072 00073 /** Return the current time (in UTC) in the format YYYY-MM-DD HH:MM+0000. */ 00074 QString 00075 create_po_timestamp() 00076 { 00077 QDateTime now = QDateTime::currentDateTime().toUTC(); 00078 return now.toString("yyyy-MM-dd hh:mm+0000"); 00079 } 00080 00081 /** Return a header to be placed at the top of the .po file. */ 00082 QString 00083 create_po_header(const QString &charset) 00084 { 00085 QString header; 00086 QString tstamp = create_po_timestamp(); 00087 00088 header.append("msgid \"\"\n"); 00089 header.append("msgstr \"\"\n"); 00090 header.append("\"Project-Id-Version: "NSH2PO_PROJECT_ID"\\n\"\n"); 00091 header.append("\"Report-Msgid-Bugs-To: "NSH2PO_CONTACT_ADDR"\\n\"\n"); 00092 header.append(QString("\"POT-Creation-Date: %1\\n\"\n").arg(tstamp)); 00093 header.append("\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n"); 00094 header.append("\"Last-Translator: \\n\"\n"); 00095 header.append("\"Language-Team: "NSH2PO_LANGUAGE_TEAM"\\n\"\n"); 00096 header.append("\"MIME-Version: 1.0\\n\"\n"); 00097 header.append(QString("\"Content-Type: text/plain; " 00098 "charset=%1\\n\"\n").arg(charset)); 00099 header.append("\"Content-Transfer-Encoding: 8bit\\n\"\n"); 00100 header.append("\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n"); 00101 header.append("\"X-Generator: Vidalia nsh2po "NSH2PO_VERSION"\\n\"\n"); 00102 header.append("\n"); 00103 00104 return header; 00105 } 00106 00107 /** Read and return the next non-empty line from <b>stream</b>. */ 00108 QString 00109 read_next_line(QTextStream *stream) 00110 { 00111 Q_ASSERT(stream); 00112 stream->skipWhiteSpace(); 00113 return stream->readLine(); 00114 } 00115 00116 /** Skip past the header portion of the POT file and any leading whitespace. 00117 * The next line read from <b>po</b> will be the first non-header line in the 00118 * document. */ 00119 void 00120 skip_pot_header(QTextStream *pot) 00121 { 00122 QString line; 00123 /* Skip any leading whitespace before the header */ 00124 pot->skipWhiteSpace(); 00125 /* Read to the first empty line */ 00126 line = pot->readLine(); 00127 while (!pot->atEnd() && !line.isEmpty()) 00128 line = pot->readLine(); 00129 } 00130 00131 /** Parse a PO template file for (context,source string) pairs, which are 00132 * be stored in <b>out</b> using <i>msgctxt</i> as the key and <i>msgid</i> 00133 * as the value. Return true on success, or false on failure and set 00134 * <b>errmsg</b>. */ 00135 bool 00136 parse_po_template(QTextStream *pot, QHash<QString,QString> *out, 00137 QString *errmsg) 00138 { 00139 QString line, msgctxt, msgid; 00140 00141 skip_pot_header(pot); 00142 line = read_next_line(pot); 00143 while (!pot->atEnd()) { 00144 if (!line.startsWith("#:") && !line.startsWith("msgctxt")) { 00145 /* Skip to the start of the next message entry */ 00146 line = read_next_line(pot); 00147 continue; 00148 } 00149 00150 if (line.startsWith("#:")) { 00151 /* Context was specified with the stupid overloaded "#:" syntax.*/ 00152 msgctxt = line.section(" ", 1); 00153 msgctxt = parse_message_context_lame(msgctxt); 00154 line = read_next_line(pot); 00155 continue; 00156 } 00157 00158 if (line.startsWith("msgctxt ")) { 00159 /* A context specified on a "msgctxt" line takes precedence over a 00160 * context specified using the overload "#:" notation. */ 00161 msgctxt = line.section(" ", 1); 00162 msgctxt = parse_message_context(msgctxt); 00163 line = read_next_line(pot); 00164 } 00165 00166 if (!line.startsWith("msgid ")) { 00167 *errmsg = "expected 'msgid' line"; 00168 return false; 00169 } 00170 msgid = line.section(" ", 1); 00171 00172 line = read_next_line(pot); 00173 while (line.startsWith("\"")) { 00174 /* This msgid line had multiple parts to it */ 00175 msgid.append(line); 00176 line = read_next_line(pot); 00177 } 00178 msgid = parse_message_string(msgid); 00179 00180 out->insert(msgctxt, msgid); 00181 } 00182 00183 return true; 00184 } 00185 00186 /** Read an NSIS-formatted file containing LangString entries from <b>nsh</b>. 00187 * If a LangString entry has a corresponding entry in <b>pot</b>, then the 00188 * message entry is PO-formatted and appended to <b>po</b>. Return true on 00189 * success, or false on failure and <b>errmsg</b> will be set. */ 00190 int 00191 nsh2po(QTextStream *nsh, const QString &charset, 00192 const QHash<QString,QString> &pot, QString *po, QString *errmsg) 00193 { 00194 QString line, msgctxt, msgid, msgstr; 00195 QStringList parts; 00196 QHash<QString,QString> langStrings; 00197 int idx, n_strings; 00198 00199 *po = create_po_header(charset); 00200 00201 /* Parse the translated strings from the NSH file */ 00202 while (!nsh->atEnd()) { 00203 line = read_next_line(nsh); 00204 if (!line.startsWith("LangString ")) 00205 continue; 00206 00207 parts = line.split(" "); 00208 if (parts.size() > 3) 00209 msgctxt = parts.at(1); 00210 else 00211 continue; /* Not properly formatted */ 00212 00213 idx = line.indexOf("\""); 00214 if (idx > 0) 00215 msgstr = parse_nsh_langstring(line.mid(idx)); 00216 langStrings.insert(msgctxt, msgstr); 00217 } 00218 00219 /* Format the PO file based on the template. */ 00220 n_strings = 0; 00221 foreach (QString msgctxt, pot.keys()) { 00222 msgid = pot.value(msgctxt); 00223 if (langStrings.contains(msgctxt)) { 00224 msgstr = langStrings.value(msgctxt); 00225 n_strings++; 00226 } else { 00227 msgstr = msgid; 00228 } 00229 00230 po->append(QString("msgctxt \"%1\"\n").arg(msgctxt)); 00231 po->append(QString("msgid \"%1\"\n").arg(msgid)); 00232 po->append(QString("msgstr \"%1\"\n").arg(msgstr)); 00233 po->append("\n"); 00234 } 00235 return n_strings; 00236 } 00237 00238 /** Write <b>po</b> to <b>poFileName</b> using <b>codec</b>. Return true on 00239 * success. On failure, return false and set <b>errmsg</b> to the reason for 00240 * failure. */ 00241 bool 00242 write_po_output(const char *poFileName, const QString &po, QTextCodec *codec, 00243 QString *errmsg) 00244 { 00245 QFile poFile(poFileName); 00246 if (!poFile.open(QIODevice::WriteOnly | QIODevice::Text)) { 00247 *errmsg = QString("Unable to open '%1' for writing.").arg(poFileName); 00248 return false; 00249 } 00250 00251 QTextStream out(&poFile); 00252 out.setCodec(codec); 00253 out << po; 00254 return true; 00255 } 00256 00257 /** Display application usage and exit. */ 00258 void 00259 print_usage_and_exit() 00260 { 00261 QTextStream error(stderr); 00262 error << "usage: nsh2po [-q] -t <template.pot> -i <infile.nsh> " 00263 "-o <outfile.po> [-c <encoding>]\n"; 00264 error << " -q (optional) Quiet mode (errors are still displayed)\n"; 00265 error << " -t <template.pot> PO template file\n"; 00266 error << " -i <infile.ts> Input .ts file\n"; 00267 error << " -o <outfile.po> Output .po file\n"; 00268 error << " -c <encoding> Text encoding (default: utf-8)\n"; 00269 error.flush(); 00270 exit(1); 00271 } 00272 00273 int 00274 main(int argc, char *argv[]) 00275 { 00276 QTextStream error(stderr); 00277 QString po, errorMessage; 00278 char *outFileName; 00279 QFile potFile, nshFile; 00280 QTextStream pot, nsh; 00281 QTextCodec *codec = QTextCodec::codecForName("utf-8"); 00282 bool quiet = false; 00283 00284 /* Check for the correct number of input parameters. */ 00285 if (argc < 7 || argc > 10) 00286 print_usage_and_exit(); 00287 for (int i = 1; i < argc; i++) { 00288 QString arg(argv[i]); 00289 if (!arg.compare("-q", Qt::CaseInsensitive)) { 00290 quiet = true; 00291 } else if (!arg.compare("-t", Qt::CaseInsensitive) && ++i < argc) { 00292 /* Open the input PO template file */ 00293 potFile.setFileName(argv[i]); 00294 if (!potFile.open(QIODevice::ReadOnly | QIODevice::Text)) { 00295 error << QString("Couldn't open '%1' for reading: ").arg(argv[i]) 00296 << potFile.errorString(); 00297 return 1; 00298 } 00299 pot.setDevice(&potFile); 00300 } else if (!arg.compare("-i", Qt::CaseInsensitive) && ++i < argc) { 00301 /* Open the input NSH file */ 00302 nshFile.setFileName(argv[i]); 00303 if (!nshFile.open(QIODevice::ReadOnly | QIODevice::Text)) { 00304 error << QString("Couldn't open '%1' for reading: ").arg(argv[i]) 00305 << nshFile.errorString(); 00306 return 1; 00307 } 00308 nsh.setDevice(&nshFile); 00309 } else if (!arg.compare("-o", Qt::CaseInsensitive) && ++i < argc) { 00310 outFileName = argv[i]; 00311 } else if (!arg.compare("-c", Qt::CaseInsensitive) && ++i < argc) { 00312 /* Set the text encoding used for input and output */ 00313 codec = QTextCodec::codecForName(argv[i]); 00314 if (!codec) { 00315 error << "Invalid text encoding specified.\n"; 00316 return 1; 00317 } 00318 } else 00319 print_usage_and_exit(); 00320 } 00321 pot.setCodec(codec); 00322 nsh.setCodec(codec); 00323 00324 /* Parse the template for the source strings */ 00325 QHash<QString,QString> poTemplate; 00326 if (!parse_po_template(&pot, &poTemplate, &errorMessage)) { 00327 error << QString("Failed to parse PO template: %1\n").arg(errorMessage); 00328 return 1; 00329 } 00330 00331 /* Parse the nsh for the translated strings */ 00332 int n_strings = nsh2po(&nsh, QString(codec->name()), poTemplate, 00333 &po, &errorMessage); 00334 if (n_strings < 0) { 00335 error << QString("Conversion failed: %1\n").arg(errorMessage); 00336 return 2; 00337 } 00338 00339 /* Write the formatted PO output */ 00340 if (!write_po_output(outFileName, po, codec, &errorMessage)) { 00341 error << QString("Failed to write PO output: %1\n").arg(errorMessage); 00342 return 3; 00343 } 00344 00345 if (!quiet) { 00346 QTextStream out(stdout); 00347 out << QString("Wrote %1 strings to '%2'.\n").arg(n_strings) 00348 .arg(outFileName); 00349 } 00350 return 0; 00351 } 00352