UNPKG

25.9 kBJavaScriptView Raw
1const { createMacro } = require('babel-plugin-macros');
2
3// copy to:
4// https://astexplorer.net/#/gist/642aebbb9e449e959f4ad8907b4adf3a/4a65742e2a3e926eb55eaa3d657d1472b9ac7970
5module.exports = createMacro(ICUMacro);
6
7function ICUMacro({ references, state, babel }) {
8 const {
9 Trans = [],
10 Plural = [],
11 Select = [],
12 SelectOrdinal = [],
13 number = [],
14 date = [],
15 select = [],
16 selectOrdinal = [],
17 plural = [],
18 time = [],
19 } = references;
20
21 // assert we have the react-i18next Trans component imported
22 addNeededImports(state, babel, references);
23
24 // transform Plural and SelectOrdinal
25 [...Plural, ...SelectOrdinal].forEach((referencePath) => {
26 if (referencePath.parentPath.type === 'JSXOpeningElement') {
27 pluralAsJSX(
28 referencePath.parentPath,
29 {
30 attributes: referencePath.parentPath.get('attributes'),
31 children: referencePath.parentPath.parentPath.get('children'),
32 },
33 babel,
34 );
35 } else {
36 // throw a helpful error message or something :)
37 }
38 });
39
40 // transform Select
41 Select.forEach((referencePath) => {
42 if (referencePath.parentPath.type === 'JSXOpeningElement') {
43 selectAsJSX(
44 referencePath.parentPath,
45 {
46 attributes: referencePath.parentPath.get('attributes'),
47 children: referencePath.parentPath.parentPath.get('children'),
48 },
49 babel,
50 );
51 } else {
52 // throw a helpful error message or something :)
53 }
54 });
55
56 // transform Trans
57 Trans.forEach((referencePath) => {
58 if (referencePath.parentPath.type === 'JSXOpeningElement') {
59 transAsJSX(
60 referencePath.parentPath,
61 {
62 attributes: referencePath.parentPath.get('attributes'),
63 children: referencePath.parentPath.parentPath.get('children'),
64 },
65 babel,
66 state,
67 );
68 } else {
69 // throw a helpful error message or something :)
70 }
71 });
72
73 // check for number`` and others outside of <Trans>
74 Object.entries({
75 number,
76 date,
77 time,
78 select,
79 plural,
80 selectOrdinal,
81 }).forEach(([name, node]) => {
82 node.forEach((item) => {
83 let f = item.parentPath;
84 while (f) {
85 if (babel.types.isJSXElement(f)) {
86 if (f.node.openingElement.name.name === 'Trans') {
87 // this is a valid use of number/date/time/etc.
88 return;
89 }
90 }
91 f = f.parentPath;
92 }
93 throw new Error(
94 `"${name}\`\`" can only be used inside <Trans> in "${item.node.loc.filename}" on line ${item.node.loc.start.line}`,
95 );
96 });
97 });
98}
99
100function pluralAsJSX(parentPath, { attributes }, babel) {
101 const t = babel.types;
102 const toObjectProperty = (name, value) =>
103 t.objectProperty(t.identifier(name), t.identifier(name), false, !value);
104
105 // plural or selectordinal
106 const nodeName = parentPath.node.name.name.toLocaleLowerCase();
107
108 // will need to merge count attribute with existing values attribute in some cases
109 const existingValuesAttribute = findAttribute('values', attributes);
110 const existingValues = existingValuesAttribute
111 ? existingValuesAttribute.node.value.expression.properties
112 : [];
113
114 let componentStartIndex = 0;
115 const extracted = attributes.reduce(
116 (mem, attr) => {
117 if (attr.node.name.name === 'i18nKey') {
118 // copy the i18nKey
119 mem.attributesToCopy.push(attr.node);
120 } else if (attr.node.name.name === 'count') {
121 // take the count for element
122 let exprName = attr.node.value.expression.name;
123 if (!exprName) {
124 exprName = 'count';
125 }
126 if (exprName === 'count') {
127 // if the prop expression name is also "count", copy it instead: <Plural count={count} --> <Trans count={count}
128 mem.attributesToCopy.push(attr.node);
129 } else {
130 mem.values.unshift(toObjectProperty(exprName));
131 }
132 mem.defaults = `{${exprName}, ${nodeName}, ${mem.defaults}`;
133 } else if (attr.node.name.name === 'values') {
134 // skip the values attribute, as it has already been processed into mem from existingValues
135 } else if (attr.node.value.type === 'StringLiteral') {
136 // take any string node as plural option
137 let pluralForm = attr.node.name.name;
138 if (pluralForm.indexOf('$') === 0) pluralForm = pluralForm.replace('$', '=');
139 mem.defaults = `${mem.defaults} ${pluralForm} {${attr.node.value.value}}`;
140 } else if (attr.node.value.type === 'JSXExpressionContainer') {
141 // convert any Trans component to plural option extracting any values and components
142 const children = attr.node.value.expression.children || [];
143 const thisTrans = processTrans(children, babel, componentStartIndex);
144
145 let pluralForm = attr.node.name.name;
146 if (pluralForm.indexOf('$') === 0) pluralForm = pluralForm.replace('$', '=');
147
148 mem.defaults = `${mem.defaults} ${pluralForm} {${thisTrans.defaults}}`;
149 mem.components = mem.components.concat(thisTrans.components);
150
151 componentStartIndex += thisTrans.components.length;
152 }
153 return mem;
154 },
155 { attributesToCopy: [], values: existingValues, components: [], defaults: '' },
156 );
157
158 // replace the node with the new Trans
159 parentPath.replaceWith(buildTransElement(extracted, extracted.attributesToCopy, t, true));
160}
161
162function selectAsJSX(parentPath, { attributes }, babel) {
163 const t = babel.types;
164 const toObjectProperty = (name, value) =>
165 t.objectProperty(t.identifier(name), t.identifier(name), false, !value);
166
167 // will need to merge switch attribute with existing values attribute
168 const existingValuesAttribute = findAttribute('values', attributes);
169 const existingValues = existingValuesAttribute
170 ? existingValuesAttribute.node.value.expression.properties
171 : [];
172
173 let componentStartIndex = 0;
174
175 const extracted = attributes.reduce(
176 (mem, attr) => {
177 if (attr.node.name.name === 'i18nKey') {
178 // copy the i18nKey
179 mem.attributesToCopy.push(attr.node);
180 } else if (attr.node.name.name === 'switch') {
181 // take the switch for select element
182 let exprName = attr.node.value.expression.name;
183 if (!exprName) {
184 exprName = 'selectKey';
185 mem.values.unshift(t.objectProperty(t.identifier(exprName), attr.node.value.expression));
186 } else {
187 mem.values.unshift(toObjectProperty(exprName));
188 }
189 mem.defaults = `{${exprName}, select, ${mem.defaults}`;
190 } else if (attr.node.name.name === 'values') {
191 // skip the values attribute, as it has already been processed into mem as existingValues
192 } else if (attr.node.value.type === 'StringLiteral') {
193 // take any string node as select option
194 mem.defaults = `${mem.defaults} ${attr.node.name.name} {${attr.node.value.value}}`;
195 } else if (attr.node.value.type === 'JSXExpressionContainer') {
196 // convert any Trans component to select option extracting any values and components
197 const children = attr.node.value.expression.children || [];
198 const thisTrans = processTrans(children, babel, componentStartIndex);
199
200 mem.defaults = `${mem.defaults} ${attr.node.name.name} {${thisTrans.defaults}}`;
201 mem.components = mem.components.concat(thisTrans.components);
202
203 componentStartIndex += thisTrans.components.length;
204 }
205 return mem;
206 },
207 { attributesToCopy: [], values: existingValues, components: [], defaults: '' },
208 );
209
210 // replace the node with the new Trans
211 parentPath.replaceWith(buildTransElement(extracted, extracted.attributesToCopy, t, true));
212}
213
214function transAsJSX(parentPath, { attributes, children }, babel, { filename }) {
215 const defaultsAttr = findAttribute('defaults', attributes);
216 const componentsAttr = findAttribute('components', attributes);
217 // if there is "defaults" attribute and no "components" attribute, parse defaults and extract from the parsed defaults instead of children
218 // if a "components" attribute has been provided, we assume they have already constructed a valid "defaults" and it does not need to be parsed
219 const parseDefaults = defaultsAttr && !componentsAttr;
220
221 let extracted;
222 if (parseDefaults) {
223 const defaultsExpression = defaultsAttr.node.value.value;
224 const parsed = babel.parse(`<>${defaultsExpression}</>`, {
225 presets: ['@babel/react'],
226 filename,
227 }).program.body[0].expression.children;
228
229 extracted = processTrans(parsed, babel);
230 } else {
231 extracted = processTrans(children, babel);
232 }
233
234 let clonedAttributes = cloneExistingAttributes(attributes);
235 if (parseDefaults) {
236 // remove existing defaults so it can be replaced later with the new parsed defaults
237 clonedAttributes = clonedAttributes.filter((node) => node.name.name !== 'defaults');
238 }
239
240 // replace the node with the new Trans
241 const replacePath = children.length ? children[0].parentPath : parentPath;
242 replacePath.replaceWith(
243 buildTransElement(extracted, clonedAttributes, babel.types, false, !!children.length),
244 );
245}
246
247function buildTransElement(
248 extracted,
249 finalAttributes,
250 t,
251 closeDefaults = false,
252 wasElementWithChildren = false,
253) {
254 const nodeName = t.jSXIdentifier('Trans');
255
256 // plural, select open { but do not close it while reduce
257 if (closeDefaults) extracted.defaults += '}';
258
259 // convert arrays into needed expressions
260 extracted.components = t.arrayExpression(extracted.components);
261 extracted.values = t.objectExpression(extracted.values);
262
263 // add generated Trans attributes
264 if (!attributeExistsAlready('defaults', finalAttributes))
265 if (extracted.defaults.includes(`"`)) {
266 // wrap defaults that contain double quotes in brackets
267 finalAttributes.push(
268 t.jSXAttribute(
269 t.jSXIdentifier('defaults'),
270 t.jSXExpressionContainer(t.StringLiteral(extracted.defaults)),
271 ),
272 );
273 } else {
274 finalAttributes.push(
275 t.jSXAttribute(t.jSXIdentifier('defaults'), t.StringLiteral(extracted.defaults)),
276 );
277 }
278
279 if (!attributeExistsAlready('components', finalAttributes))
280 finalAttributes.push(
281 t.jSXAttribute(t.jSXIdentifier('components'), t.jSXExpressionContainer(extracted.components)),
282 );
283 if (!attributeExistsAlready('values', finalAttributes))
284 finalAttributes.push(
285 t.jSXAttribute(t.jSXIdentifier('values'), t.jSXExpressionContainer(extracted.values)),
286 );
287
288 // create selfclosing Trans component
289 const openElement = t.jSXOpeningElement(nodeName, finalAttributes, true);
290 if (!wasElementWithChildren) return openElement;
291
292 return t.jSXElement(openElement, null, [], true);
293}
294
295function cloneExistingAttributes(attributes) {
296 return attributes.reduce((mem, attr) => {
297 mem.push(attr.node);
298 return mem;
299 }, []);
300}
301
302function findAttribute(name, attributes) {
303 return attributes.find((child) => {
304 const ele = child.node ? child.node : child;
305 return ele.name.name === name;
306 });
307}
308
309function attributeExistsAlready(name, attributes) {
310 return !!findAttribute(name, attributes);
311}
312
313function processTrans(children, babel, componentStartIndex = 0) {
314 const res = {};
315
316 res.defaults = mergeChildren(children, babel, componentStartIndex);
317 res.components = getComponents(children, babel);
318 res.values = getValues(children, babel);
319
320 return res;
321}
322
323const leadingNewLineAndWhitespace = /^\n\s+/g;
324const trailingNewLineAndWhitespace = /\n\s+$/g;
325function trimIndent(text) {
326 const newText = text
327 .replace(leadingNewLineAndWhitespace, '')
328 .replace(trailingNewLineAndWhitespace, '');
329 return newText;
330}
331
332/**
333 * add comma-delimited expressions like `{ val, number }`
334 */
335function mergeCommaExpressions(ele) {
336 if (ele.expression && ele.expression.expressions) {
337 return `{${ele.expression.expressions
338 .reduce((m, i) => {
339 m.push(i.name || i.value);
340 return m;
341 }, [])
342 .join(', ')}}`;
343 }
344 return '';
345}
346
347/**
348 * this is for supporting complex icu type interpolations
349 * date`${variable}` and number`{${varName}, ::percent}`
350 * also, plural`{${count}, one { ... } other { ... }}
351 */
352function mergeTaggedTemplateExpressions(ele, componentFoundIndex, t, babel) {
353 if (t.isTaggedTemplateExpression(ele.expression)) {
354 const [, text, index] = getTextAndInterpolatedVariables(
355 ele.expression.tag.name,
356 ele.expression,
357 componentFoundIndex,
358 babel,
359 );
360 return [text, index];
361 }
362 return ['', componentFoundIndex];
363}
364
365function mergeChildren(children, babel, componentStartIndex = 0) {
366 const t = babel.types;
367 let componentFoundIndex = componentStartIndex;
368
369 return children.reduce((mem, child) => {
370 const ele = child.node ? child.node : child;
371 let result = mem;
372
373 // add text, but trim indentation whitespace
374 if (t.isJSXText(ele) && ele.value) result += trimIndent(ele.value);
375 // add ?!? forgot
376 if (ele.expression && ele.expression.value) result += ele.expression.value;
377 // add `{ val }`
378 if (ele.expression && ele.expression.name) result += `{${ele.expression.name}}`;
379 // add `{ val, number }`
380 result += mergeCommaExpressions(ele);
381 const [nextText, newIndex] = mergeTaggedTemplateExpressions(ele, componentFoundIndex, t, babel);
382 result += nextText;
383 componentFoundIndex = newIndex;
384 // add <strong>...</strong> with replace to <0>inner string</0>
385 if (t.isJSXElement(ele)) {
386 result += `<${componentFoundIndex}>${mergeChildren(
387 ele.children,
388 babel,
389 )}</${componentFoundIndex}>`;
390 componentFoundIndex += 1;
391 }
392
393 return result;
394 }, '');
395}
396
397const extractTaggedTemplateValues = (ele, babel, toObjectProperty) => {
398 // date`${variable}` and so on
399 if (ele.expression && ele.expression.type === 'TaggedTemplateExpression') {
400 const [variables] = getTextAndInterpolatedVariables(
401 ele.expression.tag.name,
402 ele.expression,
403 0,
404 babel,
405 );
406 return variables.map((vari) => toObjectProperty(vari));
407 }
408 return [];
409};
410
411/**
412 * Extract the names of interpolated value as object properties to pass to Trans
413 */
414function getValues(children, babel) {
415 const t = babel.types;
416 const toObjectProperty = (name, value) =>
417 t.objectProperty(t.identifier(name), t.identifier(name), false, !value);
418
419 return children.reduce((mem, child) => {
420 const ele = child.node ? child.node : child;
421 let result = mem;
422
423 // add `{ var }` to values
424 if (ele.expression && ele.expression.name) mem.push(toObjectProperty(ele.expression.name));
425 // add `{ var, number }` to values
426 if (ele.expression && ele.expression.expressions)
427 result.push(
428 toObjectProperty(ele.expression.expressions[0].name || ele.expression.expressions[0].value),
429 );
430 // add `{ var: 'bar' }` to values
431 if (ele.expression && ele.expression.properties)
432 result = result.concat(ele.expression.properties);
433 // date`${variable}` and so on
434 result = result.concat(extractTaggedTemplateValues(ele, babel, toObjectProperty));
435 // recursive add inner elements stuff to values
436 if (t.isJSXElement(ele)) {
437 result = result.concat(getValues(ele.children, babel));
438 }
439
440 return result;
441 }, []);
442}
443
444/**
445 * Common logic for adding a child element of Trans to the list of components to hydrate the translation
446 * @param {JSXElement} jsxElement
447 * @param {JSXElement[]} mem
448 */
449const processJSXElement = (jsxElement, mem, t) => {
450 const clone = t.clone(jsxElement);
451 clone.children = clone.children.reduce((clonedMem, clonedChild) => {
452 const clonedEle = clonedChild.node ? clonedChild.node : clonedChild;
453
454 // clean out invalid definitions by replacing `{ catchDate, date, short }` with `{ catchDate }`
455 if (clonedEle.expression && clonedEle.expression.expressions)
456 clonedEle.expression.expressions = [clonedEle.expression.expressions[0]];
457
458 clonedMem.push(clonedChild);
459 return clonedMem;
460 }, []);
461
462 mem.push(jsxElement);
463};
464
465/**
466 * Extract the React components to pass to Trans as components
467 */
468function getComponents(children, babel) {
469 const t = babel.types;
470
471 return children.reduce((mem, child) => {
472 const ele = child.node ? child.node : child;
473
474 if (t.isJSXExpressionContainer(ele)) {
475 // check for date`` and so on
476 if (t.isTaggedTemplateExpression(ele.expression)) {
477 ele.expression.quasi.expressions.forEach((expr) => {
478 // check for sub-expressions. This can happen with plural`` or select`` or selectOrdinal``
479 // these can have nested components
480 if (t.isTaggedTemplateExpression(expr) && expr.quasi.expressions.length) {
481 mem.push(...getComponents(expr.quasi.expressions, babel));
482 }
483 if (!t.isJSXElement(expr)) {
484 // ignore anything that is not a component
485 return;
486 }
487 processJSXElement(expr, mem, t);
488 });
489 }
490 }
491 if (t.isJSXElement(ele)) {
492 processJSXElement(ele, mem, t);
493 }
494
495 return mem;
496 }, []);
497}
498
499const icuInterpolators = ['date', 'time', 'number', 'plural', 'select', 'selectOrdinal'];
500const importsToAdd = ['Trans'];
501
502/**
503 * helper split out of addNeededImports to make codeclimate happy
504 *
505 * This does the work of amending an existing import from "react-i18next", or
506 * creating a new one if it doesn't exist
507 */
508function addImports(state, existingImport, allImportsToAdd, t) {
509 // append imports to existing or add a new react-i18next import for the Trans and icu tagged template literals
510 if (existingImport) {
511 allImportsToAdd.forEach((name) => {
512 if (
513 existingImport.specifiers.findIndex(
514 (specifier) => specifier.imported && specifier.imported.name === name,
515 ) === -1
516 ) {
517 existingImport.specifiers.push(t.importSpecifier(t.identifier(name), t.identifier(name)));
518 }
519 });
520 } else {
521 state.file.path.node.body.unshift(
522 t.importDeclaration(
523 allImportsToAdd.map((name) => t.importSpecifier(t.identifier(name), t.identifier(name))),
524 t.stringLiteral('react-i18next'),
525 ),
526 );
527 }
528}
529
530/**
531 * Add `import { Trans, number, date, <etc.> } from "react-i18next"` as needed
532 */
533function addNeededImports(state, babel, references) {
534 const t = babel.types;
535
536 // check if there is an existing react-i18next import
537 const existingImport = state.file.path.node.body.find(
538 (importNode) =>
539 t.isImportDeclaration(importNode) && importNode.source.value === 'react-i18next',
540 );
541 // check for any of the tagged template literals that are used in the source, and add them
542 const usedRefs = Object.keys(references).filter((importName) => {
543 if (!icuInterpolators.includes(importName)) {
544 return false;
545 }
546 return references[importName].length;
547 });
548
549 // combine Trans + any tagged template literals
550 const allImportsToAdd = importsToAdd.concat(usedRefs);
551
552 addImports(state, existingImport, allImportsToAdd, t);
553}
554
555/**
556 * iterate over a node detected inside a tagged template literal
557 *
558 * This is a helper function for `extractVariableNamesFromQuasiNodes` defined below
559 *
560 * this is called using reduce as a way of tricking what would be `.map()`
561 * into passing in the parameters needed to both modify `componentFoundIndex`,
562 * `stringOutput`, and `interpolatedVariableNames`
563 * and to pass in the dependencies babel, and type. Type is the template type.
564 * For "date``" the type will be `date`. for "number``" the type is `number`, etc.
565 */
566const extractNestedTemplatesAndComponents = (
567 { componentFoundIndex: lastIndex, babel, stringOutput, type, interpolatedVariableNames },
568 node,
569) => {
570 let componentFoundIndex = lastIndex;
571 if (node.type === 'JSXElement') {
572 // perform the interpolation of components just as we do in a normal Trans setting
573 const subText = `<${componentFoundIndex}>${mergeChildren(
574 node.children,
575 babel,
576 )}</${componentFoundIndex}>`;
577 componentFoundIndex += 1;
578 stringOutput.push(subText);
579 } else if (node.type === 'TaggedTemplateExpression') {
580 // a nested date``/number``/plural`` etc., extract whatever is inside of it
581 const [variableNames, childText, newIndex] = getTextAndInterpolatedVariables(
582 node.tag.name,
583 node,
584 componentFoundIndex,
585 babel,
586 );
587 interpolatedVariableNames.push(...variableNames);
588 componentFoundIndex = newIndex;
589 stringOutput.push(childText);
590 } else if (node.type === 'Identifier') {
591 // turn date`${thing}` into `thing, date`
592 stringOutput.push(`${node.name}, ${type}`);
593 } else if (node.type === 'TemplateElement') {
594 // convert all whitespace into a single space for the text in the tagged template literal
595 stringOutput.push(node.value.cooked.replace(/\s+/g, ' '));
596 } else {
597 // unknown node type, ignore
598 }
599 return { componentFoundIndex, babel, stringOutput, type, interpolatedVariableNames };
600};
601
602/**
603 * filter the list of nodes within a tagged template literal to the 4 types we can process,
604 * and ignore anything else.
605 *
606 * this is a helper function for `extractVariableNamesFromQuasiNodes`
607 */
608const filterNodes = (node) => {
609 if (node.type === 'Identifier') {
610 // if the node has a name, keep it
611 return node.name;
612 }
613 if (node.type === 'JSXElement' || node.type === 'TaggedTemplateExpression') {
614 // always keep interpolated elements or other tagged template literals like a nested date`` inside a plural``
615 return true;
616 }
617 if (node.type === 'TemplateElement') {
618 // return the "cooked" (escaped) text for the text in the template literal (`, ::percent` in number`${varname}, ::percent`)
619 return node.value.cooked;
620 }
621 // unknown node type, ignore
622 return false;
623};
624
625const errorOnInvalidQuasiNodes = (primaryNode) => {
626 const noInterpolationError = !primaryNode.quasi.expressions.length;
627 const wrongOrderError = primaryNode.quasi.quasis[0].value.raw.length;
628 const message = `${primaryNode.tag.name} argument must be interpolated ${
629 noInterpolationError ? 'in' : 'at the beginning of'
630 } "${primaryNode.tag.name}\`\`" in "${primaryNode.loc.filename}" on line ${
631 primaryNode.loc.start.line
632 }`;
633 if (noInterpolationError || wrongOrderError) {
634 throw new Error(message);
635 }
636};
637
638const extractNodeVariableNames = (varNode, babel) => {
639 const interpolatedVariableNames = [];
640 if (varNode.type === 'JSXElement') {
641 // extract inner interpolated variables and add to the list
642 interpolatedVariableNames.push(
643 ...getValues(varNode.children, babel).map((value) => value.value.name),
644 );
645 } else if (varNode.type === 'Identifier') {
646 // the name of the interpolated variable
647 interpolatedVariableNames.push(varNode.name);
648 }
649 return interpolatedVariableNames;
650};
651
652const extractVariableNamesFromQuasiNodes = (primaryNode, babel) => {
653 errorOnInvalidQuasiNodes(primaryNode);
654 // this will contain all the nodes to convert to the ICU messageformat text
655 // at first they are unsorted, but will be ordered correctly at the end of the function
656 const text = [];
657 // the variable names. These are converted to object references as required for the Trans values
658 // in getValues() (toObjectProperty helper function)
659 const interpolatedVariableNames = [];
660 primaryNode.quasi.expressions.forEach((varNode) => {
661 if (
662 !babel.types.isIdentifier(varNode) &&
663 !babel.types.isTaggedTemplateExpression(varNode) &&
664 !babel.types.isJSXElement(varNode)
665 ) {
666 throw new Error(
667 `Must pass a variable, not an expression to "${primaryNode.tag.name}\`\`" in "${primaryNode.loc.filename}" on line ${primaryNode.loc.start.line}`,
668 );
669 }
670 text.push(varNode);
671 interpolatedVariableNames.push(...extractNodeVariableNames(varNode, babel));
672 });
673 primaryNode.quasi.quasis.forEach((quasiNode) => {
674 // these are the text surrounding the variable interpolation
675 // so in date`${varname}, short` it would be `''` and `, short`.
676 // (the empty string before `${varname}` and the stuff after it)
677 text.push(quasiNode);
678 });
679 return { text, interpolatedVariableNames };
680};
681
682const throwOnInvalidType = (type, primaryNode) => {
683 if (!icuInterpolators.includes(type)) {
684 throw new Error(
685 `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}`,
686 );
687 }
688};
689
690/**
691 * Retrieve the new text to use, and any interpolated variables
692 *
693 * This is used to process tagged template literals like date`${variable}` and number`${num}, ::percent`
694 *
695 * for the data example, it will return text of `{variable, date}` with a variable of `variable`
696 * for the number example, it will return text of `{num, number, ::percent}` with a variable of `num`
697 * @param {string} type the name of the tagged template (`date`, `number`, `plural`, etc. - any valid complex ICU type)
698 * @param {TaggedTemplateExpression} primaryNode the template expression node
699 * @param {int} index starting index number of components to be used for interpolations like <0>
700 * @param {*} babel
701 */
702function getTextAndInterpolatedVariables(type, primaryNode, index, babel) {
703 throwOnInvalidType(type, primaryNode);
704 const componentFoundIndex = index;
705 const { text, interpolatedVariableNames } = extractVariableNamesFromQuasiNodes(
706 primaryNode,
707 babel,
708 );
709 const { stringOutput, componentFoundIndex: newIndex } = text
710 .filter(filterNodes)
711 // sort by the order they appear in the source code
712 .sort((a, b) => {
713 if (a.start > b.start) return 1;
714 return -1;
715 })
716 .reduce(extractNestedTemplatesAndComponents, {
717 babel,
718 componentFoundIndex,
719 stringOutput: [],
720 type,
721 interpolatedVariableNames,
722 });
723 return [
724 interpolatedVariableNames,
725 `{${stringOutput.join('')}}`,
726 // return the new component interpolation index
727 newIndex,
728 ];
729}