Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 

401 wiersze
14 KiB

  1. const { createMacro } = require('babel-plugin-macros');
  2. // copy to:
  3. // https://astexplorer.net/#/gist/642aebbb9e449e959f4ad8907b4adf3a/4a65742e2a3e926eb55eaa3d657d1472b9ac7970
  4. module.exports = createMacro(ICUMacro);
  5. function ICUMacro({ references, state, babel }) {
  6. const t = babel.types;
  7. const { Trans = [], Plural = [], Select = [], SelectOrdinal = [] } = references;
  8. // assert we have the react-i18next Trans component imported
  9. addNeededImports(state, babel);
  10. // transform Plural and SelectOrdinal
  11. [...Plural, ...SelectOrdinal].forEach(referencePath => {
  12. if (referencePath.parentPath.type === 'JSXOpeningElement') {
  13. pluralAsJSX(
  14. referencePath.parentPath,
  15. {
  16. attributes: referencePath.parentPath.get('attributes'),
  17. children: referencePath.parentPath.parentPath.get('children'),
  18. },
  19. babel,
  20. );
  21. } else {
  22. // throw a helpful error message or something :)
  23. }
  24. });
  25. // transform Select
  26. Select.forEach(referencePath => {
  27. if (referencePath.parentPath.type === 'JSXOpeningElement') {
  28. selectAsJSX(
  29. referencePath.parentPath,
  30. {
  31. attributes: referencePath.parentPath.get('attributes'),
  32. children: referencePath.parentPath.parentPath.get('children'),
  33. },
  34. babel,
  35. );
  36. } else {
  37. // throw a helpful error message or something :)
  38. }
  39. });
  40. // transform Trans
  41. Trans.forEach(referencePath => {
  42. if (referencePath.parentPath.type === 'JSXOpeningElement') {
  43. transAsJSX(
  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. function pluralAsJSX(parentPath, { attributes }, babel) {
  57. const t = babel.types;
  58. const toObjectProperty = (name, value) =>
  59. t.objectProperty(t.identifier(name), t.identifier(name), false, !value);
  60. // plural or selectordinal
  61. const nodeName = parentPath.node.name.name.toLocaleLowerCase();
  62. // will need to merge count attribute with existing values attribute in some cases
  63. const existingValuesAttribute = findAttribute('values', attributes);
  64. const existingValues = existingValuesAttribute
  65. ? existingValuesAttribute.node.value.expression.properties
  66. : [];
  67. let componentStartIndex = 0;
  68. const extracted = attributes.reduce(
  69. (mem, attr) => {
  70. if (attr.node.name.name === 'i18nKey') {
  71. // copy the i18nKey
  72. mem.attributesToCopy.push(attr.node);
  73. } else if (attr.node.name.name === 'count') {
  74. // take the count for element
  75. let exprName = attr.node.value.expression.name;
  76. if (!exprName) {
  77. exprName = 'count';
  78. }
  79. if (exprName === 'count') {
  80. // if the prop expression name is also "count", copy it instead: <Plural count={count} --> <Trans count={count}
  81. mem.attributesToCopy.push(attr.node);
  82. } else {
  83. mem.values.unshift(toObjectProperty(exprName));
  84. }
  85. mem.defaults = `{${exprName}, ${nodeName}, ${mem.defaults}`;
  86. } else if (attr.node.name.name === 'values') {
  87. // skip the values attribute, as it has already been processed into mem from existingValues
  88. } else if (attr.node.value.type === 'StringLiteral') {
  89. // take any string node as plural option
  90. let pluralForm = attr.node.name.name;
  91. if (pluralForm.indexOf('$') === 0) pluralForm = pluralForm.replace('$', '=');
  92. mem.defaults = `${mem.defaults} ${pluralForm} {${attr.node.value.value}}`;
  93. } else if (attr.node.value.type === 'JSXExpressionContainer') {
  94. // convert any Trans component to plural option extracting any values and components
  95. const children = attr.node.value.expression.children || [];
  96. const thisTrans = processTrans(children, babel, componentStartIndex);
  97. let pluralForm = attr.node.name.name;
  98. if (pluralForm.indexOf('$') === 0) pluralForm = pluralForm.replace('$', '=');
  99. mem.defaults = `${mem.defaults} ${pluralForm} {${thisTrans.defaults}}`;
  100. mem.components = mem.components.concat(thisTrans.components);
  101. componentStartIndex += thisTrans.components.length;
  102. }
  103. return mem;
  104. },
  105. { attributesToCopy: [], values: existingValues, components: [], defaults: '' },
  106. );
  107. // replace the node with the new Trans
  108. parentPath.replaceWith(buildTransElement(extracted, extracted.attributesToCopy, t, true));
  109. }
  110. function selectAsJSX(parentPath, { attributes }, babel) {
  111. const t = babel.types;
  112. const toObjectProperty = (name, value) =>
  113. t.objectProperty(t.identifier(name), t.identifier(name), false, !value);
  114. // will need to merge switch attribute with existing values attribute
  115. const existingValuesAttribute = findAttribute('values', attributes);
  116. const existingValues = existingValuesAttribute
  117. ? existingValuesAttribute.node.value.expression.properties
  118. : [];
  119. let componentStartIndex = 0;
  120. const extracted = attributes.reduce(
  121. (mem, attr) => {
  122. if (attr.node.name.name === 'i18nKey') {
  123. // copy the i18nKey
  124. mem.attributesToCopy.push(attr.node);
  125. } else if (attr.node.name.name === 'switch') {
  126. // take the switch for select element
  127. let exprName = attr.node.value.expression.name;
  128. if (!exprName) {
  129. exprName = 'selectKey';
  130. mem.values.unshift(t.objectProperty(t.identifier(exprName), attr.node.value.expression));
  131. } else {
  132. mem.values.unshift(toObjectProperty(exprName));
  133. }
  134. mem.defaults = `{${exprName}, select, ${mem.defaults}`;
  135. } else if (attr.node.name.name === 'values') {
  136. // skip the values attribute, as it has already been processed into mem as existingValues
  137. } else if (attr.node.value.type === 'StringLiteral') {
  138. // take any string node as select option
  139. mem.defaults = `${mem.defaults} ${attr.node.name.name} {${attr.node.value.value}}`;
  140. } else if (attr.node.value.type === 'JSXExpressionContainer') {
  141. // convert any Trans component to select option extracting any values and components
  142. const children = attr.node.value.expression.children || [];
  143. const thisTrans = processTrans(children, babel, componentStartIndex);
  144. mem.defaults = `${mem.defaults} ${attr.node.name.name} {${thisTrans.defaults}}`;
  145. mem.components = mem.components.concat(thisTrans.components);
  146. componentStartIndex += thisTrans.components.length;
  147. }
  148. return mem;
  149. },
  150. { attributesToCopy: [], values: existingValues, components: [], defaults: '' },
  151. );
  152. // replace the node with the new Trans
  153. parentPath.replaceWith(buildTransElement(extracted, extracted.attributesToCopy, t, true));
  154. }
  155. function transAsJSX(parentPath, { attributes, children }, babel) {
  156. const defaultsAttr = findAttribute('defaults', attributes);
  157. const componentsAttr = findAttribute('components', attributes);
  158. // if there is "defaults" attribute and no "components" attribute, parse defaults and extract from the parsed defaults instead of children
  159. // if a "components" attribute has been provided, we assume they have already constructed a valid "defaults" and it does not need to be parsed
  160. const parseDefaults = defaultsAttr && !componentsAttr;
  161. let extracted;
  162. if (parseDefaults) {
  163. const defaultsExpression = defaultsAttr.node.value.value;
  164. const parsed = babel.parse(`<>${defaultsExpression}</>`, {
  165. presets: ['@babel/react'],
  166. }).program.body[0].expression.children;
  167. extracted = processTrans(parsed, babel);
  168. } else {
  169. extracted = processTrans(children, babel);
  170. }
  171. let clonedAttributes = cloneExistingAttributes(attributes);
  172. if (parseDefaults) {
  173. // remove existing defaults so it can be replaced later with the new parsed defaults
  174. clonedAttributes = clonedAttributes.filter(node => node.name.name !== 'defaults');
  175. }
  176. // replace the node with the new Trans
  177. const replacePath = children.length ? children[0].parentPath : parentPath;
  178. replacePath.replaceWith(
  179. buildTransElement(extracted, clonedAttributes, babel.types, false, !!children.length),
  180. );
  181. }
  182. function buildTransElement(
  183. extracted,
  184. finalAttributes,
  185. t,
  186. closeDefaults = false,
  187. wasElementWithChildren = false,
  188. ) {
  189. const nodeName = t.jSXIdentifier('Trans');
  190. // plural, select open { but do not close it while reduce
  191. if (closeDefaults) extracted.defaults += '}';
  192. // convert arrays into needed expressions
  193. extracted.components = t.arrayExpression(extracted.components);
  194. extracted.values = t.objectExpression(extracted.values);
  195. // add generated Trans attributes
  196. if (!attributeExistsAlready('defaults', finalAttributes))
  197. finalAttributes.push(
  198. t.jSXAttribute(t.jSXIdentifier('defaults'), t.StringLiteral(extracted.defaults)),
  199. );
  200. if (!attributeExistsAlready('components', finalAttributes))
  201. finalAttributes.push(
  202. t.jSXAttribute(t.jSXIdentifier('components'), t.jSXExpressionContainer(extracted.components)),
  203. );
  204. if (!attributeExistsAlready('values', finalAttributes))
  205. finalAttributes.push(
  206. t.jSXAttribute(t.jSXIdentifier('values'), t.jSXExpressionContainer(extracted.values)),
  207. );
  208. // create selfclosing Trans component
  209. const openElement = t.jSXOpeningElement(nodeName, finalAttributes, true);
  210. if (!wasElementWithChildren) return openElement;
  211. return t.jSXElement(openElement, null, [], true);
  212. }
  213. function cloneExistingAttributes(attributes) {
  214. return attributes.reduce((mem, attr) => {
  215. mem.push(attr.node);
  216. return mem;
  217. }, []);
  218. }
  219. function findAttribute(name, attributes) {
  220. return attributes.find(child => {
  221. const ele = child.node ? child.node : child;
  222. return ele.name.name === name;
  223. });
  224. }
  225. function attributeExistsAlready(name, attributes) {
  226. return !!findAttribute(name, attributes);
  227. }
  228. function processTrans(children, babel, componentStartIndex = 0) {
  229. const res = {};
  230. res.defaults = mergeChildren(children, babel, componentStartIndex);
  231. res.components = getComponents(children, babel);
  232. res.values = getValues(children, babel);
  233. return res;
  234. }
  235. // eslint-disable-next-line no-control-regex
  236. const leadingNewLineAndWhitespace = new RegExp('^\n\\s+', 'g');
  237. // eslint-disable-next-line no-control-regex
  238. const trailingNewLineAndWhitespace = new RegExp('\n\\s+$', 'g');
  239. function trimIndent(text) {
  240. const newText = text
  241. .replace(leadingNewLineAndWhitespace, '')
  242. .replace(trailingNewLineAndWhitespace, '');
  243. return newText;
  244. }
  245. function mergeChildren(children, babel, componentStartIndex = 0) {
  246. const t = babel.types;
  247. let componentFoundIndex = componentStartIndex;
  248. return children.reduce((mem, child) => {
  249. const ele = child.node ? child.node : child;
  250. // add text, but trim indentation whitespace
  251. if (t.isJSXText(ele) && ele.value) mem += trimIndent(ele.value);
  252. // add ?!? forgot
  253. if (ele.expression && ele.expression.value) mem += ele.expression.value;
  254. // add `{ val }`
  255. if (ele.expression && ele.expression.name) mem += `{${ele.expression.name}}`;
  256. // add `{ val, number }`
  257. if (ele.expression && ele.expression.expressions) {
  258. mem += `{${ele.expression.expressions
  259. .reduce((m, i) => {
  260. m.push(i.name || i.value);
  261. return m;
  262. }, [])
  263. .join(', ')}}`;
  264. }
  265. // add <strong>...</strong> with replace to <0>inner string</0>
  266. if (t.isJSXElement(ele)) {
  267. mem += `<${componentFoundIndex}>${mergeChildren(
  268. ele.children,
  269. babel,
  270. )}</${componentFoundIndex}>`;
  271. componentFoundIndex++;
  272. }
  273. return mem;
  274. }, '');
  275. }
  276. function getValues(children, babel) {
  277. const t = babel.types;
  278. const toObjectProperty = (name, value) =>
  279. t.objectProperty(t.identifier(name), t.identifier(name), false, !value);
  280. return children.reduce((mem, child) => {
  281. const ele = child.node ? child.node : child;
  282. // add `{ var }` to values
  283. if (ele.expression && ele.expression.name) mem.push(toObjectProperty(ele.expression.name));
  284. // add `{ var, number }` to values
  285. if (ele.expression && ele.expression.expressions)
  286. mem.push(
  287. toObjectProperty(ele.expression.expressions[0].name || ele.expression.expressions[0].value),
  288. );
  289. // add `{ var: 'bar' }` to values
  290. if (ele.expression && ele.expression.properties) mem = mem.concat(ele.expression.properties);
  291. // recursive add inner elements stuff to values
  292. if (t.isJSXElement(ele)) {
  293. mem = mem.concat(getValues(ele.children, babel));
  294. }
  295. return mem;
  296. }, []);
  297. }
  298. function getComponents(children, babel) {
  299. const t = babel.types;
  300. return children.reduce((mem, child) => {
  301. const ele = child.node ? child.node : child;
  302. if (t.isJSXElement(ele)) {
  303. const clone = t.clone(ele);
  304. clone.children = clone.children.reduce((clonedMem, clonedChild) => {
  305. const clonedEle = clonedChild.node ? clonedChild.node : clonedChild;
  306. // clean out invalid definitions by replacing `{ catchDate, date, short }` with `{ catchDate }`
  307. if (clonedEle.expression && clonedEle.expression.expressions)
  308. clonedEle.expression.expressions = [clonedEle.expression.expressions[0]];
  309. clonedMem.push(clonedChild);
  310. return clonedMem;
  311. }, []);
  312. mem.push(ele);
  313. }
  314. return mem;
  315. }, []);
  316. }
  317. function addNeededImports(state, babel) {
  318. const t = babel.types;
  319. const importsToAdd = ['Trans'];
  320. // check if there is an existing react-i18next import
  321. const existingImport = state.file.path.node.body.find(
  322. importNode => t.isImportDeclaration(importNode) && importNode.source.value === 'react-i18next',
  323. );
  324. // append Trans to existing or add a new react-i18next import for the Trans
  325. if (existingImport) {
  326. importsToAdd.forEach(name => {
  327. if (
  328. existingImport.specifiers.findIndex(
  329. specifier => specifier.imported && specifier.imported.name === name,
  330. ) === -1
  331. ) {
  332. existingImport.specifiers.push(t.importSpecifier(t.identifier(name), t.identifier(name)));
  333. }
  334. });
  335. } else {
  336. state.file.path.node.body.unshift(
  337. t.importDeclaration(
  338. importsToAdd.map(name => t.importSpecifier(t.identifier(name), t.identifier(name))),
  339. t.stringLiteral('react-i18next'),
  340. ),
  341. );
  342. }
  343. }