mirror of https://gitee.com/antv-l7/antv-l7
401 lines
14 KiB
JavaScript
401 lines
14 KiB
JavaScript
const { createMacro } = require('babel-plugin-macros');
|
|
|
|
// copy to:
|
|
// https://astexplorer.net/#/gist/642aebbb9e449e959f4ad8907b4adf3a/4a65742e2a3e926eb55eaa3d657d1472b9ac7970
|
|
module.exports = createMacro(ICUMacro);
|
|
|
|
function ICUMacro({ references, state, babel }) {
|
|
const t = babel.types;
|
|
const { Trans = [], Plural = [], Select = [], SelectOrdinal = [] } = references;
|
|
|
|
// assert we have the react-i18next Trans component imported
|
|
addNeededImports(state, babel);
|
|
|
|
// transform Plural and SelectOrdinal
|
|
[...Plural, ...SelectOrdinal].forEach(referencePath => {
|
|
if (referencePath.parentPath.type === 'JSXOpeningElement') {
|
|
pluralAsJSX(
|
|
referencePath.parentPath,
|
|
{
|
|
attributes: referencePath.parentPath.get('attributes'),
|
|
children: referencePath.parentPath.parentPath.get('children'),
|
|
},
|
|
babel,
|
|
);
|
|
} else {
|
|
// throw a helpful error message or something :)
|
|
}
|
|
});
|
|
|
|
// transform Select
|
|
Select.forEach(referencePath => {
|
|
if (referencePath.parentPath.type === 'JSXOpeningElement') {
|
|
selectAsJSX(
|
|
referencePath.parentPath,
|
|
{
|
|
attributes: referencePath.parentPath.get('attributes'),
|
|
children: referencePath.parentPath.parentPath.get('children'),
|
|
},
|
|
babel,
|
|
);
|
|
} else {
|
|
// throw a helpful error message or something :)
|
|
}
|
|
});
|
|
|
|
// transform Trans
|
|
Trans.forEach(referencePath => {
|
|
if (referencePath.parentPath.type === 'JSXOpeningElement') {
|
|
transAsJSX(
|
|
referencePath.parentPath,
|
|
{
|
|
attributes: referencePath.parentPath.get('attributes'),
|
|
children: referencePath.parentPath.parentPath.get('children'),
|
|
},
|
|
babel,
|
|
);
|
|
} else {
|
|
// throw a helpful error message or something :)
|
|
}
|
|
});
|
|
}
|
|
|
|
function pluralAsJSX(parentPath, { attributes }, babel) {
|
|
const t = babel.types;
|
|
const toObjectProperty = (name, value) =>
|
|
t.objectProperty(t.identifier(name), t.identifier(name), false, !value);
|
|
|
|
// plural or selectordinal
|
|
const nodeName = parentPath.node.name.name.toLocaleLowerCase();
|
|
|
|
// will need to merge count attribute with existing values attribute in some cases
|
|
const existingValuesAttribute = findAttribute('values', attributes);
|
|
const existingValues = existingValuesAttribute
|
|
? existingValuesAttribute.node.value.expression.properties
|
|
: [];
|
|
|
|
let componentStartIndex = 0;
|
|
const extracted = attributes.reduce(
|
|
(mem, attr) => {
|
|
if (attr.node.name.name === 'i18nKey') {
|
|
// copy the i18nKey
|
|
mem.attributesToCopy.push(attr.node);
|
|
} else if (attr.node.name.name === 'count') {
|
|
// take the count for element
|
|
let exprName = attr.node.value.expression.name;
|
|
if (!exprName) {
|
|
exprName = 'count';
|
|
}
|
|
if (exprName === 'count') {
|
|
// if the prop expression name is also "count", copy it instead: <Plural count={count} --> <Trans count={count}
|
|
mem.attributesToCopy.push(attr.node);
|
|
} else {
|
|
mem.values.unshift(toObjectProperty(exprName));
|
|
}
|
|
mem.defaults = `{${exprName}, ${nodeName}, ${mem.defaults}`;
|
|
} else if (attr.node.name.name === 'values') {
|
|
// skip the values attribute, as it has already been processed into mem from existingValues
|
|
} else if (attr.node.value.type === 'StringLiteral') {
|
|
// take any string node as plural option
|
|
let pluralForm = attr.node.name.name;
|
|
if (pluralForm.indexOf('$') === 0) pluralForm = pluralForm.replace('$', '=');
|
|
mem.defaults = `${mem.defaults} ${pluralForm} {${attr.node.value.value}}`;
|
|
} else if (attr.node.value.type === 'JSXExpressionContainer') {
|
|
// convert any Trans component to plural option extracting any values and components
|
|
const children = attr.node.value.expression.children || [];
|
|
const thisTrans = processTrans(children, babel, componentStartIndex);
|
|
|
|
let pluralForm = attr.node.name.name;
|
|
if (pluralForm.indexOf('$') === 0) pluralForm = pluralForm.replace('$', '=');
|
|
|
|
mem.defaults = `${mem.defaults} ${pluralForm} {${thisTrans.defaults}}`;
|
|
mem.components = mem.components.concat(thisTrans.components);
|
|
|
|
componentStartIndex += thisTrans.components.length;
|
|
}
|
|
return mem;
|
|
},
|
|
{ attributesToCopy: [], values: existingValues, components: [], defaults: '' },
|
|
);
|
|
|
|
// replace the node with the new Trans
|
|
parentPath.replaceWith(buildTransElement(extracted, extracted.attributesToCopy, t, true));
|
|
}
|
|
|
|
function selectAsJSX(parentPath, { attributes }, babel) {
|
|
const t = babel.types;
|
|
const toObjectProperty = (name, value) =>
|
|
t.objectProperty(t.identifier(name), t.identifier(name), false, !value);
|
|
|
|
// will need to merge switch attribute with existing values attribute
|
|
const existingValuesAttribute = findAttribute('values', attributes);
|
|
const existingValues = existingValuesAttribute
|
|
? existingValuesAttribute.node.value.expression.properties
|
|
: [];
|
|
|
|
let componentStartIndex = 0;
|
|
|
|
const extracted = attributes.reduce(
|
|
(mem, attr) => {
|
|
if (attr.node.name.name === 'i18nKey') {
|
|
// copy the i18nKey
|
|
mem.attributesToCopy.push(attr.node);
|
|
} else if (attr.node.name.name === 'switch') {
|
|
// take the switch for select element
|
|
let exprName = attr.node.value.expression.name;
|
|
if (!exprName) {
|
|
exprName = 'selectKey';
|
|
mem.values.unshift(t.objectProperty(t.identifier(exprName), attr.node.value.expression));
|
|
} else {
|
|
mem.values.unshift(toObjectProperty(exprName));
|
|
}
|
|
mem.defaults = `{${exprName}, select, ${mem.defaults}`;
|
|
} else if (attr.node.name.name === 'values') {
|
|
// skip the values attribute, as it has already been processed into mem as existingValues
|
|
} else if (attr.node.value.type === 'StringLiteral') {
|
|
// take any string node as select option
|
|
mem.defaults = `${mem.defaults} ${attr.node.name.name} {${attr.node.value.value}}`;
|
|
} else if (attr.node.value.type === 'JSXExpressionContainer') {
|
|
// convert any Trans component to select option extracting any values and components
|
|
const children = attr.node.value.expression.children || [];
|
|
const thisTrans = processTrans(children, babel, componentStartIndex);
|
|
|
|
mem.defaults = `${mem.defaults} ${attr.node.name.name} {${thisTrans.defaults}}`;
|
|
mem.components = mem.components.concat(thisTrans.components);
|
|
|
|
componentStartIndex += thisTrans.components.length;
|
|
}
|
|
return mem;
|
|
},
|
|
{ attributesToCopy: [], values: existingValues, components: [], defaults: '' },
|
|
);
|
|
|
|
// replace the node with the new Trans
|
|
parentPath.replaceWith(buildTransElement(extracted, extracted.attributesToCopy, t, true));
|
|
}
|
|
|
|
function transAsJSX(parentPath, { attributes, children }, babel) {
|
|
const defaultsAttr = findAttribute('defaults', attributes);
|
|
const componentsAttr = findAttribute('components', attributes);
|
|
// if there is "defaults" attribute and no "components" attribute, parse defaults and extract from the parsed defaults instead of children
|
|
// if a "components" attribute has been provided, we assume they have already constructed a valid "defaults" and it does not need to be parsed
|
|
const parseDefaults = defaultsAttr && !componentsAttr;
|
|
|
|
let extracted;
|
|
if (parseDefaults) {
|
|
const defaultsExpression = defaultsAttr.node.value.value;
|
|
const parsed = babel.parse(`<>${defaultsExpression}</>`, {
|
|
presets: ['@babel/react'],
|
|
}).program.body[0].expression.children;
|
|
|
|
extracted = processTrans(parsed, babel);
|
|
} else {
|
|
extracted = processTrans(children, babel);
|
|
}
|
|
|
|
let clonedAttributes = cloneExistingAttributes(attributes);
|
|
if (parseDefaults) {
|
|
// remove existing defaults so it can be replaced later with the new parsed defaults
|
|
clonedAttributes = clonedAttributes.filter(node => node.name.name !== 'defaults');
|
|
}
|
|
|
|
// replace the node with the new Trans
|
|
const replacePath = children.length ? children[0].parentPath : parentPath;
|
|
replacePath.replaceWith(
|
|
buildTransElement(extracted, clonedAttributes, babel.types, false, !!children.length),
|
|
);
|
|
}
|
|
|
|
function buildTransElement(
|
|
extracted,
|
|
finalAttributes,
|
|
t,
|
|
closeDefaults = false,
|
|
wasElementWithChildren = false,
|
|
) {
|
|
const nodeName = t.jSXIdentifier('Trans');
|
|
|
|
// plural, select open { but do not close it while reduce
|
|
if (closeDefaults) extracted.defaults += '}';
|
|
|
|
// convert arrays into needed expressions
|
|
extracted.components = t.arrayExpression(extracted.components);
|
|
extracted.values = t.objectExpression(extracted.values);
|
|
|
|
// add generated Trans attributes
|
|
if (!attributeExistsAlready('defaults', finalAttributes))
|
|
finalAttributes.push(
|
|
t.jSXAttribute(t.jSXIdentifier('defaults'), t.StringLiteral(extracted.defaults)),
|
|
);
|
|
if (!attributeExistsAlready('components', finalAttributes))
|
|
finalAttributes.push(
|
|
t.jSXAttribute(t.jSXIdentifier('components'), t.jSXExpressionContainer(extracted.components)),
|
|
);
|
|
if (!attributeExistsAlready('values', finalAttributes))
|
|
finalAttributes.push(
|
|
t.jSXAttribute(t.jSXIdentifier('values'), t.jSXExpressionContainer(extracted.values)),
|
|
);
|
|
|
|
// create selfclosing Trans component
|
|
const openElement = t.jSXOpeningElement(nodeName, finalAttributes, true);
|
|
if (!wasElementWithChildren) return openElement;
|
|
|
|
return t.jSXElement(openElement, null, [], true);
|
|
}
|
|
|
|
function cloneExistingAttributes(attributes) {
|
|
return attributes.reduce((mem, attr) => {
|
|
mem.push(attr.node);
|
|
return mem;
|
|
}, []);
|
|
}
|
|
|
|
function findAttribute(name, attributes) {
|
|
return attributes.find(child => {
|
|
const ele = child.node ? child.node : child;
|
|
return ele.name.name === name;
|
|
});
|
|
}
|
|
|
|
function attributeExistsAlready(name, attributes) {
|
|
return !!findAttribute(name, attributes);
|
|
}
|
|
|
|
function processTrans(children, babel, componentStartIndex = 0) {
|
|
const res = {};
|
|
|
|
res.defaults = mergeChildren(children, babel, componentStartIndex);
|
|
res.components = getComponents(children, babel);
|
|
res.values = getValues(children, babel);
|
|
|
|
return res;
|
|
}
|
|
|
|
// eslint-disable-next-line no-control-regex
|
|
const leadingNewLineAndWhitespace = new RegExp('^\n\\s+', 'g');
|
|
// eslint-disable-next-line no-control-regex
|
|
const trailingNewLineAndWhitespace = new RegExp('\n\\s+$', 'g');
|
|
function trimIndent(text) {
|
|
const newText = text
|
|
.replace(leadingNewLineAndWhitespace, '')
|
|
.replace(trailingNewLineAndWhitespace, '');
|
|
return newText;
|
|
}
|
|
|
|
function mergeChildren(children, babel, componentStartIndex = 0) {
|
|
const t = babel.types;
|
|
let componentFoundIndex = componentStartIndex;
|
|
|
|
return children.reduce((mem, child) => {
|
|
const ele = child.node ? child.node : child;
|
|
|
|
// add text, but trim indentation whitespace
|
|
if (t.isJSXText(ele) && ele.value) mem += trimIndent(ele.value);
|
|
// add ?!? forgot
|
|
if (ele.expression && ele.expression.value) mem += ele.expression.value;
|
|
// add `{ val }`
|
|
if (ele.expression && ele.expression.name) mem += `{${ele.expression.name}}`;
|
|
// add `{ val, number }`
|
|
if (ele.expression && ele.expression.expressions) {
|
|
mem += `{${ele.expression.expressions
|
|
.reduce((m, i) => {
|
|
m.push(i.name || i.value);
|
|
return m;
|
|
}, [])
|
|
.join(', ')}}`;
|
|
}
|
|
// add <strong>...</strong> with replace to <0>inner string</0>
|
|
if (t.isJSXElement(ele)) {
|
|
mem += `<${componentFoundIndex}>${mergeChildren(
|
|
ele.children,
|
|
babel,
|
|
)}</${componentFoundIndex}>`;
|
|
componentFoundIndex++;
|
|
}
|
|
|
|
return mem;
|
|
}, '');
|
|
}
|
|
|
|
function getValues(children, babel) {
|
|
const t = babel.types;
|
|
const toObjectProperty = (name, value) =>
|
|
t.objectProperty(t.identifier(name), t.identifier(name), false, !value);
|
|
|
|
return children.reduce((mem, child) => {
|
|
const ele = child.node ? child.node : child;
|
|
|
|
// add `{ var }` to values
|
|
if (ele.expression && ele.expression.name) mem.push(toObjectProperty(ele.expression.name));
|
|
// add `{ var, number }` to values
|
|
if (ele.expression && ele.expression.expressions)
|
|
mem.push(
|
|
toObjectProperty(ele.expression.expressions[0].name || ele.expression.expressions[0].value),
|
|
);
|
|
// add `{ var: 'bar' }` to values
|
|
if (ele.expression && ele.expression.properties) mem = mem.concat(ele.expression.properties);
|
|
// recursive add inner elements stuff to values
|
|
if (t.isJSXElement(ele)) {
|
|
mem = mem.concat(getValues(ele.children, babel));
|
|
}
|
|
|
|
return mem;
|
|
}, []);
|
|
}
|
|
|
|
function getComponents(children, babel) {
|
|
const t = babel.types;
|
|
|
|
return children.reduce((mem, child) => {
|
|
const ele = child.node ? child.node : child;
|
|
|
|
if (t.isJSXElement(ele)) {
|
|
const clone = t.clone(ele);
|
|
clone.children = clone.children.reduce((clonedMem, clonedChild) => {
|
|
const clonedEle = clonedChild.node ? clonedChild.node : clonedChild;
|
|
|
|
// clean out invalid definitions by replacing `{ catchDate, date, short }` with `{ catchDate }`
|
|
if (clonedEle.expression && clonedEle.expression.expressions)
|
|
clonedEle.expression.expressions = [clonedEle.expression.expressions[0]];
|
|
|
|
clonedMem.push(clonedChild);
|
|
return clonedMem;
|
|
}, []);
|
|
|
|
mem.push(ele);
|
|
}
|
|
|
|
return mem;
|
|
}, []);
|
|
}
|
|
|
|
function addNeededImports(state, babel) {
|
|
const t = babel.types;
|
|
const importsToAdd = ['Trans'];
|
|
|
|
// check if there is an existing react-i18next import
|
|
const existingImport = state.file.path.node.body.find(
|
|
importNode => t.isImportDeclaration(importNode) && importNode.source.value === 'react-i18next',
|
|
);
|
|
|
|
// append Trans to existing or add a new react-i18next import for the Trans
|
|
if (existingImport) {
|
|
importsToAdd.forEach(name => {
|
|
if (
|
|
existingImport.specifiers.findIndex(
|
|
specifier => specifier.imported && specifier.imported.name === name,
|
|
) === -1
|
|
) {
|
|
existingImport.specifiers.push(t.importSpecifier(t.identifier(name), t.identifier(name)));
|
|
}
|
|
});
|
|
} else {
|
|
state.file.path.node.body.unshift(
|
|
t.importDeclaration(
|
|
importsToAdd.map(name => t.importSpecifier(t.identifier(name), t.identifier(name))),
|
|
t.stringLiteral('react-i18next'),
|
|
),
|
|
);
|
|
}
|
|
}
|