// Jala Project [http://opensvn.csie.org/traccgi/jala] // // Copyright 2004 ORF Online und Teletext GmbH. // // Licensed under the Apache License, Version 2.0 (the ``License''); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an ``AS IS'' BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @fileoverview * MessageParser script that extracts all gettext message macros * out of skin files and all calls of gettext functions * (that is "gettext", "ngettext" and "_") out of function * files and directly generates a .pot file from it. * If an argument "-o" is given and it is followed by * a path to a file, the output is written to this file. * Any other argument is interpreted as directory or file * that should be parsed. */ /** * @constructor */ var Message = function(id, pluralId) { this.id = id && String(id); this.pluralId = pluralId && String(pluralId); this.locations = []; return this; }; /** * Static method that constructs a message key by * which a message can be identified in the messages map. * @param {String} id The message Id * @param {String} pluralId The plural message Id * @returns The generated message key * @type String */ Message.getKey = function(id, pluralId) { if (id && pluralId) { return id + pluralId; } else { return id; } }; /** * Encloses the string passed as argument in quotes * and wraps the string if it is longer than 80 characters. * @param {String} str The string to format * @param {Boolean} wrap If true the message string will be splitted in * parts where each one is max. 80 characters long * @returns The formatted string. * @type String */ Message.formatId = function(str, wrap) { var escapeQuotes = function(s) { return s.replace(/(^|[^\\])"/g, '$1\\"'); }; var len = 80; var buf = new java.lang.StringBuffer(); if (wrap == true && str.length > len) { buf.append('""\n'); var offset = 0; while (offset < str.length) { buf.append('"'); buf.append(escapeQuotes(str.substring(offset, offset += len))); buf.append('"'); buf.append("\n"); } return buf.toString(); } else { buf.append('"'); buf.append(escapeQuotes(str)); buf.append('"\n'); } return buf.toString(); }; /** * Adds a new location to this Message instance. * @param {String} filePath The path to the file this message * is located in. * @param {Number} lineNum The line number at which this message * was found at */ Message.prototype.addLocation = function(filePath, lineNum) { const basePath = java.nio.file.Paths.get(java.io.File(app.dir).getCanonicalPath()); const locationPath = java.nio.file.Paths.get(filePath); const relativePath = basePath.relativize(locationPath); this.locations.push(relativePath + ":" + lineNum); }; /** * Writes this Message instance as .po compatible string to * the StringBuffer passed as argument. * @param {java.lang.StringBuffer} buf The StringBuffer instance * to write into */ Message.prototype.write = function(buf) { for (var i=0;i -1 || (this.pluralId != null && this.pluralId.indexOf("{") > -1)) { buf.append("#, java-format\n"); } buf.append('msgid '); buf.append(Message.formatId(this.id)); if (this.pluralId != null) { buf.append('msgid_plural '); buf.append(Message.formatId(this.pluralId)); buf.append('msgstr[0] ""\nmsgstr[1] ""\n') } else { buf.append('msgstr ""\n') } buf.append("\n"); return; }; /** * @constructor */ var MessageParser = function() { this.messages = {}; return this; }; /** * Object containing the accepted function names, currently * supported are "gettext", "ngettext" and "_". This is used * as a lookup map during function file parsing. * @type Object */ MessageParser.FUNCTION_NAMES = { "_": true, "gettext": true, "ngettext": true, "markgettext": true, "cgettext": true }; /** * The name of the gettext macro * @type String */ MessageParser.MACRO_NAME = "message"; /** * The name of the macro attribute that will be interpreted * as gettext attribute. * @type String */ MessageParser.ATTRIBUTE_NAME = MessageParser.MACRO_NAME; /** * A regular expression for parsing macros in a skin. The result * of this regular expression contains: * result[1] = macro handler name (can be empty for global macros) * result[2] = macro name * result[3] = the macro's attributes * @type RegExp */ MessageParser.REGEX_MACRO = /<%\s*(?:([\w]+)\.)?([\w]+)\s+([^%]+?)\s*%>/gm; /** * A regular expression for parsing the attributes of a macro. The result * of this regular expression contains: * result[1] = attribute name * result[2] = attribute value * @type RegExp */ MessageParser.REGEX_PARAM = /([\w]*)\s*=\s*["'](.*?)["']\s*(?=\w+=|$)/gm; /** * Calculates the line number in the string passed as argument * at which the specified index position is located. * @param {String} str The source string * @param {Number} idx The index position to get the line number for. * @returns The line number of the index position in the source string. * @type Number */ MessageParser.getLineNum = function(str, idx) { return str.substring(0, idx).split(/.*(?:\r\n|\n\r|\r|\n)/).length; }; /** * Parses the file passed as argument. If the file * is a directory, this method recurses down the directory * tree and parses all skin and function files. * @param {java.io.File} file The file or directory to start at. * @param {String} encoding The encoding to use */ MessageParser.prototype.parse = function(file, encoding) { if (file.isDirectory()) { var list = file.list(); for (var i=0;i -1) { switch (String(fName.substring(dotIdx+1))) { case "skin": print("Parsing skin file " + file.getCanonicalPath() + "..."); this.parseSkinFile(file, encoding); break; case "hac": case "js": print("Parsing function file " + file.getCanonicalPath() + "..."); this.parseFunctionFile(file, encoding); break; default: break; } } } return; }; /** @ignore */ MessageParser.prototype.toString = function() { return "[Jala Message Parser]"; }; /** * Parses a .js file and creates Message instances for all * calls of "gettext", "ngettext", "markgettext" and "_". * @param {java.io.File} file The function file to parse * @param {String} encoding The encoding to use */ MessageParser.prototype.parseFunctionFile = function(file, encoding) { var fis = new java.io.FileInputStream(file); var isr = new java.io.InputStreamReader(fis, encoding || "UTF-8"); var reader = new java.io.BufferedReader(isr); var tokenizer = new java.io.StreamTokenizer(reader); var messages = [], stack = []; var c; while ((c = tokenizer.nextToken()) != java.io.StreamTokenizer.TT_EOF) { switch (c) { case java.io.StreamTokenizer.TT_WORD: if (MessageParser.FUNCTION_NAMES[tokenizer.sval] == true) { stack.push({name: tokenizer.sval, lineNr: tokenizer.lineno()}); } else if (stack.length > 0) { // it's something else than a string argument inside a gettext method call // so finalize the argument parsing here as we aren't interested in that messages.push(stack.pop()); } break; case java.io.StreamTokenizer.TT_NUMBER: break; default: if (stack.length > 0) { if ("\u0028".charCodeAt(0) == c) { // start of arguments (an opening bracket) stack[stack.length-1].args = []; } else if ("\u0029".charCodeAt(0) == c) { // end of arguments (a closing bracket) messages.push(stack.pop()); } else if ("\u0022".charCodeAt(0) == c || "\u0027".charCodeAt(0) == c) { // a quoted string argument stack[stack.length-1].args.push(tokenizer.sval); } } break; } } if (messages.length > 0) { var msgParam, key, msg; for (var i=0;i 0) { if (msgParam.name === "cgettext" || msgParam.name === "markgettext") { msgParam.args[0] = cgettext.getKey(msgParam.args[0], msgParam.args[1]); delete msgParam.args[1]; } key = Message.getKey(msgParam.args[0]); if (!(msg = this.messages[key])) { this.messages[key] = msg = new Message(msgParam.args[0], msgParam.args[1]); } if (!msg.pluralId && msgParam.args.length > 1) { msg.pluralId = msgParam.args[1]; } msg.addLocation(file.getCanonicalPath(), msgParam.lineNr); } } } fis.close(); isr.close(); reader.close(); return; }; /** * Parses a skin file and creates Message instances for * all macros which name is either "message" or * that have attributes named "message" and optional * "plural" * @param {java.io.File} file The skin file to parse * @param {String} encoding The encoding to use */ MessageParser.prototype.parseSkinFile = function(file, encoding) { var self = this; var source = readFile(file.getAbsolutePath(), encoding || "UTF-8"); var checkNestedMacros = function(iterator) { var macros = []; while (iterator.hasNext()) { macro = iterator.next(); if (macro && macro.constructor !== String) { macros.push(macro); } } processMacros(macros); } var processMacros = function(macros) { var re = gettext_macro.REGEX; var id, pluralId, name, args, param, key, msg; for (let macro of macros) { id = pluralId = null; name = macro.getName(); param = macro.getNamedParams(); if (param) { checkNestedMacros(param.values().iterator()); if (name === MessageParser.MACRO_NAME) { id = param.get("text"); pluralId = param.get("plural"); } else if (param.containsKey("message") === MessageParser.ATTRIBUTE_NAME) { id = param.get("message"); pluralId = param.get("plural"); } } args = macro.getPositionalParams(); if (args) { checkNestedMacros(args.iterator()); if (name === "gettext" || name === "markgettext") { id = cgettext.getKey(args.get(0), param && param.get("context")); } else if (name === "ngettext") { id = args.get(0); pluralId = args.get(1); } } if (id != null) { if (id.constructor !== String) { continue; } // create new Message instance or update the existing one id = id.replace(re, String.SPACE); pluralId && (pluralId = pluralId.replace(re, String.SPACE)); key = Message.getKey(id); if (!(msg = self.messages[key])) { self.messages[key] = msg = new Message(id, pluralId, file.getCanonicalPath()); } msg.addLocation(file.getCanonicalPath(), MessageParser.getLineNum(source, macro.start)); } } } var skin = createSkin(source); if (skin.hasMainskin()) { processMacros(skin.getMacros()); } for (let name of skin.getSubskinNames()) { var subskin = skin.getSubskin(name); processMacros(subskin.getMacros()); } return; } /** * Prints a standard Header of a .po file * FIXME: Allow custom header (template?) * FIXME: why the hell is Plural-Forms ignored in poEdit? * @see http://drupal.org/node/17564 */ MessageParser.prototype.getPotString = function() { var date = new Date; var buf = new java.lang.StringBuffer(); buf.append('#\n'); buf.append('# The Antville Project\n'); buf.append('# http://code.google.com/p/antville\n'); buf.append('#\n'); buf.append('# Copyright 2001-' + date.getFullYear() + ' by the Workers of Antville.\n'); buf.append('#\n'); buf.append("# Licensed under the Apache License, Version 2.0 (the ``License''\n"); buf.append('# you may not use this file except in compliance with the License.\n'); buf.append('# You may obtain a copy of the License at\n'); buf.append('#\n'); buf.append('# http://www.apache.org/licenses/LICENSE-2.0\n'); buf.append('#\n'); buf.append('# Unless required by applicable law or agreed to in writing, software\n'); buf.append("# distributed under the License is distributed on an ``AS IS'' BASIS,\n"); buf.append('# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n'); buf.append('# See the License for the specific language governing permissions and\n'); buf.append('# limitations under the License.\n'); buf.append('#\n\n'); buf.append('#, fuzzy\n'); buf.append('msgid ""\n'); buf.append('msgstr ""\n'); buf.append('"Project-Id-Version: Antville-' + Root.VERSION + '\\n"\n'); buf.append('"Report-Msgid-Bugs-To: mail@antville.org\\n"\n'); var sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mmZ"); buf.append('"POT-Creation-Date: ' + sdf.format(new java.util.Date()) + '\\n"\n'); buf.append('"PO-Revision-Date: ' + sdf.format(new java.util.Date()) + '\\n"\n'); //buf.append('"Last-Translator: FULL NAME \\n"\n'); buf.append('"Language-Team: The Antville People \\n"\n'); buf.append('"MIME-Version: 1.0\\n"\n'); buf.append('"Content-Type: text/plain; charset=utf-8\\n"\n'); buf.append('"Content-Transfer-Encoding: 8bit\\n"\n'); buf.append('"Plural-Forms: nplurals=2; plural=(n != 1);\\n"\n'); buf.append('\n'); // sort all messages by their singular key var keys = []; for (var i in this.messages) { keys[keys.length] = this.messages[i].id; } keys.sort(); // add all the messages for (var i=0;i