|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400 |
- 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'),
- ),
- );
- }
- }
|