@lingui/babel-plugin-lingui-macro
Version:
Babel plugin for transforming Lingui Macros
743 lines (734 loc) • 24.7 kB
JavaScript
import { c as createMacroJsContext, m as makeCounter, b as createMessageDescriptorFromTokens, M as MsgDescriptorPropKey, t as tokenizeExpression, a as JsxMacroName, d as tokenizeTemplateLiteral, p as processDescriptor, i as isDefineMessage, e as isLinguiIdentifier, J as JsMacroName, f as tokenizeNode } from './shared/babel-plugin-lingui-macro.048e39ee.mjs';
import * as t from '@babel/types';
import { getConfig as getConfig$1 } from '@lingui/conf';
import '@lingui/message-utils/generateMessageId';
function cleanJSXElementLiteralChild(value) {
const lines = value.split(/\r\n|\n|\r/);
let lastNonEmptyLine = 0;
for (let i = 0; i < lines.length; i++) {
if (lines[i].match(/[^ \t]/)) {
lastNonEmptyLine = i;
}
}
let str = "";
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const isFirstLine = i === 0;
const isLastLine = i === lines.length - 1;
const isLastNonEmptyLine = i === lastNonEmptyLine;
let trimmedLine = line.replace(/\t/g, " ");
if (!isFirstLine) {
trimmedLine = trimmedLine.replace(/^[ ]+/, "");
}
if (!isLastLine) {
trimmedLine = trimmedLine.replace(/[ ]+$/, "");
}
if (trimmedLine) {
if (!isLastNonEmptyLine) {
trimmedLine += " ";
}
str += trimmedLine;
}
}
return str;
}
var __defProp$1 = Object.defineProperty;
var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField$1 = (obj, key, value) => {
__defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
const pluralRuleRe = /(_[\d\w]+|zero|one|two|few|many|other)/;
const jsx2icuExactChoice = (value) => value.replace(/_(\d+)/, "=$1").replace(/_(\w+)/, "$1");
function maybeNodeValue(node) {
if (!node)
return null;
if (node.type === "StringLiteral")
return { text: node.value, loc: node.loc };
if (node.type === "JSXAttribute")
return maybeNodeValue(node.value);
if (node.type === "JSXExpressionContainer")
return maybeNodeValue(node.expression);
if (node.type === "TemplateLiteral" && node.expressions.length === 0)
return { text: node.quasis[0].value.raw, loc: node.loc };
return null;
}
class MacroJSX {
constructor({ types }, opts) {
__publicField$1(this, "types");
__publicField$1(this, "ctx");
__publicField$1(this, "replacePath", (path) => {
if (!path.isJSXElement()) {
return false;
}
const tokens = this.tokenizeNode(path, true, true);
if (!tokens) {
return false;
}
const { attributes, id, comment, context } = this.stripMacroAttributes(
path
);
if (!tokens.length) {
throw new Error("Incorrect usage of Trans");
}
const messageDescriptor = createMessageDescriptorFromTokens(
tokens,
path.node.loc,
this.ctx.stripNonEssentialProps,
this.ctx.stripMessageProp,
{
id,
context,
comment
}
);
attributes.push(this.types.jsxSpreadAttribute(messageDescriptor));
const newNode = this.types.jsxElement(
this.types.jsxOpeningElement(
this.types.jsxIdentifier(this.ctx.transImportName),
attributes,
true
),
null,
[],
true
);
newNode.loc = path.node.loc;
return newNode;
});
__publicField$1(this, "attrName", (names, exclude = false) => {
const namesRe = new RegExp("^(" + names.join("|") + ")$");
return (attr) => {
const name = attr.name.name;
return exclude ? !namesRe.test(name) : namesRe.test(name);
};
});
__publicField$1(this, "stripMacroAttributes", (path) => {
const { attributes } = path.node.openingElement;
const id = attributes.find(this.attrName([MsgDescriptorPropKey.id]));
const message = attributes.find(
this.attrName([MsgDescriptorPropKey.message])
);
const comment = attributes.find(
this.attrName([MsgDescriptorPropKey.comment])
);
const context = attributes.find(
this.attrName([MsgDescriptorPropKey.context])
);
let reserved = [
MsgDescriptorPropKey.id,
MsgDescriptorPropKey.message,
MsgDescriptorPropKey.comment,
MsgDescriptorPropKey.context
];
if (this.isChoiceComponent(path)) {
reserved = [
...reserved,
"_\\w+",
"_\\d+",
"zero",
"one",
"two",
"few",
"many",
"other",
"value",
"offset"
];
}
return {
id: maybeNodeValue(id),
message: maybeNodeValue(message),
comment: maybeNodeValue(comment),
context: maybeNodeValue(context),
attributes: attributes.filter(this.attrName(reserved, true))
};
});
__publicField$1(this, "tokenizeNode", (path, ignoreExpression = false, ignoreElement = false) => {
if (this.isTransComponent(path)) {
return this.tokenizeTrans(path);
}
const componentName = this.isChoiceComponent(path);
if (componentName) {
return [
this.tokenizeChoiceComponent(
path,
componentName
)
];
}
if (path.isJSXElement() && !ignoreElement) {
return [this.tokenizeElement(path)];
}
if (!ignoreExpression) {
return [this.tokenizeExpression(path)];
}
});
__publicField$1(this, "tokenizeTrans", (path) => {
return path.get("children").flatMap((child) => this.tokenizeChildren(child)).filter(Boolean);
});
__publicField$1(this, "tokenizeChildren", (path) => {
if (path.isJSXExpressionContainer()) {
const exp = path.get("expression");
if (exp.isStringLiteral()) {
return [this.tokenizeText(exp.node.value)];
}
if (exp.isTemplateLiteral()) {
return this.tokenizeTemplateLiteral(exp);
}
if (exp.isConditionalExpression()) {
return [this.tokenizeConditionalExpression(exp)];
}
if (exp.isJSXElement()) {
return this.tokenizeNode(exp);
}
return [this.tokenizeExpression(exp)];
} else if (path.isJSXElement()) {
return this.tokenizeNode(path);
} else if (path.isJSXSpreadChild()) {
throw new Error(
"Incorrect usage of Trans: Spread could not be used as Trans children"
);
} else if (path.isJSXText()) {
return [this.tokenizeText(cleanJSXElementLiteralChild(path.node.value))];
} else ;
});
__publicField$1(this, "tokenizeChoiceComponent", (path, componentName) => {
const element = path.get("openingElement");
const format = componentName.toLowerCase();
const props = element.get("attributes").filter((attr) => {
return this.attrName(
[
MsgDescriptorPropKey.id,
MsgDescriptorPropKey.comment,
MsgDescriptorPropKey.message,
MsgDescriptorPropKey.context,
"key",
// we remove <Trans /> react props that are not useful for translation
"render",
"component",
"components"
],
true
)(attr.node);
});
let token = {
type: "arg",
format,
name: null,
value: void 0,
options: {
offset: void 0
}
};
for (const _attr of props) {
if (_attr.isJSXSpreadAttribute()) {
continue;
}
const attr = _attr;
if (this.types.isJSXNamespacedName(attr.node.name)) {
continue;
}
const name = attr.node.name.name;
const value = attr.get("value");
if (name === "value") {
token = {
...token,
...this.tokenizeExpression(
value.isLiteral() ? value : value.get("expression")
)
};
} else if (format !== "select" && name === "offset") {
token.options.offset = value.isStringLiteral() || value.isNumericLiteral() ? value.node.value : value.get(
"expression"
).node.value;
} else {
let option;
if (value.isStringLiteral()) {
option = value.node.extra.raw.replace(
/(["'])(.*)\1/,
"$2"
);
} else {
option = this.tokenizeChildren(value);
}
if (pluralRuleRe.test(name)) {
token.options[jsx2icuExactChoice(name)] = option;
} else {
token.options[name] = option;
}
}
}
return token;
});
__publicField$1(this, "tokenizeElement", (path) => {
const name = this.ctx.elementIndex();
return {
type: "element",
name,
value: {
...path.node,
children: [],
openingElement: {
...path.node.openingElement,
selfClosing: true
}
},
children: this.tokenizeTrans(path)
};
});
__publicField$1(this, "tokenizeExpression", (path) => {
return tokenizeExpression(path.node, this.ctx);
});
__publicField$1(this, "tokenizeConditionalExpression", (exp) => {
exp.traverse(
{
JSXElement: (el) => {
if (this.isTransComponent(el) || this.isChoiceComponent(el)) {
this.replacePath(el);
el.skip();
}
}
},
exp.state
);
return this.tokenizeExpression(exp);
});
__publicField$1(this, "tokenizeText", (value) => {
return {
type: "text",
value
};
});
__publicField$1(this, "isLinguiComponent", (path, name) => {
if (!path.isJSXElement()) {
return false;
}
const config = path.context.state.get(
"linguiConfig"
);
const identifier = path.get("openingElement").get("name");
return config.macro.jsxPackage.some(
(moduleSource) => identifier.referencesImport(moduleSource, name)
);
});
__publicField$1(this, "isTransComponent", (path) => {
return this.isLinguiComponent(path, JsxMacroName.Trans);
});
__publicField$1(this, "isChoiceComponent", (path) => {
if (this.isLinguiComponent(path, JsxMacroName.Plural)) {
return JsxMacroName.Plural;
}
if (this.isLinguiComponent(path, JsxMacroName.Select)) {
return JsxMacroName.Select;
}
if (this.isLinguiComponent(path, JsxMacroName.SelectOrdinal)) {
return JsxMacroName.SelectOrdinal;
}
});
this.types = types;
this.ctx = {
...createMacroJsContext(
opts.isLinguiIdentifier,
opts.stripNonEssentialProps,
opts.stripMessageProp
),
transImportName: opts.transImportName,
elementIndex: makeCounter()
};
}
tokenizeTemplateLiteral(exp) {
const expressions = exp.get("expressions");
return exp.get("quasis").flatMap(({ node: text }, i) => {
const value = text.value.cooked;
let argTokens = [];
const currExp = expressions[i];
if (currExp) {
argTokens = currExp.isCallExpression() ? this.tokenizeNode(currExp) : [this.tokenizeExpression(currExp)];
}
return [...value ? [this.tokenizeText(value)] : [], ...argTokens];
});
}
}
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => {
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
class MacroJs {
constructor(opts) {
// Identifier of i18n object
__publicField(this, "i18nImportName");
__publicField(this, "useLinguiImportName");
__publicField(this, "needsUseLinguiImport", false);
__publicField(this, "needsI18nImport", false);
__publicField(this, "_ctx");
__publicField(this, "replacePathWithMessage", (path, tokens, linguiInstance) => {
return this.createI18nCall(
createMessageDescriptorFromTokens(
tokens,
path.node.loc,
this._ctx.stripNonEssentialProps,
this._ctx.stripMessageProp
),
linguiInstance
);
});
__publicField(this, "replacePath", (path) => {
const ctx = this._ctx;
if (
//
path.isCallExpression() && isDefineMessage(path.get("callee").node, ctx)
) {
return processDescriptor(
path.get("arguments")[0].node,
ctx
);
}
if (path.isTaggedTemplateExpression() && isDefineMessage(path.get("tag").node, ctx)) {
const tokens2 = tokenizeTemplateLiteral(path.get("quasi").node, ctx);
return createMessageDescriptorFromTokens(
tokens2,
path.node.loc,
ctx.stripNonEssentialProps,
ctx.stripMessageProp
);
}
if (path.isTaggedTemplateExpression()) {
const tag = path.get("tag");
if (tag.isCallExpression() && tag.get("arguments")[0]?.isExpression() && isLinguiIdentifier(tag.get("callee").node, JsMacroName.t, ctx)) {
const i18nInstance = tag.get("arguments")[0].node;
const tokens2 = tokenizeNode(path.node, false, ctx);
return this.replacePathWithMessage(path, tokens2, i18nInstance);
}
}
if (path.isCallExpression()) {
const callee = path.get("callee");
if (callee.isCallExpression() && callee.get("arguments")[0]?.isExpression() && isLinguiIdentifier(callee.get("callee").node, JsMacroName.t, ctx)) {
const i18nInstance = callee.node.arguments[0];
return this.replaceTAsFunction(
path.node,
ctx,
i18nInstance
);
}
}
if (path.isCallExpression() && isLinguiIdentifier(path.get("callee").node, JsMacroName.t, ctx)) {
this.needsI18nImport = true;
return this.replaceTAsFunction(path.node, ctx);
}
if (path.isCallExpression() && isLinguiIdentifier(path.get("callee").node, JsMacroName.useLingui, ctx)) {
this.needsUseLinguiImport = true;
return this.processUseLingui(path, ctx);
}
const tokens = tokenizeNode(path.node, true, ctx);
if (tokens) {
this.needsI18nImport = true;
return this.replacePathWithMessage(path, tokens);
}
return false;
});
/**
* macro `t` is called with MessageDescriptor, after that
* we create a new node to append it to i18n._
*/
__publicField(this, "replaceTAsFunction", (node, ctx, linguiInstance) => {
let arg = node.arguments[0];
if (t.isObjectExpression(arg)) {
arg = processDescriptor(arg, ctx);
}
return this.createI18nCall(arg, linguiInstance);
});
this.i18nImportName = opts.i18nImportName;
this.useLinguiImportName = opts.useLinguiImportName;
this._ctx = createMacroJsContext(
opts.isLinguiIdentifier,
opts.stripNonEssentialProps,
opts.stripMessageProp
);
}
/**
* Receives reference to `useLingui()` call
*
* Finds every usage of { t } destructured from the call
* and process each reference as usual `t` macro.
*
* const { t } = useLingui()
* t`Message`
*
* ↓ ↓ ↓ ↓ ↓ ↓
*
* const { _: _t } = useLingui()
* _t({id: <hash>, message: "Message"})
*/
processUseLingui(path, ctx) {
if (!path.parentPath.isVariableDeclarator()) {
throw new Error(
`\`useLingui\` macro must be used in variable declaration.
Example:
const { t } = useLingui()
`
);
}
const varDec = path.parentPath.node;
if (!t.isObjectPattern(varDec.id)) {
throw new Error(
`You have to destructure \`t\` when using the \`useLingui\` macro, i.e:
const { t } = useLingui()
or
const { t: _ } = useLingui()
`
);
}
const _property = t.isObjectPattern(varDec.id) ? varDec.id.properties.find(
(property) => t.isObjectProperty(property) && t.isIdentifier(property.key) && t.isIdentifier(property.value) && property.key.name == "t"
) : null;
const newNode = t.callExpression(t.identifier(this.useLinguiImportName), []);
if (!_property) {
return newNode;
}
const uniqTIdentifier = path.scope.generateUidIdentifier("t");
path.scope.getBinding(_property.value.name)?.referencePaths.forEach((refPath) => {
const currentPath = refPath.parentPath;
const _ctx = createMacroJsContext(
ctx.isLinguiIdentifier,
ctx.stripNonEssentialProps,
ctx.stripMessageProp
);
if (currentPath.isTaggedTemplateExpression()) {
const tokens = tokenizeTemplateLiteral(currentPath.node, _ctx);
const descriptor = createMessageDescriptorFromTokens(
tokens,
currentPath.node.loc,
_ctx.stripNonEssentialProps,
_ctx.stripMessageProp
);
const callExpr = t.callExpression(
t.identifier(uniqTIdentifier.name),
[descriptor]
);
return currentPath.replaceWith(callExpr);
}
if (currentPath.isCallExpression() && currentPath.get("arguments")[0]?.isObjectExpression()) {
const descriptor = processDescriptor(
currentPath.get("arguments")[0].node,
_ctx
);
const callExpr = t.callExpression(
t.identifier(uniqTIdentifier.name),
[descriptor]
);
return currentPath.replaceWith(callExpr);
}
refPath.replaceWith(t.identifier(uniqTIdentifier.name));
});
_property.key.name = "_";
_property.value.name = uniqTIdentifier.name;
return t.callExpression(t.identifier(this.useLinguiImportName), []);
}
createI18nCall(messageDescriptor, linguiInstance) {
return t.callExpression(
t.memberExpression(
linguiInstance ?? t.identifier(this.i18nImportName),
t.identifier("_")
),
messageDescriptor ? [messageDescriptor] : []
);
}
}
let config;
function getConfig(_config) {
if (_config) {
config = _config;
}
if (!config) {
config = getConfig$1();
}
return config;
}
function reportUnsupportedSyntax(path, e) {
const codeFrameError = path.buildCodeFrameError(
`Unsupported macro usage. Please check the examples at https://lingui.dev/ref/macro#examples-of-js-macros.
If you think this is a bug, fill in an issue at https://github.com/lingui/js-lingui/issues
Error: ${e.message}`
);
codeFrameError.stack = codeFrameError.message + "\n" + e.stack;
throw codeFrameError;
}
function shouldStripMessageProp(opts) {
if (typeof opts.stripMessageField === "boolean") {
return opts.stripMessageField;
}
return process.env.NODE_ENV === "production" && !opts.extract;
}
const getIdentifierPath = (path, node) => {
let foundPath;
path.traverse(
{
Identifier: (path2) => {
if (path2.node === node) {
foundPath = path2;
path2.stop();
}
}
},
path.state
);
return foundPath;
};
const LinguiSymbolToImportMap = {
Trans: "jsxPackage",
useLingui: "jsxPackage",
i18n: "corePackage"
};
function linguiPlugin({
types: t
}) {
function addImport(macroImports, state, name) {
const [path] = macroImports[LinguiSymbolToImportMap[name]];
const config2 = state.get("linguiConfig");
if (!state.get("has_import_" + name)) {
state.set("has_import_" + name, true);
const [moduleSource, importName] = config2.runtimeConfigModule[name];
const importDecl = t.importDeclaration(
[
t.importSpecifier(
getSymbolIdentifier(state, name),
t.identifier(importName)
)
],
t.stringLiteral(moduleSource)
);
importDecl.loc = path.node.loc;
const [newPath] = path.insertAfter(importDecl);
path.parentPath.scope.registerDeclaration(newPath);
}
return path.parentPath.scope.getBinding(
getSymbolIdentifier(state, name).name
);
}
function getMacroImports(path) {
const corePackage = new Set(
path.get("body").filter(
(statement) => statement.isImportDeclaration() && config.macro.corePackage.includes(
statement.get("source").node.value
)
)
);
const jsxPackage = new Set(
path.get("body").filter(
(statement) => statement.isImportDeclaration() && config.macro.jsxPackage.includes(statement.get("source").node.value)
)
);
return {
corePackage,
jsxPackage,
all: /* @__PURE__ */ new Set([...corePackage, ...jsxPackage])
};
}
function getSymbolIdentifier(state, name) {
return state.get("linguiIdentifiers")[name];
}
function isLinguiIdentifier(path, node, macro) {
let identPath = getIdentifierPath(path, node);
if (macro === JsMacroName.useLingui) {
if (config.macro.jsxPackage.some(
(moduleSource) => identPath.referencesImport(moduleSource, JsMacroName.useLingui)
)) {
return true;
}
} else {
identPath = identPath || getIdentifierPath(path.getFunctionParent(), node);
if (config.macro.corePackage.some(
(moduleSource) => identPath.referencesImport(moduleSource, macro)
)) {
return true;
}
}
return false;
}
return {
name: "lingui-macro-plugin",
visitor: {
Program: {
enter(path, state) {
state.set(
"linguiConfig",
getConfig(state.opts.linguiConfig)
);
const macroImports = getMacroImports(path);
if (!macroImports.all.size) {
return;
}
state.set("linguiIdentifiers", {
i18n: path.scope.generateUidIdentifier("i18n"),
Trans: path.scope.generateUidIdentifier("Trans"),
useLingui: path.scope.generateUidIdentifier("useLingui")
});
path.traverse(
{
JSXElement(path2, state2) {
const macro = new MacroJSX(
{ types: t },
{
transImportName: getSymbolIdentifier(state2, "Trans").name,
stripNonEssentialProps: process.env.NODE_ENV == "production" && !state2.opts.extract,
stripMessageProp: shouldStripMessageProp(
state2.opts
),
isLinguiIdentifier: (node, macro2) => isLinguiIdentifier(path2, node, macro2)
}
);
let newNode;
try {
newNode = macro.replacePath(path2);
} catch (e) {
reportUnsupportedSyntax(path2, e);
}
if (newNode) {
const [newPath] = path2.replaceWith(newNode);
addImport(macroImports, state2, "Trans").reference(newPath);
}
},
"CallExpression|TaggedTemplateExpression"(path2, state2) {
const macro = new MacroJs({
stripNonEssentialProps: process.env.NODE_ENV == "production" && !state2.opts.extract,
stripMessageProp: shouldStripMessageProp(
state2.opts
),
i18nImportName: getSymbolIdentifier(state2, "i18n").name,
useLinguiImportName: getSymbolIdentifier(state2, "useLingui").name,
isLinguiIdentifier: (node, macro2) => isLinguiIdentifier(path2, node, macro2)
});
let newNode;
try {
newNode = macro.replacePath(path2);
} catch (e) {
reportUnsupportedSyntax(path2, e);
}
if (newNode) {
const [newPath] = path2.replaceWith(newNode);
if (macro.needsUseLinguiImport) {
addImport(macroImports, state2, "useLingui").reference(
newPath
);
}
if (macro.needsI18nImport) {
addImport(macroImports, state2, "i18n").reference(newPath);
}
}
}
},
state
);
},
exit(path, state) {
const macroImports = getMacroImports(path);
macroImports.all.forEach((path2) => path2.remove());
}
}
}
};
}
export { linguiPlugin as default };