'use strict'; function collectMessage(path, props, ctx) { if (props.id === void 0) return; const node = path.node; const line = node.loc ? node.loc.start.line : null; const column = node.loc ? node.loc.start.column : null; ctx.opts.onMessageExtracted({ id: props.id, message: props.message, context: props.context, comment: props.comment, placeholders: props.placeholders || {}, origin: [ctx.file.opts.filename, line, column] }); } function getTextFromExpression(t, exp, hub, emitErrorOnVariable = true) { if (t.isStringLiteral(exp)) { return exp.value; } if (t.isBinaryExpression(exp)) { return getTextFromExpression( t, exp.left, hub, emitErrorOnVariable ) + getTextFromExpression( t, exp.right, hub, emitErrorOnVariable ); } if (t.isTemplateLiteral(exp)) { if (exp?.quasis.length > 1) { console.warn( hub.buildError( exp, "Could not extract from template literal with expressions.", SyntaxError ).message ); return ""; } return exp.quasis[0]?.value?.cooked; } if (emitErrorOnVariable) { console.warn( hub.buildError( exp, "Only strings or template literals could be extracted.", SyntaxError ).message ); } } function getNodeSource(fileContents, node) { return fileContents.slice(node.start, node.end); } function valuesObjectExpressionToPlaceholdersRecord(t, exp, hub) { const props = {}; exp.properties.forEach(({ key, value }, i) => { let name; if (t.isStringLiteral(key) || t.isNumericLiteral(key)) { name = key.value.toString(); } else if (t.isIdentifier(key)) { name = key.name; } else { console.warn( hub.buildError( exp, `Could not extract values to placeholders. The key #${i} has unsupported syntax`, SyntaxError ).message ); } if (name) { props[name] = getNodeSource(hub.getCode(), value); } }); return props; } function extractFromObjectExpression(t, exp, hub) { const props = {}; const textKeys = ["id", "message", "comment", "context"]; exp.properties.forEach(({ key, value }, i) => { const name = key.name; if (name === "values" && t.isObjectExpression(value)) { props.placeholders = valuesObjectExpressionToPlaceholdersRecord( t, value, hub ); } else if (textKeys.includes(name)) { props[name] = getTextFromExpression( t, value, hub ); } }); return props; } const I18N_OBJECT = "i18n"; function hasComment(node, comment) { return node.leadingComments && node.leadingComments.some((comm) => comm.value.trim() === comment); } function hasIgnoreComment(node) { return hasComment(node, "lingui-extract-ignore"); } function hasI18nComment(node) { return hasComment(node, "i18n"); } function index({ types: t }) { function isTransComponent(path) { return path.isJSXElement() && path.get("openingElement").get("name").referencesImport("@lingui/react", "Trans"); } const isI18nMethod = (node) => t.isMemberExpression(node) && (t.isIdentifier(node.object, { name: I18N_OBJECT }) || t.isMemberExpression(node.object) && t.isIdentifier(node.object.property, { name: I18N_OBJECT })) && (t.isIdentifier(node.property, { name: "_" }) || t.isIdentifier(node.property, { name: "t" })); const extractFromMessageDescriptor = (path, ctx) => { const props = extractFromObjectExpression(t, path.node, ctx.file.hub); if (!props.id) { console.warn( path.buildCodeFrameError("Missing message ID, skipping.").message ); return; } collectMessage(path, props, ctx); }; return { visitor: { // Extract translation from component. JSXElement(path, ctx) { const { node } = path; if (!isTransComponent(path)) return; const attrs = node.openingElement.attributes || []; if (attrs.find( (attr) => t.isJSXSpreadAttribute(attr) && hasI18nComment(attr.argument) )) { return; } const props = attrs.reduce((acc, item) => { if (t.isJSXSpreadAttribute(item)) { return acc; } const key = item.name.name; if (key === "id" || key === "message" || key === "comment" || key === "context") { if (t.isStringLiteral(item.value)) { acc[key] = item.value.value; } else if (t.isJSXExpressionContainer(item.value) && t.isStringLiteral(item.value.expression)) { acc[key] = item.value.expression.value; } } if (key === "values" && t.isJSXExpressionContainer(item.value) && t.isObjectExpression(item.value.expression)) { acc.placeholders = valuesObjectExpressionToPlaceholdersRecord( t, item.value.expression, ctx.file.hub ); } return acc; }, {}); if (!props.id) { const idProp = attrs.filter( (item) => t.isJSXAttribute(item) && item.name.name === "id" )[0]; if (idProp === void 0 || t.isLiteral(props.id)) { console.warn( path.buildCodeFrameError("Missing message ID, skipping.").message ); } return; } collectMessage(path, props, ctx); }, CallExpression(path, ctx) { if ([path.node, path.parent].some((node) => hasIgnoreComment(node))) { return; } const firstArgument = path.get("arguments")[0]; if (!isI18nMethod(path.node.callee)) { return; } if (hasI18nComment(firstArgument.node)) { return; } if (firstArgument.isObjectExpression()) { extractFromMessageDescriptor(firstArgument, ctx); return; } else { let props = { id: getTextFromExpression( t, firstArgument.node, ctx.file.hub, false ) }; if (!props.id) { return; } const secondArgument = path.node.arguments[1]; if (secondArgument && t.isObjectExpression(secondArgument)) { props.placeholders = valuesObjectExpressionToPlaceholdersRecord( t, secondArgument, ctx.file.hub ); } const msgDescArg = path.node.arguments[2]; if (t.isObjectExpression(msgDescArg)) { props = { ...props, ...extractFromObjectExpression(t, msgDescArg, ctx.file.hub) }; } collectMessage(path, props, ctx); } }, StringLiteral(path, ctx) { if (!hasI18nComment(path.node)) { return; } const props = { id: path.node.value }; if (!props.id) { console.warn( path.buildCodeFrameError("Empty StringLiteral, skipping.").message ); return; } collectMessage(path, props, ctx); }, // Extract message descriptors ObjectExpression(path, ctx) { if (!hasI18nComment(path.node)) return; extractFromMessageDescriptor(path, ctx); } } }; } module.exports = index;