jsxgettext-andris
Version:
Extracts gettext strings from JavaScript, EJS, Jade, Jinja and Handlebars files. A fork of https://github.com/zaach/jsxgettext
270 lines (223 loc) • 7.86 kB
JavaScript
;
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var fs = require('fs');
var path = require('path');
var parser = require('acorn-jsx');
var walk = require('acorn/dist/walk');
var gettextParser = require('gettext-parser');
var regExpEscape = require('escape-string-regexp');
var walkBase = Object.assign({}, walk.base, {
JSXElement: function (node, st, c) {
var i;
for (i = 0; i < node.openingElement.attributes.length; i++) {
c(node.openingElement.attributes[i], st);
}
for (i = 0; i < node.children.length; i++) {
c(node.children[i], st);
}
},
JSXAttribute: function (node, st, c) {
if (node.value.type === 'JSXExpressionContainer') {
c(node.value, st);
}
},
JSXSpreadAttribute: function (node, st, c) {
c(node.argument, st);
},
JSXExpressionContainer: function (node, st, c) {
c(node.expression, st);
},
JSXEmptyExpression: function () {}
});
function isStringLiteral(node) {
return node.type === 'Literal' && (typeof node.value === 'string');
}
function isStrConcatExpr(node) {
var left = node.left;
var right = node.right;
return node.type === "BinaryExpression" && node.operator === '+' && (
(isStringLiteral(left) || isStrConcatExpr(left)) &&
(isStringLiteral(right) || isStrConcatExpr(right))
);
}
function getTranslatable(node, options) {
// must be a call expression with arguments
if (!node.arguments)
return false;
var callee = node.callee;
var funcName = callee.name;
var arg = node.arguments[0];
var prop;
if (!funcName) {
if (callee.type !== 'MemberExpression')
return false;
// Special case for functionName.call calls
if (callee.property.name === 'call') {
prop = callee.object.property;
funcName = callee.object.name || prop && (prop.name || prop.value);
node.arguments = node.arguments.slice( 1 ); // skip context object
arg = node.arguments[0];
} else {
funcName = callee.property.name;
}
}
if (options.keyword.indexOf(funcName) === -1)
return false;
// If the gettext function's name starts with "n" (i.e. ngettext or n_) and its first 2 arguments are strings, we regard it as a plural function
if (arg && funcName.substr(0, 1) === "n" && (isStrConcatExpr(arg) || isStringLiteral(arg)) && node.arguments[1] && (isStrConcatExpr(node.arguments[1]) || isStringLiteral(node.arguments[1])))
return [arg, node.arguments[1]];
if (arg && (isStrConcatExpr(arg) || isStringLiteral(arg)))
return arg;
if (options.sanity)
throw new Error("Could not parse translatable: " + JSON.stringify(arg, null, 2));
}
// Assumes node is either a string Literal or a strConcatExpression
function extractStr(node) {
if (isStringLiteral(node))
return node.value;
else
return extractStr(node.left) + extractStr(node.right);
}
function loadStrings(poFile) {
try {
return gettextParser.po.parse(fs.readFileSync(path.resolve(poFile)), "utf-8");
} catch (e) {
return null;
}
}
function parse(sources, options) {
var useExisting = options.joinExisting;
var poJSON;
if (useExisting)
poJSON = loadStrings(path.resolve(path.join(options.outputDir || '', options.output)));
if (!poJSON) {
var headers = {
"project-id-version": options.projectIdVersion || "PACKAGE VERSION",
"language-team": "LANGUAGE <LL@li.org>",
"report-msgid-bugs-to": options.reportBugsTo,
"po-revision-date": "YEAR-MO-DA HO:MI+ZONE",
"language": "",
"mime-version": "1.0",
"content-type": "text/plain; charset=utf-8",
"content-transfer-encoding": "8bit"
};
poJSON = {
charset: "utf-8",
headers: headers,
translations: {'': {} }
};
}
var translations;
try {
poJSON.headers["pot-creation-date"] = new Date().toISOString().replace('T', ' ').replace(/:\d{2}.\d{3}Z/, '+0000');
// Always use the default context for now
// TODO: Take into account different contexts
translations = poJSON.translations[''];
} catch (err) {
if (useExisting)
throw new Error("An error occurred while using the provided PO file. Please make sure it is valid by using `msgfmt -c`.");
else
throw err;
}
if( options.keyword ) {
Object.keys(options.keyword).forEach(function (index) {
options.keyword.push('n' + options.keyword[index]);
});
}
else {
options.keyword = ['gettext', 'ngettext'];
}
var tagName = options.addComments || "L10n:";
var commentRegex = new RegExp([
"^\\s*" + regExpEscape(tagName), // The "TAG" provided externally or "L10n:" by default
"^\\/" // The "///" style comments which is the xgettext standard
].join("|"));
Object.keys(sources).forEach(function (filename) {
var source = sources[filename].replace(/^#.*/, ''); // strip leading hash-bang
var astComments = [];
var ast = parser.parse(source, {
ecmaVersion: 6,
sourceType: 'module',
plugins: { jsx: { allowNamespaces: false } },
onComment: function (block, text, start, end, line/*, column*/) {
text = text.match(commentRegex) && text.replace(/^\//, '').trim();
if (!text)
return;
astComments.push({
line : line,
value: text
});
},
locations: true
});
// finds comments that end on the previous line
function findComments(comments, line) {
return comments.map(function (node) {
var commentLine = node.line.line;
if (commentLine === line || commentLine + 1 === line) {
return node.value;
}
}).filter(Boolean).join('\n');
}
walk.simple(ast, {'CallExpression': function (node) {
var arg = getTranslatable(node, options);
if (!arg)
return;
var msgid = arg;
if( arg.constructor === Array )
msgid = arg[0];
var str = extractStr(msgid);
var line = node.loc.start.line;
var comments = findComments(astComments, line);
var ref = filename + ':' + line;
if (!translations[str]) {
translations[str] = {
msgid: str,
msgstr: [],
comments: {
extracted: comments,
reference: ref
}
};
if( arg.constructor === Array ) {
translations[str].msgid_plural = extractStr(arg[1]);
translations[str].msgstr = ['', ''];
}
} else {
if(translations[str].comments) {
translations[str].comments.reference += '\n' + ref;
}
if (comments)
translations[str].comments.extracted += '\n' + comments;
}
}
}, walkBase);
function dedupeNCoalesce(item, i, arr) {
return item && arr.indexOf(item) === i;
}
Object.keys(translations).forEach(function (msgid) {
var comments = translations[msgid].comments;
if (!comments)
return;
if (comments.reference)
comments.reference = comments.reference.split('\n').filter(dedupeNCoalesce).join('\n');
if (comments.extracted)
comments.extracted = comments.extracted.split('\n').filter(dedupeNCoalesce).join('\n');
});
});
return poJSON;
}
exports.parse = parse;
// generate extracted strings file
function gen(sources, options) {
return gettextParser.po.compile(parse(sources, options)).toString();
}
exports.generate = gen;
// Backwards compatibility interface for 0.3.x - Deprecated!
var parsers = require('./parsers');
Object.keys(parsers).forEach(function (parser) {
parser = parsers[parser];
exports['generateFrom' + parser.name] = parser;
});