@transifex/cli
Version:
i18n CLI tool for Transifex Native
170 lines (153 loc) • 5.41 kB
JavaScript
const _ = require('lodash');
const vueTemplateCompiler = require('@vue/compiler-sfc');
const { createPayload, isPayloadValid } = require('./utils');
const { mergePayload } = require('../merge');
const { babelExtractPhrases } = require('./babel');
/**
* A function to traverse the AST provided by the vue/compiler-sfc package
*
* @param {*} ast
* @param {*} visitor
*/
function traverseVueTemplateAst(ast, visitor = {}) {
// Use this in order to identify expressions in the AST since there is no
// Tag to identify it with when traversing
const VISITORS = {
5: 'Expression',
};
function traverseArray(array, parent) {
array.forEach((child) => {
// eslint-disable-next-line no-use-before-define
traverseNode(child, parent);
});
}
function traverseNode(node, parent) {
if (visitor.enter) visitor.enter(node, parent);
if (visitor[node.tag]) visitor[node.tag](node, parent);
// Take care of expressions
if (visitor[VISITORS[node.type]] && node.content) {
if (node.content.loc.source) visitor[VISITORS[node.type]](node, parent);
}
if (node.children) traverseArray(node.children, node);
if (node.content) {
if (node.content.children) traverseArray(node.content.children, node);
traverseNode(node.content, node);
}
// Take care of template conditions
if (node.branches) {
if (node.branches.length) {
node.branches.forEach((element) => {
traverseArray(element.children, element);
});
}
}
// Take care of props
if (node.props) {
if (node.props.length) {
node.props.forEach((element) => {
if (element.exp && element.exp.type === 8) {
visitor.PropsExpression(element, node);
}
});
}
}
if (visitor.exit) visitor.exit(node, parent);
}
traverseNode(ast, null);
}
/**
* Extracted function for the T/UT components to use as a visitor for the
* template AST traverse.
*
* @param {Object} HASHES A map of keys and content for phrases
* @param {String} relativeFile occurence
* @param {Object} options
* @param {String[]} options.appendTags
* @param {String[]} options.filterWithTags
* @param {String[]} options.filterWithoutTags
* @param {Boolean} options.useHashedKeys
* @returns {Function}
*/
function vueElementVisitor(HASHES, relativeFile, options) {
return (node) => {
let string;
const params = {};
_.each(node.props, (attr) => {
const property = attr.name;
if (!property || !attr.value) return;
const attrValue = attr.value.content;
if (!attrValue) return;
if (property === '_str') {
string = attrValue;
return;
}
params[property] = attrValue;
});
if (!string) return;
const partial = createPayload(string, params, relativeFile, options);
if (!isPayloadValid(partial, options)) return;
mergePayload(HASHES, {
[partial.key]: {
string: partial.string,
meta: partial.meta,
},
});
};
}
/**
* Parses the source with vue-template-compiler package and breaks the content
* to "template" and "script".
*
* The script gets passed to babel as usual to get the phrases and the template
* goes through compilation again to get the AST and a custom traverse
* function in order to get the phrases back.
*
* @param {Object} HASHES A map of keys and content for phrases
* @param {String} source The content we want to parse
* @param {String} relativeFile occurence
* @param {Object} options
* @param {String[]} options.appendTags
* @param {String[]} options.filterWithTags
* @param {String[]} options.filterWithoutTags
* @param {Boolean} options.useHashedKeys
*/
function extractVuePhrases(HASHES, source, relativeFile, options) {
// Use the vue-template-compiler API to parse content
const vueContent = vueTemplateCompiler.parse(source);
// Get the JS Content from the file and extract hashes/phrases with Babel
if (vueContent.descriptor.script && vueContent.descriptor.script.content) {
const script = vueContent.descriptor.script.content;
babelExtractPhrases(HASHES, script, relativeFile, options);
}
// Also detect Vue3 script setup
if (vueContent.descriptor.scriptSetup && vueContent.descriptor.scriptSetup.content) {
const script = vueContent.descriptor.scriptSetup.content;
babelExtractPhrases(HASHES, script, relativeFile, options);
}
// Get the template content from the file and extract hashes/phrases with
// custom traverse function
if (vueContent.descriptor.template && vueContent.descriptor.template.content) {
// Compile to get the AST
const template = vueTemplateCompiler.compileTemplate({
id: 'sfc-compiler',
source: vueContent.descriptor.template.content,
});
traverseVueTemplateAst(template.ast, {
PropsExpression(node) {
// Complex props might cause unrelated errors in Babel
try {
babelExtractPhrases(HASHES, node.exp.loc.source, relativeFile, options);
// eslint-disable-next-line no-empty
} catch (e) {}
},
Expression(node) {
babelExtractPhrases(HASHES, node.content.loc.source, relativeFile, options);
},
T: vueElementVisitor(HASHES, relativeFile, options),
UT: vueElementVisitor(HASHES, relativeFile, options),
});
}
}
module.exports = {
extractVuePhrases,
};