Skip to content

Commit

Permalink
Add JSX support
Browse files Browse the repository at this point in the history
  • Loading branch information
ruscoder committed Mar 31, 2016
1 parent 84753f0 commit 1b0d79a
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 40 deletions.
131 changes: 93 additions & 38 deletions lib/jsxgettext.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,50 @@
* 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 fs = require('fs');
var path = require('path');

var parser = require('acorn');
var traverse = require('acorn/dist/walk').simple;
var parser = require('acorn-jsx');
var walk = require('acorn/dist/walk');
var gettextParser = require('gettext-parser');
var regExpEscape = require('escape-string-regexp');
var regExpEscape = require('escape-string-regexp');
var objectAssign = require('object-assign');

var jsxBase = {
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 () {},

ClassProperty: function () {},

BindExpression: function () {},

AwaitExpression: function () {},
};

function isStringLiteral(node) {
return node.type === 'Literal' && (typeof node.value === 'string');
Expand All @@ -23,54 +60,61 @@ function isStrConcatExpr(node) {
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)
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')
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
node.arguments = node.arguments.slice(1); // skip context object
arg = node.arguments[0];
} else {
funcName = callee.property.name;
}
}

if (options.keyword.indexOf(funcName) === -1)
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])))
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)))
if (arg && (isStrConcatExpr(arg) || isStringLiteral(arg))) {
return arg;
}

if (options.sanity)
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))
if (isStringLiteral(node)) {
return node.value;
else
} else {
return extractStr(node.left) + extractStr(node.right);
}
}

function loadStrings(poFile) {
Expand All @@ -84,8 +128,9 @@ function loadStrings(poFile) {
function parse(sources, options) {
var useExisting = options.joinExisting;
var poJSON;
if (useExisting)
if (useExisting) {
poJSON = loadStrings(path.resolve(path.join(options.outputDir || '', options.output)));
}

if (!poJSON) {
var headers = {
Expand All @@ -102,7 +147,7 @@ function parse(sources, options) {
poJSON = {
charset: "utf-8",
headers: headers,
translations: {'': {} }
translations: { '': {} }
};
}

Expand All @@ -115,13 +160,14 @@ function parse(sources, options) {
// TODO: Take into account different contexts
translations = poJSON.translations[''];
} catch (err) {
if (useExisting)
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
} else {
throw err;
}
}

if( options.keyword ) {
if (options.keyword) {
Object.keys(options.keyword).forEach(function (index) {
options.keyword.push('n' + options.keyword[index]);
});
Expand All @@ -135,18 +181,21 @@ function parse(sources, options) {
"^\\/" // 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 source = sources[filename].replace(/^#.*/, ''); // strip leading hash-bang
var astComments = [];
var ast = parser.parse(source, {
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)
if (!text) {
return;
}

astComments.push({
line : line,
line: line,
value: text
});
},
Expand All @@ -163,14 +212,17 @@ function parse(sources, options) {
}).filter(Boolean).join('\n');
}

traverse(ast, {'CallExpression': function (node) {
walk.simple(ast, {
'CallExpression': function (node) {
var arg = getTranslatable(node, options);
if (!arg)
if (!arg) {
return;
}

var msgid = arg;
if( arg.constructor === Array )
msgid = arg[0];
if (arg.constructor === Array) {
msgid = arg[0];
}
var str = extractStr(msgid);
var line = node.loc.start.line;
var comments = findComments(astComments, line);
Expand All @@ -185,20 +237,20 @@ function parse(sources, options) {
reference: ref
}
};
if( arg.constructor === Array ) {
translations[str].msgid_plural = extractStr(arg[1]);
translations[str].msgstr = ['', ''];
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 (translations[str].comments) {
translations[str].comments.reference += '\n' + ref;
}
if (comments)
if (comments) {
translations[str].comments.extracted += '\n' + comments;
}
}
}
});

}, objectAssign({}, walk.base, jsxBase));

function dedupeNCoalesce(item, i, arr) {
return item && arr.indexOf(item) === i;
Expand All @@ -207,13 +259,16 @@ function parse(sources, options) {
Object.keys(translations).forEach(function (msgid) {
var comments = translations[msgid].comments;

if (!comments)
if (!comments) {
return;
}

if (comments.reference)
if (comments.reference) {
comments.reference = comments.reference.split('\n').filter(dedupeNCoalesce).join('\n');
if (comments.extracted)
}
if (comments.extracted) {
comments.extracted = comments.extracted.split('\n').filter(dedupeNCoalesce).join('\n');
}
});
});

Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@
"bin": "lib/cli.js",
"preferGlobal": true,
"dependencies": {
"acorn": "^2.7.0",
"acorn": "^3.0.0",
"acorn-jsx": "^3.0.0",
"commander": "^2.9.0",
"escape-string-regexp": "^1.0.4",
"gettext-parser": "^1.1.2",
"jade": "^1.11.0"
"jade": "^1.11.0",
"object-assign": "^4.0.1"
},
"devDependencies": {
"jshint": "2.5.5",
Expand Down

0 comments on commit 1b0d79a

Please sign in to comment.