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
			| 
											3 years ago
										 | 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, | ||
|  |   ]; | ||
|  | } |