You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							732 lines
						
					
					
						
							25 KiB
						
					
					
				
			
		
		
	
	
							732 lines
						
					
					
						
							25 KiB
						
					
					
				| const { createMacro } = require('babel-plugin-macros');
 | |
| 
 | |
| // copy to:
 | |
| // https://astexplorer.net/#/gist/642aebbb9e449e959f4ad8907b4adf3a/4a65742e2a3e926eb55eaa3d657d1472b9ac7970
 | |
| module.exports = createMacro(ICUMacro);
 | |
| 
 | |
| function ICUMacro({ references, state, babel }) {
 | |
|   const {
 | |
|     Trans = [],
 | |
|     Plural = [],
 | |
|     Select = [],
 | |
|     SelectOrdinal = [],
 | |
|     number = [],
 | |
|     date = [],
 | |
|     select = [],
 | |
|     selectOrdinal = [],
 | |
|     plural = [],
 | |
|     time = [],
 | |
|   } = references;
 | |
| 
 | |
|   // assert we have the react-i18next Trans component imported
 | |
|   addNeededImports(state, babel, references);
 | |
| 
 | |
|   // 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,
 | |
|         state,
 | |
|       );
 | |
|     } else {
 | |
|       // throw a helpful error message or something :)
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   // check for number`` and others outside of <Trans>
 | |
|   Object.entries({
 | |
|     number,
 | |
|     date,
 | |
|     time,
 | |
|     select,
 | |
|     plural,
 | |
|     selectOrdinal,
 | |
|   }).forEach(([name, node]) => {
 | |
|     node.forEach((item) => {
 | |
|       let f = item.parentPath;
 | |
|       while (f) {
 | |
|         if (babel.types.isJSXElement(f)) {
 | |
|           if (f.node.openingElement.name.name === 'Trans') {
 | |
|             // this is a valid use of number/date/time/etc.
 | |
|             return;
 | |
|           }
 | |
|         }
 | |
|         f = f.parentPath;
 | |
|       }
 | |
|       throw new Error(
 | |
|         `"${name}\`\`" can only be used inside <Trans> in "${item.node.loc.filename}" on line ${item.node.loc.start.line}`,
 | |
|       );
 | |
|     });
 | |
|   });
 | |
| }
 | |
| 
 | |
| 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, { filename }) {
 | |
|   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'],
 | |
|       filename,
 | |
|     }).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))
 | |
|     if (extracted.defaults.includes(`"`)) {
 | |
|       // wrap defaults that contain double quotes in brackets
 | |
|       finalAttributes.push(
 | |
|         t.jSXAttribute(
 | |
|           t.jSXIdentifier('defaults'),
 | |
|           t.jSXExpressionContainer(t.StringLiteral(extracted.defaults)),
 | |
|         ),
 | |
|       );
 | |
|     } else {
 | |
|       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;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * add comma-delimited expressions like `{ val, number }`
 | |
|  */
 | |
| function mergeCommaExpressions(ele) {
 | |
|   if (ele.expression && ele.expression.expressions) {
 | |
|     return `{${ele.expression.expressions
 | |
|       .reduce((m, i) => {
 | |
|         m.push(i.name || i.value);
 | |
|         return m;
 | |
|       }, [])
 | |
|       .join(', ')}}`;
 | |
|   }
 | |
|   return '';
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * this is for supporting complex icu type interpolations
 | |
|  * date`${variable}` and number`{${varName}, ::percent}`
 | |
|  * also, plural`{${count}, one { ... } other { ... }}
 | |
|  */
 | |
| function mergeTaggedTemplateExpressions(ele, componentFoundIndex, t, babel) {
 | |
|   if (t.isTaggedTemplateExpression(ele.expression)) {
 | |
|     const [, text, index] = getTextAndInterpolatedVariables(
 | |
|       ele.expression.tag.name,
 | |
|       ele.expression,
 | |
|       componentFoundIndex,
 | |
|       babel,
 | |
|     );
 | |
|     return [text, index];
 | |
|   }
 | |
|   return ['', componentFoundIndex];
 | |
| }
 | |
| 
 | |
| 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;
 | |
|     let result = mem;
 | |
| 
 | |
|     // add text, but trim indentation whitespace
 | |
|     if (t.isJSXText(ele) && ele.value) result += trimIndent(ele.value);
 | |
|     // add ?!? forgot
 | |
|     if (ele.expression && ele.expression.value) result += ele.expression.value;
 | |
|     // add `{ val }`
 | |
|     if (ele.expression && ele.expression.name) result += `{${ele.expression.name}}`;
 | |
|     // add `{ val, number }`
 | |
|     result += mergeCommaExpressions(ele);
 | |
|     const [nextText, newIndex] = mergeTaggedTemplateExpressions(ele, componentFoundIndex, t, babel);
 | |
|     result += nextText;
 | |
|     componentFoundIndex = newIndex;
 | |
|     // add <strong>...</strong> with replace to <0>inner string</0>
 | |
|     if (t.isJSXElement(ele)) {
 | |
|       result += `<${componentFoundIndex}>${mergeChildren(
 | |
|         ele.children,
 | |
|         babel,
 | |
|       )}</${componentFoundIndex}>`;
 | |
|       componentFoundIndex += 1;
 | |
|     }
 | |
| 
 | |
|     return result;
 | |
|   }, '');
 | |
| }
 | |
| 
 | |
| const extractTaggedTemplateValues = (ele, babel, toObjectProperty) => {
 | |
|   // date`${variable}` and so on
 | |
|   if (ele.expression && ele.expression.type === 'TaggedTemplateExpression') {
 | |
|     const [variables] = getTextAndInterpolatedVariables(
 | |
|       ele.expression.tag.name,
 | |
|       ele.expression,
 | |
|       0,
 | |
|       babel,
 | |
|     );
 | |
|     return variables.map((vari) => toObjectProperty(vari));
 | |
|   }
 | |
|   return [];
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Extract the names of interpolated value as object properties to pass to Trans
 | |
|  */
 | |
| 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;
 | |
|     let result = mem;
 | |
| 
 | |
|     // 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)
 | |
|       result.push(
 | |
|         toObjectProperty(ele.expression.expressions[0].name || ele.expression.expressions[0].value),
 | |
|       );
 | |
|     // add `{ var: 'bar' }` to values
 | |
|     if (ele.expression && ele.expression.properties)
 | |
|       result = result.concat(ele.expression.properties);
 | |
|     // date`${variable}` and so on
 | |
|     result = result.concat(extractTaggedTemplateValues(ele, babel, toObjectProperty));
 | |
|     // recursive add inner elements stuff to values
 | |
|     if (t.isJSXElement(ele)) {
 | |
|       result = result.concat(getValues(ele.children, babel));
 | |
|     }
 | |
| 
 | |
|     return result;
 | |
|   }, []);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Common logic for adding a child element of Trans to the list of components to hydrate the translation
 | |
|  * @param {JSXElement} jsxElement
 | |
|  * @param {JSXElement[]} mem
 | |
|  */
 | |
| const processJSXElement = (jsxElement, mem, t) => {
 | |
|   const clone = t.clone(jsxElement);
 | |
|   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(jsxElement);
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Extract the React components to pass to Trans as components
 | |
|  */
 | |
| function getComponents(children, babel) {
 | |
|   const t = babel.types;
 | |
| 
 | |
|   return children.reduce((mem, child) => {
 | |
|     const ele = child.node ? child.node : child;
 | |
| 
 | |
|     if (t.isJSXExpressionContainer(ele)) {
 | |
|       // check for date`` and so on
 | |
|       if (t.isTaggedTemplateExpression(ele.expression)) {
 | |
|         ele.expression.quasi.expressions.forEach((expr) => {
 | |
|           // check for sub-expressions. This can happen with plural`` or select`` or selectOrdinal``
 | |
|           // these can have nested components
 | |
|           if (t.isTaggedTemplateExpression(expr) && expr.quasi.expressions.length) {
 | |
|             mem.push(...getComponents(expr.quasi.expressions, babel));
 | |
|           }
 | |
|           if (!t.isJSXElement(expr)) {
 | |
|             // ignore anything that is not a component
 | |
|             return;
 | |
|           }
 | |
|           processJSXElement(expr, mem, t);
 | |
|         });
 | |
|       }
 | |
|     }
 | |
|     if (t.isJSXElement(ele)) {
 | |
|       processJSXElement(ele, mem, t);
 | |
|     }
 | |
| 
 | |
|     return mem;
 | |
|   }, []);
 | |
| }
 | |
| 
 | |
| const icuInterpolators = ['date', 'time', 'number', 'plural', 'select', 'selectOrdinal'];
 | |
| const importsToAdd = ['Trans'];
 | |
| 
 | |
| /**
 | |
|  * helper split out of addNeededImports to make codeclimate happy
 | |
|  *
 | |
|  * This does the work of amending an existing import from "react-i18next", or
 | |
|  * creating a new one if it doesn't exist
 | |
|  */
 | |
| function addImports(state, existingImport, allImportsToAdd, t) {
 | |
|   // append imports to existing or add a new react-i18next import for the Trans and icu tagged template literals
 | |
|   if (existingImport) {
 | |
|     allImportsToAdd.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(
 | |
|         allImportsToAdd.map((name) => t.importSpecifier(t.identifier(name), t.identifier(name))),
 | |
|         t.stringLiteral('react-i18next'),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Add `import { Trans, number, date, <etc.> } from "react-i18next"` as needed
 | |
|  */
 | |
| function addNeededImports(state, babel, references) {
 | |
|   const t = babel.types;
 | |
| 
 | |
|   // 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',
 | |
|   );
 | |
|   // check for any of the tagged template literals that are used in the source, and add them
 | |
|   const usedRefs = Object.keys(references).filter((importName) => {
 | |
|     if (!icuInterpolators.includes(importName)) {
 | |
|       return false;
 | |
|     }
 | |
|     return references[importName].length;
 | |
|   });
 | |
| 
 | |
|   // combine Trans + any tagged template literals
 | |
|   const allImportsToAdd = importsToAdd.concat(usedRefs);
 | |
| 
 | |
|   addImports(state, existingImport, allImportsToAdd, t);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * iterate over a node detected inside a tagged template literal
 | |
|  *
 | |
|  * This is a helper function for `extractVariableNamesFromQuasiNodes` defined below
 | |
|  *
 | |
|  * this is called using reduce as a way of tricking what would be `.map()`
 | |
|  * into passing in the parameters needed to both modify `componentFoundIndex`,
 | |
|  * `stringOutput`, and `interpolatedVariableNames`
 | |
|  * and to pass in the dependencies babel, and type. Type is the template type.
 | |
|  * For "date``" the type will be `date`. for "number``" the type is `number`, etc.
 | |
|  */
 | |
| const extractNestedTemplatesAndComponents = (
 | |
|   { componentFoundIndex: lastIndex, babel, stringOutput, type, interpolatedVariableNames },
 | |
|   node,
 | |
| ) => {
 | |
|   let componentFoundIndex = lastIndex;
 | |
|   if (node.type === 'JSXElement') {
 | |
|     // perform the interpolation of components just as we do in a normal Trans setting
 | |
|     const subText = `<${componentFoundIndex}>${mergeChildren(
 | |
|       node.children,
 | |
|       babel,
 | |
|     )}</${componentFoundIndex}>`;
 | |
|     componentFoundIndex += 1;
 | |
|     stringOutput.push(subText);
 | |
|   } else if (node.type === 'TaggedTemplateExpression') {
 | |
|     // a nested date``/number``/plural`` etc., extract whatever is inside of it
 | |
|     const [variableNames, childText, newIndex] = getTextAndInterpolatedVariables(
 | |
|       node.tag.name,
 | |
|       node,
 | |
|       componentFoundIndex,
 | |
|       babel,
 | |
|     );
 | |
|     interpolatedVariableNames.push(...variableNames);
 | |
|     componentFoundIndex = newIndex;
 | |
|     stringOutput.push(childText);
 | |
|   } else if (node.type === 'Identifier') {
 | |
|     // turn date`${thing}` into `thing, date`
 | |
|     stringOutput.push(`${node.name}, ${type}`);
 | |
|   } else if (node.type === 'TemplateElement') {
 | |
|     // convert all whitespace into a single space for the text in the tagged template literal
 | |
|     stringOutput.push(node.value.cooked.replace(/\s+/g, ' '));
 | |
|   } else {
 | |
|     // unknown node type, ignore
 | |
|   }
 | |
|   return { componentFoundIndex, babel, stringOutput, type, interpolatedVariableNames };
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * filter the list of nodes within a tagged template literal to the 4 types we can process,
 | |
|  * and ignore anything else.
 | |
|  *
 | |
|  * this is a helper function for `extractVariableNamesFromQuasiNodes`
 | |
|  */
 | |
| const filterNodes = (node) => {
 | |
|   if (node.type === 'Identifier') {
 | |
|     // if the node has a name, keep it
 | |
|     return node.name;
 | |
|   }
 | |
|   if (node.type === 'JSXElement' || node.type === 'TaggedTemplateExpression') {
 | |
|     // always keep interpolated elements or other tagged template literals like a nested date`` inside a plural``
 | |
|     return true;
 | |
|   }
 | |
|   if (node.type === 'TemplateElement') {
 | |
|     // return the "cooked" (escaped) text for the text in the template literal (`, ::percent` in number`${varname}, ::percent`)
 | |
|     return node.value.cooked;
 | |
|   }
 | |
|   // unknown node type, ignore
 | |
|   return false;
 | |
| };
 | |
| 
 | |
| const errorOnInvalidQuasiNodes = (primaryNode) => {
 | |
|   const noInterpolationError = !primaryNode.quasi.expressions.length;
 | |
|   const wrongOrderError = primaryNode.quasi.quasis[0].value.raw.length;
 | |
|   const message = `${primaryNode.tag.name} argument must be interpolated ${
 | |
|     noInterpolationError ? 'in' : 'at the beginning of'
 | |
|   } "${primaryNode.tag.name}\`\`" in "${primaryNode.loc.filename}" on line ${
 | |
|     primaryNode.loc.start.line
 | |
|   }`;
 | |
|   if (noInterpolationError || wrongOrderError) {
 | |
|     throw new Error(message);
 | |
|   }
 | |
| };
 | |
| 
 | |
| const extractNodeVariableNames = (varNode, babel) => {
 | |
|   const interpolatedVariableNames = [];
 | |
|   if (varNode.type === 'JSXElement') {
 | |
|     // extract inner interpolated variables and add to the list
 | |
|     interpolatedVariableNames.push(
 | |
|       ...getValues(varNode.children, babel).map((value) => value.value.name),
 | |
|     );
 | |
|   } else if (varNode.type === 'Identifier') {
 | |
|     // the name of the interpolated variable
 | |
|     interpolatedVariableNames.push(varNode.name);
 | |
|   }
 | |
|   return interpolatedVariableNames;
 | |
| };
 | |
| 
 | |
| const extractVariableNamesFromQuasiNodes = (primaryNode, babel) => {
 | |
|   errorOnInvalidQuasiNodes(primaryNode);
 | |
|   // this will contain all the nodes to convert to the ICU messageformat text
 | |
|   // at first they are unsorted, but will be ordered correctly at the end of the function
 | |
|   const text = [];
 | |
|   // the variable names. These are converted to object references as required for the Trans values
 | |
|   // in getValues() (toObjectProperty helper function)
 | |
|   const interpolatedVariableNames = [];
 | |
|   primaryNode.quasi.expressions.forEach((varNode) => {
 | |
|     if (
 | |
|       !babel.types.isIdentifier(varNode) &&
 | |
|       !babel.types.isTaggedTemplateExpression(varNode) &&
 | |
|       !babel.types.isJSXElement(varNode)
 | |
|     ) {
 | |
|       throw new Error(
 | |
|         `Must pass a variable, not an expression to "${primaryNode.tag.name}\`\`" in "${primaryNode.loc.filename}" on line ${primaryNode.loc.start.line}`,
 | |
|       );
 | |
|     }
 | |
|     text.push(varNode);
 | |
|     interpolatedVariableNames.push(...extractNodeVariableNames(varNode, babel));
 | |
|   });
 | |
|   primaryNode.quasi.quasis.forEach((quasiNode) => {
 | |
|     // these are the text surrounding the variable interpolation
 | |
|     // so in date`${varname}, short` it would be `''` and `, short`.
 | |
|     // (the empty string before `${varname}` and the stuff after it)
 | |
|     text.push(quasiNode);
 | |
|   });
 | |
|   return { text, interpolatedVariableNames };
 | |
| };
 | |
| 
 | |
| const throwOnInvalidType = (type, primaryNode) => {
 | |
|   if (!icuInterpolators.includes(type)) {
 | |
|     throw new Error(
 | |
|       `Unsupported tagged template literal "${type}", must be one of date, time, number, plural, select, selectOrdinal in "${primaryNode.loc.filename}" on line ${primaryNode.loc.start.line}`,
 | |
|     );
 | |
|   }
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Retrieve the new text to use, and any interpolated variables
 | |
|  *
 | |
|  * This is used to process tagged template literals like date`${variable}` and number`${num}, ::percent`
 | |
|  *
 | |
|  * for the data example, it will return text of `{variable, date}` with a variable of `variable`
 | |
|  * for the number example, it will return text of `{num, number, ::percent}` with a variable of `num`
 | |
|  * @param {string} type the name of the tagged template (`date`, `number`, `plural`, etc. - any valid complex ICU type)
 | |
|  * @param {TaggedTemplateExpression} primaryNode the template expression node
 | |
|  * @param {int} index starting index number of components to be used for interpolations like <0>
 | |
|  * @param {*} babel
 | |
|  */
 | |
| function getTextAndInterpolatedVariables(type, primaryNode, index, babel) {
 | |
|   throwOnInvalidType(type, primaryNode);
 | |
|   const componentFoundIndex = index;
 | |
|   const { text, interpolatedVariableNames } = extractVariableNamesFromQuasiNodes(
 | |
|     primaryNode,
 | |
|     babel,
 | |
|   );
 | |
|   const { stringOutput, componentFoundIndex: newIndex } = text
 | |
|     .filter(filterNodes)
 | |
|     // sort by the order they appear in the source code
 | |
|     .sort((a, b) => {
 | |
|       if (a.start > b.start) return 1;
 | |
|       return -1;
 | |
|     })
 | |
|     .reduce(extractNestedTemplatesAndComponents, {
 | |
|       babel,
 | |
|       componentFoundIndex,
 | |
|       stringOutput: [],
 | |
|       type,
 | |
|       interpolatedVariableNames,
 | |
|     });
 | |
|   return [
 | |
|     interpolatedVariableNames,
 | |
|     `{${stringOutput.join('')}}`,
 | |
|     // return the new component interpolation index
 | |
|     newIndex,
 | |
|   ];
 | |
| }
 |