找回密码
 立即注册
首页 业界区 业界 基于AST实现国际化文本提取

基于AST实现国际化文本提取

损注 3 天前
我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。
本文作者:霜序
前言

在阅读本文之前,需要读者有一些 babel 的基础知识,babel 的架构图如下:
1.png

确定中文范围

先需要明确项目中可能存在中文的情况有哪些?
  1. const a = '霜序';
  2. const b = `霜序`;
  3. const c = `${isBoolean} ? "霜序" : "FBB"`;
  4. const obj = { a: '霜序' };
  5. // enum Status {
  6. //     Todo = "未完成",
  7. //     Complete = "完成"
  8. // }
  9. // enum Status {
  10. //     "未完成",
  11. //     "完成"
  12. // }
  13. const dom = 霜序;
  14. const dom1 = <Customer name="霜序" />;
复制代码
虽然有很多情况下会出现中文,在代码中存在的时候大部分是string或者模版字符串,在react中的时候一个是dom的子节点还是一种是props上的属性包含中文。
  1. // const a = '霜序';
  2. {
  3.   "type": "StringLiteral",
  4.   "start": 10,
  5.   "end": 14,
  6.   "extra": {
  7.     "rawValue": "霜序",
  8.     "raw": "'霜序'"
  9.   },
  10.   "value": "霜序"
  11. }
复制代码
StringLiteral

对应的AST节点为StringLiteral,需要去遍历所有的StringLiteral节点,将当前的节点替换为我们需要的I18N.key这种节点。
  1. // const b = `${finalRoles}(质量项目:${projects})`
  2. {
  3.   "type": "TemplateLiteral",
  4.   "start": 10,
  5.   "end": 43,
  6.   "expressions": [
  7.     {
  8.       "type": "Identifier",
  9.       "start": 13,
  10.       "end": 23,
  11.       "name": "finalRoles"
  12.     },
  13.     {
  14.       "type": "Identifier",
  15.       "start": 32,
  16.       "end": 40,
  17.       "name": "projects"
  18.     }
  19.   ],
  20.   "quasis": [
  21.     {
  22.       "type": "TemplateElement",
  23.       "start": 11,
  24.       "end": 11,
  25.       "value": {
  26.         "raw": "",
  27.         "cooked": ""
  28.       }
  29.     },
  30.     {
  31.       "type": "TemplateElement",
  32.       "start": 24,
  33.       "end": 30,
  34.       "value": {
  35.         "raw": "(质量项目:",
  36.         "cooked": "(质量项目:"
  37.       }
  38.     },
  39.     {
  40.       "type": "TemplateElement",
  41.       "start": 41,
  42.       "end": 42,
  43.       "value": {
  44.         "raw": ")",
  45.         "cooked": ")"
  46.       }
  47.     }
  48.   ]
  49. }
复制代码
TemplateLiteral

相对于字符串情况会复杂一些,TemplateLiteral中会出现变量的情况,能够看到在TemplateLiteral节点中存在expressions和quasis两个字段分别表示变量和字符串
其实可以发现对于字符串来说全部都在TemplateElement节点上,那么是否可以直接遍历所有的TemplateElement节点,和StringLiteral一样。
直接遍历TemplateElement的时候,处理之后的效果如下:
  1. const b = `${finalRoles}(质量项目:${projects})`;
  2. const b = `${finalRoles}${I18N.K}${projects})`;
  3. // I18N.K = "(质量项目:"
复制代码
那么这种只提取中文不管变量的情况,会导致翻译不到的问题,上下文很缺失。
最后我们会处理成{val1}(质量项目:{val2})的情况,将对应val1和val2传入
  1. I18N.get(I18N.K, {
  2.   val1: finalRoles,
  3.   val2: projects,
  4. });
复制代码
JSXText

对应的AST节点为JSXText,去遍历JSXElement节点,在遍历对应的children中的JSXText处理中文文本
  1. {
  2.   "type": "JSXElement",
  3.   "start": 12,
  4.   "end": 25,
  5.   "children": [
  6.     {
  7.       "type": "JSXText",
  8.       "start": 17,
  9.       "end": 19,
  10.       "extra": {
  11.         "rawValue": "霜序",
  12.         "raw": "霜序"
  13.       },
  14.       "value": "霜序"
  15.     }
  16.   ]
  17. }
复制代码
JSXAttribute

对应的AST节点为JSXAttribute,中文存在的节点还是StringLiteral,但是在处理的时候还是特殊处理JSXAttribute中的StringLiteral,因为对于这种JSX中的数据来说我们需要包裹{},不是直接做文本替换的
  1. {
  2.   "type": "JSXOpeningElement",
  3.   "start": 13,
  4.   "end": 35,
  5.   "name": {
  6.     "type": "JSXIdentifier",
  7.     "start": 14,
  8.     "end": 22,
  9.     "name": "Customer"
  10.   },
  11.   "attributes": [
  12.     {
  13.       "type": "JSXAttribute",
  14.       "start": 23,
  15.       "end": 32,
  16.       "name": {
  17.         "type": "JSXIdentifier",
  18.         "start": 23,
  19.         "end": 27,
  20.         "name": "name"
  21.       },
  22.       "value": {
  23.         "type": "StringLiteral",
  24.         "start": 28,
  25.         "end": 32,
  26.         "extra": {
  27.           "rawValue": "霜序",
  28.           "raw": ""霜序""
  29.         },
  30.         "value": "霜序"
  31.       }
  32.     }
  33.   ],
  34.   "selfClosing": true
  35. }
复制代码
使用 Babel 处理

2.png

使用 @babel/parser 将源代码转译为 AST
  1. const plugins: ParserOptions['plugins'] = ['decorators-legacy', 'typescript'];
  2. if (fileName.endsWith('text') || fileName.endsWith('text')) {
  3.   plugins.push('text');
  4. }
  5. const ast = parse(sourceCode, {
  6.   sourceType: 'module',
  7.   plugins,
  8. });
复制代码
@babel/traverse 特殊处理上述的节点,转化 AST
  1. babelTraverse(ast, {
  2.   StringLiteral(path) {
  3.     const { node } = path;
  4.     const { value } = node;
  5.     if (
  6.       !value.match(DOUBLE_BYTE_REGEX) ||
  7.       (path.parentPath.node.type === 'CallExpression' &&
  8.         path.parentPath.toString().includes('console'))
  9.     ) {
  10.       return;
  11.     }
  12.     path.replaceWithMultiple(template.ast(`I18N.${key}`));
  13.   },
  14.   TemplateLiteral(path) {
  15.     const { node } = path;
  16.     const { start, end } = node;
  17.     if (!start || !end) return;
  18.     let templateContent = sourceCode.slice(start + 1, end - 1);
  19.     if (
  20.       !templateContent.match(DOUBLE_BYTE_REGEX) ||
  21.       (path.parentPath.node.type === 'CallExpression' &&
  22.         path.parentPath.toString().includes('console')) ||
  23.       path.parentPath.node.type === 'TaggedTemplateExpression'
  24.     ) {
  25.       return;
  26.     }
  27.     if (!node.expressions.length) {
  28.       path.replaceWithMultiple(template.ast(`I18N.${key}`));
  29.       path.skip();
  30.       return;
  31.     }
  32.     const expressions = node.expressions.map((expression) => {
  33.       const { start, end } = expression;
  34.       if (!start || !end) return;
  35.       return sourceCode.slice(start, end);
  36.     });
  37.     const kvPair = expressions.map((expression, index) => {
  38.       templateContent = templateContent.replace(
  39.         `\${${expression}}`,
  40.         `{val${index + 1}}`,
  41.       );
  42.       return `val${index + 1}: ${expression}`;
  43.     });
  44.     path.replaceWithMultiple(
  45.       template.ast(`I18N.get(I18N.${key},{${kvPair.join(',\n')}})`),
  46.     );
  47.   },
  48.   JSXElement(path) {
  49.     const children = path.node.children;
  50.     const newChild = children.map((child) => {
  51.       if (babelTypes.isJSXText(child)) {
  52.         const { value } = child;
  53.         if (value.match(DOUBLE_BYTE_REGEX)) {
  54.           const newExpression = babelTypes.jsxExpressionContainer(
  55.             babelTypes.identifier(`I18N.${key}`),
  56.           );
  57.           return newExpression;
  58.         }
  59.       }
  60.       return child;
  61.     });
  62.     path.node.children = newChild;
  63.   },
  64.   JSXAttribute(path) {
  65.     const { node } = path;
  66.     if (
  67.       babelTypes.isStringLiteral(node.value) &&
  68.       node.value.value.match(DOUBLE_BYTE_REGEX)
  69.     ) {
  70.       const expression = babelTypes.jsxExpressionContainer(
  71.         babelTypes.memberExpression(
  72.           babelTypes.identifier('I18N'),
  73.           babelTypes.identifier(`${key}`),
  74.         ),
  75.       );
  76.       node.value = expression;
  77.     }
  78.   },
  79. });
复制代码
对于TemplateLiteral来说需要处理expression,通过截取的方式获取到对应的模版字符串 templateContent,如果不存在expressions,直接类似StringLiteral处理;存在expressions的情况下,遍历expressions通过${val(index)}替换掉templateContent中的expression,最后使用I18N.get的方式获取对应的值
  1. const name = `${a}霜序`;
  2. // const name = I18N.get(I18N.test.A, { val1: a });
  3. const name1 = `${a ? '霜' : '序'}霜序`;
  4. // const name1 = I18N.get(I18N.test.B, { val1: a ? I18N.test.C : I18N.test.D });
复制代码
对于TemplateLiteral节点来说,如果是嵌套的情况,会出现问题。
  1. const name1 = `${a ? `霜` : `序`}霜序`;
  2. // const name1 = I18N.get(I18N.test.B, { val1: a ? `霜` : `序` });
复制代码
<blockquote>

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册