var fs = require('fs'); var nodes = require('../node_modules/nunjucks/src/nodes'); var uglify = require('uglify-js'); function L10nContext() { var localizable_strings = []; var localizable_string_map = {}; function LocalString(string) { this.str = string; this.comment = null; this.plural = null; this.locations = []; this.pushLocation = function(location) { while (location.indexOf('../') !== -1) { location = location.replace(/\.\.\/[a-zA-Z]+/g, ''); } this.locations.push(location); }; this.escaped = function(str) { return JSON.stringify(str || this.str); }; this.toString = function() { var out = []; if (this.comment) { out.push('#. ' + this.comment.replace(/\n/g, '\n#. ').trim()); } out = out.concat([ '#: ' + this.locations.join('\n#: '), 'msgid ' + this.escaped() ]); if (this.plural) { out.push('msgid_plural ' + this.escaped(this.plural)); out.push('msgstr[0] ""'); out.push('msgstr[1] ""'); } else { out.push('msgstr ""'); } return out.join('\n'); }; this.allStrings = function() { if (this.plural) { return [this.str, this.plural]; } return [this.str]; }; } function normalize_string(string) { string = string.replace(/\n/g, ''); string = string.replace(/\t/g, ' '); string = string.replace(/\s\s+/g, ' '); string = string.replace(/^\s+/, ''); string = string.replace(/\s+$/, ''); return string; } function save_singular(normalized, location, comment) { comment = comment || null; normalized = normalize_string(normalized); var ls; if (normalized in localizable_string_map) { ls = localizable_string_map[normalized]; } else { ls = new LocalString(normalized); ls.comment = comment; localizable_string_map[normalized] = ls; localizable_strings.push(ls); } ls.locations.push(location); } function extract_singular(node, filename, comment) { if (!node.args.children.length) { throw new Error( 'No string supplied for localization (line ' + node.lineno + ')'); } var string_node = node.args.children[0]; if (!(string_node instanceof nodes.Literal)) { throw new Error( 'Cannot localize string (line ' + node.lineno + ')'); } save_singular(string_node.value, filename + ':' + node.lineno, comment); } function save_plural(norm_singular, norm_plural, location, comment) { norm_singular = normalize_string(norm_singular); norm_plural = normalize_string(norm_plural); var ls; if (norm_singular in localizable_string_map && localizable_string_map[norm_singular].plural === norm_plural) { ls = localizable_string_map[norm_singular]; } else { ls = new LocalString(norm_singular); ls.plural = norm_plural; ls.comment = comment; localizable_string_map[norm_singular] = ls; localizable_strings.push(ls); } ls.locations.push(location); } function extract_plural(node, filename, comment) { if (node.args.children.length < 3) { throw new Error( 'Invalid plural localization. Must have singular, plural, and parameters. (line ' + string_node.lineno + ')'); } if (!(node.args.children[0] instanceof nodes.Literal)) { throw new Error( 'Cannot localize singular string (line ' + node.lineno + ')'); } if (!(node.args.children[1] instanceof nodes.Literal)) { throw new Error( 'Cannot localize plural string (line ' + node.lineno + ')'); } save_plural(node.args.children[0].value, node.args.children[1].value, filename + ':' + node.lineno, comment); } function find_comments(doc) { function getLineNo(index) { return doc.substr(0, index).split('\n').length; } var patterns = [ /\{#\s*L10n(\(.+\))?:((.|\n)+?)\s*#\}/im, /\/\/ L10n(\(.+\))?:(.+)$/im, /\/\*\s*L10n(\(.+\))?:((.|\n)+?)\s*\*\//im ]; var comments = []; for (var i = 0, p; p = patterns[i++];) { var match; //console.log('Matching', p); var pat_doc = doc; while (match = p.exec(pat_doc)) { pat_doc = pat_doc.substr(match.index + match[0].length); comments.push({ line: getLineNo(match.index), paren: match[1], body: match[2] }); } } comments.sort(function(a, b) {return a.line - b.line;}); function formatComment(comment) { if (comment.paren) { return '(' + comment.paren + '): ' + comment.body; } else { return comment.body; } } return function(line) { if (!comments.length) { return null; } if (comments[0].line > line) { return null; } var comment = comments.shift(); if (!comments.length) { return formatComment(comment); } while (comments.length && comments[0].line <= line) { comment = comments.shift(); } return formatComment(comment); }; } this.extract_template = function(data, parseTree, filename) { var comments = find_comments(data); function extract_callextension(tree) { var calls = []; var defers = tree.findAll(nodes.CallExtension); for (var i = 0, e; e = defers[i++];) { for (var j = 0, arg; arg = e.contentArgs[j++];) { if (!arg) { continue; } calls = calls.concat(arg.findAll(nodes.FunCall)); calls = calls.concat(extract_callextension(arg)); } } return calls; } var calls = parseTree.findAll(nodes.FunCall); calls = calls.concat(extract_callextension(parseTree)); for (var i = 0, node; node = calls[i++];) { // Exclude function calls that aren't to gettext. var node_name = node.name; if (!node_name || !(node_name instanceof nodes.Symbol) || !(node_name.value === '_' || node_name.value === '_plural')) { continue; } var comment = comments(node.lineno); switch (node_name.value) { case '_': extract_singular(node, filename, comment); break; case '_plural': extract_plural(node, filename, comment); break; } } }; this.extract_js = function(data, filename) { var comments = find_comments(data); uglify.parse( data, {filename: filename, toplevel: null} ).walk(new uglify.TreeWalker(function(node) { if (node instanceof uglify.AST_Call && (node.start.value === 'gettext' || node.start.value === 'ngettext')) { var args = node.args; var raw_location = node.start.file + ':' + node.start.line; var location = '[' + raw_location + ']'; if (!args.length) { console.error(node.start.value + ' with no arguments '); return; } if (typeof args[0].value !== 'string') { console.error('Invalid ' + node.start.value + ' call: Not a string ' + location); return; } var comment = comments(node.start.line); if (node.start.value === 'gettext') { save_singular(args[0].value, raw_location, comment); } else if (node.start.value === 'ngettext') { if (args.length < 3) { console.error('Invalid ngettext call: not enough parameters ' + location); return; } if (typeof args[1].value !== 'string') { console.error('Invlid ngettext call: plural form not string ' + location); return; } save_plural(args[0].value, args[1].value, raw_location, comment); } } })); }; var stringHash; function getHash() { if (stringHash) return stringHash; var values = localizable_strings.reduce(function(a, b) { return a.concat(b); }, []).sort().join('\n'); var crypto = require('crypto'); return stringHash = crypto.createHash('md5').update(values).digest("hex"); } this.hasChanged = function(path) { if (!fs.existsSync(path)) return true; var previous = fs.readFileSync(path) + ''; var current = getHash(); return previous !== current; }; this.saveHash = function(path) { fs.writeFile(path, getHash()); return getHash(); }; this.save_po = function(path, callback) { var data = [ '#, fuzzy', 'msgid ""', 'msgstr ""', '"Project-Id-Version: PACKAGE VERSION\\n"', '"Report-Msgid-Bugs-To: \\n"', '"POT-Creation-Date: ' + (new Date()).toISOString() + '\\n"', '"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"', '"Last-Translator: FULL NAME \\n"', '"Language-Team: LANGUAGE \\n"', '"MIME-Version: 1.0\\n"', '"Plural-Forms: nplurals=2; plural=(n != 1);"', '"Content-Type: text/plain; charset=utf-8\\n"', '"Content-Transfer-Encoding: 8bit\\n"', '"X-Generator: Fireplace L10n Tools 1.0\\n"', '', '' ].join('\n') + localizable_strings.join('\n\n'); fs.writeFile(path, data, callback); }; this.string_count = function() { return localizable_strings.length; }; } module.exports.L10nContext = L10nContext;