babel-plugin-react-intl
Version:
Extracts string messages for translation from modules that use React Intl.
416 lines (415 loc) • 19.7 kB
JavaScript
;
/*
* Copyright 2015, Yahoo Inc.
* Copyrights licensed under the New BSD License.
* See the accompanying LICENSE file for terms.
*/
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var intl_messageformat_parser_1 = require("intl-messageformat-parser");
var helper_plugin_utils_1 = require("@babel/helper-plugin-utils");
var core_1 = require("@babel/core");
var types_1 = require("@babel/types");
var schema_utils_1 = require("schema-utils");
var OPTIONS_SCHEMA = tslib_1.__importStar(require("./options.schema.json"));
var ts_transformer_1 = require("@formatjs/ts-transformer");
var DEFAULT_COMPONENT_NAMES = ['FormattedMessage'];
var EXTRACTED = Symbol('ReactIntlExtracted');
var DESCRIPTOR_PROPS = new Set(['id', 'description', 'defaultMessage']);
function getICUMessageValue(messagePath, _a) {
var _b = _a === void 0 ? {} : _a, _c = _b.isJSXSource, isJSXSource = _c === void 0 ? false : _c;
if (!messagePath) {
return '';
}
var message = getMessageDescriptorValue(messagePath)
.trim()
.replace(/\s+/gm, ' ');
try {
intl_messageformat_parser_1.parse(message);
}
catch (parseError) {
if (isJSXSource &&
messagePath.isLiteral() &&
message.indexOf('\\\\') >= 0) {
throw messagePath.buildCodeFrameError('[React Intl] Message failed to parse. ' +
'It looks like `\\`s were used for escaping, ' +
"this won't work with JSX string literals. " +
'Wrap with `{}`. ' +
'See: http://facebook.github.io/react/docs/jsx-gotchas.html');
}
throw messagePath.buildCodeFrameError('[React Intl] Message failed to parse. ' +
'See: https://formatjs.io/docs/core-concepts/icu-syntax' +
("\n" + parseError));
}
return message;
}
function evaluatePath(path) {
var evaluated = path.evaluate();
if (evaluated.confident) {
return evaluated.value;
}
throw path.buildCodeFrameError('[React Intl] Messages must be statically evaluate-able for extraction.');
}
function getMessageDescriptorKey(path) {
if (path.isIdentifier() || path.isJSXIdentifier()) {
return path.node.name;
}
return evaluatePath(path);
}
function getMessageDescriptorValue(path) {
if (!path) {
return '';
}
if (path.isJSXExpressionContainer()) {
path = path.get('expression');
}
// Always trim the Message Descriptor values.
var descriptorValue = evaluatePath(path);
return descriptorValue;
}
function createMessageDescriptor(propPaths) {
return propPaths.reduce(function (hash, _a) {
var keyPath = _a[0], valuePath = _a[1];
var key = getMessageDescriptorKey(keyPath);
if (DESCRIPTOR_PROPS.has(key)) {
hash[key] = valuePath;
}
return hash;
}, {
id: undefined,
defaultMessage: undefined,
description: undefined,
});
}
function evaluateMessageDescriptor(descriptorPath, isJSXSource, filename, idInterpolationPattern, overrideIdFn) {
if (isJSXSource === void 0) { isJSXSource = false; }
if (idInterpolationPattern === void 0) { idInterpolationPattern = '[contenthash:5]'; }
var id = getMessageDescriptorValue(descriptorPath.id);
var defaultMessage = getICUMessageValue(descriptorPath.defaultMessage, {
isJSXSource: isJSXSource,
});
var description = getMessageDescriptorValue(descriptorPath.description);
if (overrideIdFn) {
id = overrideIdFn(id, defaultMessage, description, filename);
}
else if (!id && idInterpolationPattern && defaultMessage) {
id = ts_transformer_1.interpolateName({ resourcePath: filename }, idInterpolationPattern, {
content: description
? defaultMessage + "#" + description
: defaultMessage,
});
}
var descriptor = {
id: id,
};
if (description) {
descriptor.description = description;
}
if (defaultMessage) {
descriptor.defaultMessage = defaultMessage;
}
return descriptor;
}
function storeMessage(_a, path, _b, filename, messages) {
var id = _a.id, description = _a.description, defaultMessage = _a.defaultMessage;
var extractSourceLocation = _b.extractSourceLocation;
if (!id && !defaultMessage) {
throw path.buildCodeFrameError('[React Intl] Message Descriptors require an `id` or `defaultMessage`.');
}
if (messages.has(id)) {
var existing = messages.get(id);
if (description !== existing.description ||
defaultMessage !== existing.defaultMessage) {
throw path.buildCodeFrameError("[React Intl] Duplicate message id: \"" + id + "\", " +
'but the `description` and/or `defaultMessage` are different.');
}
}
var loc = {};
if (extractSourceLocation) {
loc = tslib_1.__assign({ file: filename }, path.node.loc);
}
messages.set(id, tslib_1.__assign({ id: id, description: description, defaultMessage: defaultMessage }, loc));
}
function referencesImport(path, mod, importedNames) {
if (!(path.isIdentifier() || path.isJSXIdentifier())) {
return false;
}
return importedNames.some(function (name) { return path.referencesImport(mod, name); });
}
function isFormatMessageDestructuring(scope) {
var binding = scope.getBinding('formatMessage');
var block = scope.block;
var declNode = binding === null || binding === void 0 ? void 0 : binding.path.node;
// things like `const {formatMessage} = intl; formatMessage(...)`
if (core_1.types.isVariableDeclarator(declNode)) {
// things like `const {formatMessage} = useIntl(); formatMessage(...)`
if (core_1.types.isCallExpression(declNode.init)) {
if (core_1.types.isIdentifier(declNode.init.callee)) {
return declNode.init.callee.name === 'useIntl';
}
}
return (core_1.types.isObjectPattern(declNode.id) &&
declNode.id.properties.find(function (value) { return value.key.name === 'intl'; }));
}
// things like const fn = ({ intl: { formatMessage }}) => { formatMessage(...) }
if (core_1.types.isFunctionDeclaration(block) &&
block.params.length &&
core_1.types.isObjectPattern(block.params[0])) {
return block.params[0].properties.find(function (value) { return value.key.name === 'intl'; });
}
return false;
}
function isFormatMessageCall(callee, path) {
if (callee.isIdentifier() &&
callee.node.name === 'formatMessage' &&
isFormatMessageDestructuring(path.scope)) {
return true;
}
if (!callee.isMemberExpression()) {
return false;
}
var object = callee.get('object');
var property = callee.get('property');
return (property.isIdentifier() &&
property.node.name === 'formatMessage' &&
!Array.isArray(object) &&
// things like `intl.formatMessage`
((object.isIdentifier() && object.node.name === 'intl') ||
// things like `this.props.intl.formatMessage`
(object.isMemberExpression() &&
object.get('property').node.name === 'intl')));
}
function assertObjectExpression(path, callee) {
if (!path || !path.isObjectExpression()) {
throw path.buildCodeFrameError("[React Intl] `" + callee.get('property').node.name + "()` must be " +
'called with an object expression with values ' +
'that are React Intl Message Descriptors, also ' +
'defined as object expressions.');
}
return true;
}
exports.default = helper_plugin_utils_1.declare(function (api, options) {
api.assertVersion(7);
schema_utils_1.validate(OPTIONS_SCHEMA, options, {
name: 'babel-plugin-react-intl',
baseDataPath: 'options',
});
var pragma = options.pragma;
/**
* Store this in the node itself so that multiple passes work. Specifically
* if we remove `description` in the 1st pass, 2nd pass will fail since
* it expect `description` to be there.
* HACK: We store this in the node instance since this persists across
* multiple plugin runs
*/
function tagAsExtracted(path) {
path.node[EXTRACTED] = true;
}
function wasExtracted(path) {
return !!path.node[EXTRACTED];
}
return {
pre: function () {
if (!this.ReactIntlMessages) {
this.ReactIntlMessages = new Map();
this.ReactIntlMeta = {};
}
},
post: function (state) {
var _a = this, messages = _a.ReactIntlMessages, ReactIntlMeta = _a.ReactIntlMeta;
var descriptors = Array.from(messages.values());
state.metadata['react-intl'] = {
messages: descriptors,
meta: ReactIntlMeta,
};
},
visitor: {
Program: function (path) {
var body = path.node.body;
var ReactIntlMeta = this.ReactIntlMeta;
if (!pragma) {
return;
}
for (var _i = 0, body_1 = body; _i < body_1.length; _i++) {
var leadingComments = body_1[_i].leadingComments;
if (!leadingComments) {
continue;
}
var pragmaLineNode = leadingComments.find(function (c) {
return c.value.includes(pragma);
});
if (!pragmaLineNode) {
continue;
}
pragmaLineNode.value
.split(pragma)[1]
.trim()
.split(/\s+/g)
.forEach(function (kv) {
var _a = kv.split(':'), k = _a[0], v = _a[1];
ReactIntlMeta[k] = v;
});
}
},
JSXOpeningElement: function (path, _a) {
var opts = _a.opts, filename = _a.file.opts.filename;
var _b = opts.moduleSourceName, moduleSourceName = _b === void 0 ? 'react-intl' : _b, _c = opts.additionalComponentNames, additionalComponentNames = _c === void 0 ? [] : _c, removeDefaultMessage = opts.removeDefaultMessage, idInterpolationPattern = opts.idInterpolationPattern, overrideIdFn = opts.overrideIdFn, ast = opts.ast;
if (wasExtracted(path)) {
return;
}
var name = path.get('name');
if (name.referencesImport(moduleSourceName, 'FormattedPlural')) {
if (path.node && path.node.loc)
console.warn("[React Intl] Line " + path.node.loc.start.line + ": " +
'Default messages are not extracted from ' +
'<FormattedPlural>, use <FormattedMessage> instead.');
return;
}
if (name.isJSXIdentifier() &&
(referencesImport(name, moduleSourceName, DEFAULT_COMPONENT_NAMES) ||
additionalComponentNames.includes(name.node.name))) {
var attributes = path
.get('attributes')
.filter(function (attr) { return attr.isJSXAttribute(); });
var descriptorPath = createMessageDescriptor(attributes.map(function (attr) { return [
attr.get('name'),
attr.get('value'),
]; }));
// In order for a default message to be extracted when
// declaring a JSX element, it must be done with standard
// `key=value` attributes. But it's completely valid to
// write `<FormattedMessage {...descriptor} />`, because it will be
// skipped here and extracted elsewhere. The descriptor will
// be extracted only (storeMessage) if a `defaultMessage` prop.
if (descriptorPath.id || descriptorPath.defaultMessage) {
// Evaluate the Message Descriptor values in a JSX
// context, then store it.
var descriptor = evaluateMessageDescriptor(descriptorPath, true, filename, idInterpolationPattern, overrideIdFn);
storeMessage(descriptor, path, opts, filename, this.ReactIntlMessages);
var idAttr = void 0;
var descriptionAttr = void 0;
var defaultMessageAttr = void 0;
for (var _i = 0, attributes_1 = attributes; _i < attributes_1.length; _i++) {
var attr = attributes_1[_i];
if (!attr.isJSXAttribute()) {
continue;
}
switch (getMessageDescriptorKey(attr.get('name'))) {
case 'description':
descriptionAttr = attr;
break;
case 'defaultMessage':
defaultMessageAttr = attr;
break;
case 'id':
idAttr = attr;
break;
}
}
if (descriptionAttr) {
descriptionAttr.remove();
}
if (!removeDefaultMessage &&
ast &&
descriptor.defaultMessage &&
defaultMessageAttr) {
defaultMessageAttr
.get('value')
.replaceWith(core_1.types.jsxExpressionContainer(core_1.types.stringLiteral('foo')));
defaultMessageAttr.get('value')
.get('expression')
.replaceWithSourceString(JSON.stringify(intl_messageformat_parser_1.parse(descriptor.defaultMessage)));
}
if (overrideIdFn || (descriptor.id && idInterpolationPattern)) {
if (idAttr) {
idAttr.get('value').replaceWith(core_1.types.stringLiteral(descriptor.id));
}
else if (defaultMessageAttr) {
defaultMessageAttr.insertBefore(core_1.types.jsxAttribute(core_1.types.jsxIdentifier('id'), core_1.types.stringLiteral(descriptor.id)));
}
}
if (removeDefaultMessage && defaultMessageAttr) {
defaultMessageAttr.remove();
}
// Tag the AST node so we don't try to extract it twice.
tagAsExtracted(path);
}
}
},
CallExpression: function (path, _a) {
var opts = _a.opts, filename = _a.file.opts.filename;
var messages = this.ReactIntlMessages;
var _b = opts.moduleSourceName, moduleSourceName = _b === void 0 ? 'react-intl' : _b, overrideIdFn = opts.overrideIdFn, idInterpolationPattern = opts.idInterpolationPattern, removeDefaultMessage = opts.removeDefaultMessage, extractFromFormatMessageCall = opts.extractFromFormatMessageCall, ast = opts.ast;
var callee = path.get('callee');
/**
* Process MessageDescriptor
* @param messageDescriptor Message Descriptor
*/
function processMessageObject(messageDescriptor) {
assertObjectExpression(messageDescriptor, callee);
if (wasExtracted(messageDescriptor)) {
return;
}
var properties = messageDescriptor.get('properties');
var descriptorPath = createMessageDescriptor(properties.map(function (prop) {
return [prop.get('key'), prop.get('value')];
}));
// Evaluate the Message Descriptor values, then store it.
var descriptor = evaluateMessageDescriptor(descriptorPath, false, filename, idInterpolationPattern, overrideIdFn);
storeMessage(descriptor, messageDescriptor, opts, filename, messages);
// Remove description since it's not used at runtime.
messageDescriptor.replaceWithSourceString(JSON.stringify(tslib_1.__assign({ id: descriptor.id }, (!removeDefaultMessage && descriptor.defaultMessage
? {
defaultMessage: ast
? intl_messageformat_parser_1.parse(descriptor.defaultMessage)
: descriptor.defaultMessage,
}
: {}))));
// Tag the AST node so we don't try to extract it twice.
tagAsExtracted(messageDescriptor);
}
// Check that this is `defineMessages` call
if (isMultipleMessagesDeclMacro(callee, moduleSourceName) ||
isSingularMessagesDeclMacro(callee, moduleSourceName)) {
var firstArgument = path.get('arguments')[0];
var messagesObj = getMessagesObjectFromExpression(firstArgument);
if (assertObjectExpression(messagesObj, callee)) {
if (isSingularMessagesDeclMacro(callee, moduleSourceName)) {
processMessageObject(messagesObj);
}
else {
var properties = messagesObj.get('properties');
if (Array.isArray(properties)) {
properties
.map(function (prop) { return prop.get('value'); })
.forEach(processMessageObject);
}
}
}
}
// Check that this is `intl.formatMessage` call
if (extractFromFormatMessageCall && isFormatMessageCall(callee, path)) {
var messageDescriptor = path.get('arguments')[0];
if (messageDescriptor.isObjectExpression()) {
processMessageObject(messageDescriptor);
}
}
},
},
};
});
function isMultipleMessagesDeclMacro(callee, moduleSourceName) {
return referencesImport(callee, moduleSourceName, ['defineMessages']);
}
function isSingularMessagesDeclMacro(callee, moduleSourceName) {
return referencesImport(callee, moduleSourceName, ['defineMessage']);
}
function getMessagesObjectFromExpression(nodePath) {
var currentPath = nodePath;
while (types_1.isTSAsExpression(currentPath.node) ||
types_1.isTSTypeAssertion(currentPath.node) ||
types_1.isTypeCastExpression(currentPath.node)) {
currentPath = currentPath.get('expression');
}
return currentPath;
}