@lingui/babel-plugin-transform-react
Version:
Transform React components to ICU message format
523 lines (428 loc) • 18.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _values = require("babel-runtime/core-js/object/values");
var _values2 = _interopRequireDefault(_values);
var _getIterator2 = require("babel-runtime/core-js/get-iterator");
var _getIterator3 = _interopRequireDefault(_getIterator2);
var _keys = require("babel-runtime/core-js/object/keys");
var _keys2 = _interopRequireDefault(_keys);
var _classCallCheck2 = require("babel-runtime/helpers/classCallCheck");
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
var _createClass2 = require("babel-runtime/helpers/createClass");
var _createClass3 = _interopRequireDefault(_createClass2);
var _assign = require("babel-runtime/core-js/object/assign");
var _assign2 = _interopRequireDefault(_assign);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var pluralRules = ["zero", "one", "two", "few", "many", "other"];
var commonProps = ["id", "className", "render"];
// replace whitespace before/after newline with single space
var nlRe = /\s*(?:\r\n|\r|\n)+\s*/g;
// remove whitespace before/after tag
var nlTagRe = /(?:(>)(?:\r\n|\r|\n)+\s+|(?:\r\n|\r|\n)+\s+(?=<))/g;
function cleanChildren(node) {
node.children = [];
node.openingElement.selfClosing = true;
}
var mergeProps = function mergeProps(props, nextProps) {
return {
text: props.text + nextProps.text,
values: (0, _assign2.default)({}, props.values, nextProps.values),
components: props.components.concat(nextProps.components),
formats: props.formats,
elementIndex: nextProps.elementIndex
};
};
var initialProps = function initialProps() {
var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
formats = _ref.formats;
return {
text: "",
values: {},
components: [],
formats: formats || {}
};
};
var generatorFactory = function generatorFactory() {
var index = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
return function () {
return index++;
};
};
var Transformer = function () {
function Transformer(_ref2) {
var t = _ref2.types;
(0, _classCallCheck3.default)(this, Transformer);
_initialiseProps.call(this);
this.t = t;
this.isTransElement = this.elementName("Trans");
}
(0, _createClass3.default)(Transformer, [{
key: "getOriginalImportName",
value: function getOriginalImportName(local) {
var _this = this;
// Either find original import name or use local one
var original = (0, _keys2.default)(this.importDeclarations).filter(function (name) {
return _this.importDeclarations[name] === local;
})[0];
return original || local;
}
}, {
key: "getLocalImportName",
value: function getLocalImportName(name) {
var strict = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
return this.importDeclarations[name] || !strict && name;
}
}, {
key: "isIdAttribute",
value: function isIdAttribute(node) {
return this.t.isJSXAttribute(node) && this.t.isJSXIdentifier(node.name, { name: "id" });
}
}, {
key: "isDefaultsAttribute",
value: function isDefaultsAttribute(node) {
return this.t.isJSXAttribute(node) && this.t.isJSXIdentifier(node.name, { name: "defaults" });
}
}, {
key: "isDescriptionAttribute",
value: function isDescriptionAttribute(node) {
return this.t.isJSXAttribute(node) && this.t.isJSXIdentifier(node.name, { name: "description" });
}
}, {
key: "processElement",
value: function processElement(node, file, props) {
var root = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
var t = this.t;
var element = node.openingElement;
// Trans
if (this.isTransElement(node)) {
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = (0, _getIterator3.default)(node.children), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var child = _step.value;
props = this.processChildren(child, file, props);
}
// Plural, Select, SelectOrdinal
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
} else if (this.isChooseElement(node)) {
var componentName = this.getOriginalImportName(element.name.name);
if (node.children.length) {
throw file.buildCodeFrameError(element, "Children of " + componentName + " aren't allowed.");
}
var choicesType = componentName.toLowerCase();
var choices = {};
var variable = void 0;
var offset = "";
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = (0, _getIterator3.default)(element.attributes), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var attr = _step2.value;
var name = attr.name.name;
if (name === "value") {
var exp = t.isLiteral(attr.value) ? attr.value : attr.value.expression;
variable = t.isIdentifier(exp) ? exp.name : this.argumentGenerator();
var key = t.isIdentifier(exp) ? exp : t.numericLiteral(variable);
props.values[variable] = t.objectProperty(key, exp);
} else if (commonProps.includes(name)) {
// just do nothing
} else if (choicesType !== "select" && name === "offset") {
// offset is static parameter, so it must be either string or number
var offsetExp = t.isStringLiteral(attr.value) ? attr.value : attr.value.expression;
if (offsetExp.value === undefined) {
throw file.buildCodeFrameError(element, "Offset argument cannot be a variable.");
}
offset = " offset:" + offsetExp.value;
} else {
props = this.processChildren(attr.value, file, (0, _assign2.default)({}, props, { text: "" }));
choices[name.replace("_", "=")] = props.text;
}
}
// missing value
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
if (variable === undefined) {
throw file.buildCodeFrameError(element, "Value argument is missing.");
}
var choicesKeys = (0, _keys2.default)(choices);
// 'other' choice is required
if (!choicesKeys.length) {
throw file.buildCodeFrameError(element, "Missing " + choicesType + " choices. At least fallback argument 'other' is required.");
} else if (!choicesKeys.includes("other")) {
throw file.buildCodeFrameError(element, "Missing fallback argument 'other'.");
}
// validate plural rules
if (choicesType === "plural" || choicesType === "selectordinal") {
choicesKeys.forEach(function (rule) {
if (!pluralRules.includes(rule) && !/=\d+/.test(rule)) {
throw file.buildCodeFrameError(element, "Invalid plural rule '" + rule + "'. Must be " + pluralRules.join(", ") + " or exact number depending on your source language ('one' and 'other' for English).");
}
});
}
var argument = choicesKeys.map(function (form) {
return form + " {" + choices[form] + "}";
}).join(" ");
props.text = "{" + variable + ", " + choicesType + "," + offset + " " + argument + "}";
element.attributes = element.attributes.filter(function (attr) {
return commonProps.includes(attr.name.name);
});
element.name = t.JSXIdentifier(this.getLocalImportName("Trans"));
} else if (this.isFormatElement(node)) {
if (root) {
// Don't convert standalone Format elements to ICU MessageFormat.
// It doesn't make sense to have `{name, number}` message, because we
// can call number() formatter directly in component.
return;
}
var type = this.getOriginalImportName(element.name.name).toLowerCase().replace("format", "");
var _variable = void 0,
format = void 0;
var _iteratorNormalCompletion3 = true;
var _didIteratorError3 = false;
var _iteratorError3 = undefined;
try {
for (var _iterator3 = (0, _getIterator3.default)(element.attributes), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
var _attr = _step3.value;
var _name = _attr.name.name;
if (_name === "value") {
var _exp = t.isLiteral(_attr.value) ? _attr.value : _attr.value.expression;
_variable = t.isIdentifier(_exp) ? _exp.name : this.argumentGenerator();
var _key = t.isIdentifier(_exp) ? _exp : t.numericLiteral(_variable);
props.values[_variable] = t.objectProperty(_key, _exp);
} else if (_name === "format") {
if (t.isStringLiteral(_attr.value)) {
format = _attr.value.value;
} else if (t.isJSXExpressionContainer(_attr.value)) {
var _exp2 = _attr.value.expression;
if (t.isStringLiteral(_exp2)) {
format = _exp2.value;
} else if (t.isObjectExpression(_exp2) || t.isIdentifier(_exp2)) {
if (t.isIdentifier(_exp2)) {
format = _exp2.name;
} else {
(function () {
var formatName = new RegExp("^" + type + "\\d+$");
var existing = (0, _keys2.default)(props.formats).filter(function (name) {
return formatName.test(name);
});
format = "" + type + (existing.length || 0);
})();
}
props.formats[format] = t.objectProperty(t.identifier(format), _exp2);
}
}
if (!format) {
throw file.buildCodeFrameError(element, "Format can be either string for buil-in formats, variable or object for custom defined formats.");
}
}
}
// missing value
} catch (err) {
_didIteratorError3 = true;
_iteratorError3 = err;
} finally {
try {
if (!_iteratorNormalCompletion3 && _iterator3.return) {
_iterator3.return();
}
} finally {
if (_didIteratorError3) {
throw _iteratorError3;
}
}
}
if (_variable === undefined) {
throw file.buildCodeFrameError(element, "Value argument is missing.");
}
var parts = [_variable, type];
if (format) parts.push(format);
props.text = "{" + parts.join(",") + "}";
element.attributes = element.attributes.filter(function (attr) {
return commonProps.includes(attr.name.name);
});
element.name = t.JSXIdentifier(this.getLocalImportName("Trans"));
// Other elements
} else {
if (root) return;
var index = this.elementGenerator();
var selfClosing = node.openingElement.selfClosing;
props.text += !selfClosing ? "<" + index + ">" : "<" + index + "/>";
var _iteratorNormalCompletion4 = true;
var _didIteratorError4 = false;
var _iteratorError4 = undefined;
try {
for (var _iterator4 = (0, _getIterator3.default)(node.children), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
var _child = _step4.value;
props = this.processChildren(_child, file, props);
}
} catch (err) {
_didIteratorError4 = true;
_iteratorError4 = err;
} finally {
try {
if (!_iteratorNormalCompletion4 && _iterator4.return) {
_iterator4.return();
}
} finally {
if (_didIteratorError4) {
throw _iteratorError4;
}
}
}
if (!selfClosing) props.text += "</" + index + ">";
cleanChildren(node);
props.components.unshift(node);
}
return props;
}
}, {
key: "processChildren",
value: function processChildren(node, file, props) {
var _this2 = this;
var t = this.t;
var nextProps = initialProps({ formats: props.formats });
if (t.isJSXExpressionContainer(node)) {
var exp = node.expression;
if (t.isStringLiteral(exp)) {
nextProps.text += exp.value;
} else if (t.isTemplateLiteral(exp)) {
var parts = [];
exp.quasis.forEach(function (item, index) {
parts.push(item);
if (!item.tail) parts.push(exp.expressions[index]);
});
parts.forEach(function (item) {
if (t.isTemplateElement(item)) {
nextProps.text += item.value.raw;
} else {
var name = t.isIdentifier(item) ? item.name : _this2.argumentGenerator();
var key = t.isIdentifier(item) ? item : t.numericLiteral(name);
nextProps.text += "{" + name + "}";
nextProps.values[name] = t.objectProperty(key, item);
}
});
} else if (t.isJSXElement(exp)) {
nextProps = this.processElement(exp, file, nextProps);
} else {
var name = t.isIdentifier(exp) ? exp.name : this.argumentGenerator();
var key = t.isIdentifier(exp) ? exp : t.numericLiteral(name);
nextProps.text += "{" + name + "}";
nextProps.values[name] = t.objectProperty(key, exp);
}
} else if (t.isJSXElement(node)) {
nextProps = this.processElement(node, file, nextProps);
} else if (t.isJSXSpreadChild(node)) {
// TODO: I don't have a clue what's the usecase
} else {
nextProps.text += node.value;
}
return mergeProps(props, nextProps);
}
/**
* Used for macro
* @param imports
*/
}, {
key: "setImportDeclarations",
value: function setImportDeclarations(imports) {
// Used for the macro to override the imports
this.importDeclarations = imports;
}
}, {
key: "getImportDeclarations",
value: function getImportDeclarations() {
return this.importDeclarations;
}
}]);
return Transformer;
}();
var _initialiseProps = function _initialiseProps() {
var _this3 = this;
this.elementName = function (name) {
return function (node) {
return _this3.t.isJSXElement(node) && _this3.t.isJSXIdentifier(node.openingElement.name, {
name: _this3.getLocalImportName(name, true)
});
};
};
this.isChooseElement = function (node) {
return _this3.elementName("Plural")(node) || _this3.elementName("Select")(node) || _this3.elementName("SelectOrdinal")(node);
};
this.isFormatElement = function (node) {
return _this3.elementName("DateFormat")(node) || _this3.elementName("NumberFormat")(node);
};
this.transform = function (path, file) {
if (!_this3.importDeclarations || !(0, _keys2.default)(_this3.importDeclarations).length) {
return;
}
var node = path.node;
var t = _this3.t;
_this3.elementGenerator = generatorFactory();
_this3.argumentGenerator = generatorFactory();
// 1. Collect all parameters and inline elements and generate message ID
var props = _this3.processElement(node, file, initialProps(),
/* root= */true);
if (!props) return;
// 2. Replace children and add collected data
cleanChildren(node);
var text = props.text.replace(nlTagRe, "$1").replace(nlRe, " ").trim();
var attrs = node.openingElement.attributes;
// If `id` prop already exists and generated ID is different,
// add it as a `default` prop
var idAttr = attrs.filter(_this3.isIdAttribute.bind(_this3))[0];
if (idAttr && text && idAttr.value.value !== text) {
attrs.push(t.JSXAttribute(t.JSXIdentifier("defaults"), t.StringLiteral(text)));
} else if (!idAttr) {
attrs.push(t.JSXAttribute(t.JSXIdentifier("id"), t.StringLiteral(text)));
}
// Parameters for variable substitution
var valuesList = (0, _values2.default)(props.values);
if (valuesList.length) {
attrs.push(t.JSXAttribute(t.JSXIdentifier("values"), t.JSXExpressionContainer(t.objectExpression(valuesList))));
}
// Inline elements
if (props.components.length) {
attrs.push(t.JSXAttribute(t.JSXIdentifier("components"), t.JSXExpressionContainer(t.arrayExpression(props.components))));
}
// Custom formats
var formatsList = (0, _values2.default)(props.formats);
if (formatsList.length) {
attrs.push(t.JSXAttribute(t.JSXIdentifier("formats"), t.JSXExpressionContainer(t.objectExpression(formatsList))));
}
if (process.env.NODE_ENV === "production") {
node.openingElement.attributes = attrs.filter(function (node) {
return !_this3.isDefaultsAttribute(node) && !_this3.isDescriptionAttribute(node);
});
}
};
};
exports.default = Transformer;