软件教程 2025年08月6日
0 收藏 0 点赞 815 浏览 7294 个字
摘要 :

文章目录 一、确定项目中的中文范围 二、利用Babel进行文本提取处理 三、其他相关处理 (一)动态生成key (二)脚手架命令 今天,我们来深入探讨如何基于抽象语法树……




  • 一、确定项目中的中文范围
  • 二、利用Babel进行文本提取处理
  • 三、其他相关处理
    • (一)动态生成key
    • (二)脚手架命令

    今天,我们来深入探讨如何基于抽象语法树(AST)实现国际化文本提取,在阅读前,建议读者先对babel有一定的了解,其架构主要涵盖工具层(像@babeltypes、@babeltemplate等 )、核心部分、生态系统等,其中语法解析器支持多种语法,如ESNext、Typescript、JSX等。基于AST实现国际化文本提取:原理、实践与工具详解

    一、确定项目中的中文范围

    在开始提取国际化文本前,首先要明确项目里可能出现中文的场景。在JavaScript代码中,常见的情况有普通字符串定义、模版字符串使用、在React组件的DOM子节点以及props属性中。例如:

    const a = \'霜序\';
    const b = `霜序`;
    const c = `${isBoolean} ? \"霜序\" : \"FBB\"`;
    const obj = { a: \'霜序\' };
    // enum Status {
    //     Todo = \"未完成\",
    //     Complete = \"完成\"
    // }
    // enum Status {
    //     \"未完成\",
    //     \"完成\"
    // }
    const dom = <div>霜序</div>;
    const dom1 = <Customer name=\"霜序\" />;
    

    从AST的角度来看,不同的中文存在形式对应不同的节点类型。

    • StringLiteral节点:普通字符串在AST中对应的节点为StringLiteral。当我们要进行国际化文本提取时,需要遍历所有的StringLiteral节点,然后将其替换为I18N.key这样的节点形式,以便后续进行国际化处理。例如,对于const a = \'霜序\';,在AST中会表示为:
    {
      \"type\": \"StringLiteral\",
      \"start\": 10,
      \"end\": 14,
      \"extra\": {
        \"rawValue\": \"霜序\",
        \"raw\": \"\'霜序\'\"
      },
      \"value\": \"霜序\"
    }
    
    • TemplateLiteral节点:模版字符串对应的AST节点是TemplateLiteral,它的情况相对复杂一些,因为其中可能包含变量。在TemplateLiteral节点中,expressions字段表示变量,quasis字段表示字符串部分。比如const b = ${finalRoles}(质量项目:${projects});,其AST表示如下:
    {
      \"type\": \"TemplateLiteral\",
      \"start\": 10,
      \"end\": 43,
      \"expressions\": [
        {
          \"type\": \"Identifier\",
          \"start\": 13,
          \"end\": 23,
          \"name\": \"finalRoles\"
        },
        {
          \"type\": \"Identifier\",
          \"start\": 32,
          \"end\": 40,
          \"name\": \"projects\"
        }
      ],
      \"quasis\": [
        {
          \"type\": \"TemplateElement\",
          \"start\": 11,
          \"end\": 11,
          \"value\": {
            \"raw\": \"\",
            \"cooked\": \"\"
          }
        },
        {
          \"type\": \"TemplateElement\",
          \"start\": 24,
          \"end\": 30,
          \"value\": {
            \"raw\": \"(质量项目:\",
            \"cooked\": \"(质量项目:\"
          }
        },
        {
          \"type\": \"TemplateElement\",
          \"start\": 41,
          \"end\": 42,
          \"value\": {
            \"raw\": \")\",
            \"cooked\": \")\"
          }
        }
      ]
    }
    

    如果直接遍历TemplateElement节点,只提取中文而不管变量,会导致翻译时上下文缺失,出现翻译不准确的问题。理想的处理方式是将其处理成{val1}(质量项目:{val2})这种形式,并把对应的val1val2传入,例如:

    I18N.get(I18N.K, {
      val1: finalRoles,
      val2: projects,
    });
    
    • JSXText节点:在React的JSX中,文本内容对应的AST节点为JSXText。我们需要遍历JSXElement节点,然后在其children中找到JSXText节点来处理中文文本。比如:
    {
      \"type\": \"JSXElement\",
      \"start\": 12,
      \"end\": 25,
      \"children\": [
        {
          \"type\": \"JSXText\",
          \"start\": 17,
          \"end\": 19,
          \"extra\": {
            \"rawValue\": \"霜序\",
            \"raw\": \"霜序\"
          },
          \"value\": \"霜序\"
        }
      ]
    }
    
    • JSXAttribute节点:当中文出现在JSX的属性中时,对应的AST节点是JSXAttribute,而中文实际存在的节点还是StringLiteral。不过在处理时需要特殊对待,因为对于JSX中的数据,我们需要用{}包裹,而不是直接进行文本替换。示例如下:
    {
      \"type\": \"JSXOpeningElement\",
      \"start\": 13,
      \"end\": 35,
      \"name\": {
        \"type\": \"JSXIdentifier\",
        \"start\": 14,
        \"end\": 22,
        \"name\": \"Customer\"
      },
      \"attributes\": [
        {
          \"type\": \"JSXAttribute\",
          \"start\": 23,
          \"end\": 32,
          \"name\": {
            \"type\": \"JSXIdentifier\",
            \"start\": 23,
            \"end\": 27,
            \"name\": \"name\"
          },
          \"value\": {
            \"type\": \"StringLiteral\",
            \"start\": 28,
            \"end\": 32,
            \"extra\": {
              \"rawValue\": \"霜序\",
              \"raw\": \"\\\"霜序\\\"\"
            },
            \"value\": \"霜序\"
          }
        }
      ],
      \"selfClosing\": true
    }
    

    二、利用Babel进行文本提取处理

    明确了中文在AST中的节点类型后,接下来就可以借助Babel工具来实现国际化文本的提取。Babel主要涉及@babel/parser@babel/traverse@babel/generate等模块。

    • 使用@babel/parser转译源代码为AST:利用@babel/parser可以将源代码解析成AST,代码如下:
    const plugins: ParserOptions[\'plugins\'] = [\'decorators-legacy\', \'typescript\'];
    if (fileName.endsWith(\'text\') || fileName.endsWith(\'text\')) {
      plugins.push(\'text\');
    }
    const ast = parse(sourceCode, {
      sourceType: \'module\',
      plugins,
    });
    

    在这段代码中,根据文件类型选择合适的插件,然后将源代码解析为AST,为后续处理做准备。

    • 使用@babel/traverse处理AST节点@babel/traverse用于遍历和修改AST节点。针对前面提到的不同节点类型,我们进行如下处理:
    babelTraverse(ast, {
      StringLiteral(path) {
        const { node } = path;
        const { value } = node;
        if (
          !value.match(DOUBLE_BYTE_REGEX) ||
          (path.parentPath.node.type === \'CallExpression\' &&
            path.parentPath.toString().includes(\'console\'))
        ) {
          return;
        }
        path.replaceWithMultiple(template.ast(`I18N.${key}`));
      },
      TemplateLiteral(path) {
        const { node } = path;
        const { start, end } = node;
        if (!start ||!end) return;
        let templateContent = sourceCode.slice(start + 1, end - 1);
        if (
          !templateContent.match(DOUBLE_BYTE_REGEX) ||
          (path.parentPath.node.type === \'CallExpression\' &&
            path.parentPath.toString().includes(\'console\')) ||
          path.parentPath.node.type === \'TaggedTemplateExpression\'
        ) {
          return;
        }
        if (!node.expressions.length) {
          path.replaceWithMultiple(template.ast(`I18N.${key}`));
          path.skip();
          return;
        }
        const expressions = node.expressions.map((expression) => {
          const { start, end } = expression;
          if (!start ||!end) return;
          return sourceCode.slice(start, end);
        });
        const kvPair = expressions.map((expression, index) => {
          templateContent = templateContent.replace(
            `${${expression}}`,
            `{val${index + 1}}`,
          );
          return `val${index + 1}: ${expression}`;
        });
        path.replaceWithMultiple(
          template.ast(`I18N.get(I18N.${key},{${kvPair.join(\',\\n\')}})`),
        );
      },
      JSXElement(path) {
        const children = path.node.children;
        const newChild = children.map((child) => {
          if (babelTypes.isJSXText(child)) {
            const { value } = child;
            if (value.match(DOUBLE_BYTE_REGEX)) {
              const newExpression = babelTypes.jsxExpressionContainer(
                babelTypes.identifier(`I18N.${key}`),
              );
              return newExpression;
            }
          }
          return child;
        });
        path.node.children = newChild;
      },
      JSXAttribute(path) {
        const { node } = path;
        if (
          babelTypes.isStringLiteral(node.value) &&
          node.value.value.match(DOUBLE_BYTE_REGEX)
        ) {
          const expression = babelTypes.jsxExpressionContainer(
            babelTypes.memberExpression(
              babelTypes.identifier(\'I18N\'),
              babelTypes.identifier(`${key}`),
            ),
          );
          node.value = expression;
        }
      },
    });
    

    在处理TemplateLiteral节点时,如果存在变量,需要通过截取的方式获取模版字符串templateContent ,然后遍历expressions,用{val(index)}替换掉templateContent中的变量,最后使用I18N.get的方式来获取对应的值。不过,TemplateLiteral节点如果存在嵌套情况,会出现处理问题,这是因为babel不会自动递归处理其嵌套模板。

    • 在AST顶部插入引入语句:处理完AST节点后,我们需要统一引入I18N变量。在文件的AST顶部的import语句后插入相关的importStatement,代码如下:
    Program: {
        exit(path) {
            const importStatement = projectConfig.importStatement;
            const result = importStatement
                .replace(/^import\\s+|\\s+from\\s+/g, \',\')
                .split(\',\')
                .filter(Boolean);
            // 判断当前的文件中是否存在importStatement语句
            const existingImport = path.node.body.find((node) => {
                return (
                    babelTypes.isImportDeclaration(node) &&
                    node.source.value === result[1]
                );
            });
            if (!existingImport) {
                const importDeclaration = babelTypes.importDeclaration(
                    [
                        babelTypes.importDefaultSpecifier(
                            babelTypes.identifier(result[0]),
                        ),
                    ],
                    babelTypes.stringLiteral(result[1]),
                );
                path.node.body.unshift(importDeclaration);
            }
        },
    }
    
    • 将处理后的AST转为代码:使用@babel/generate将处理后的AST转换回代码,代码如下:
    const { code } = generate(ast, {
      retainLines: true,
      comments: true,
    });
    

    三、其他相关处理

    (一)动态生成key

    为了确保每个中文文本在国际化处理中有唯一的标识,我们需要动态生成key。这里的生成方式类似excel列名的生成规则,代码如下:

    export const getSortKey = (n: number, extractMap = {}): string => {
      let label = \'\';
      let num = n;
      while (num > 0) {
        num--;
        label = String.fromCharCode((num % 26) + 65) + label;
        num = Math.floor(num / 26);
      }
      const key = `${label}`;
      if (_.get(extractMap, key)) {
        return getSortKey(n + 1, extractMap);
      }
      return key;
    };
    

    每个文件的key前缀则是根据文件路径生成的,且不包含extractDir之前的内容,具体实现如下:

    export const getFileKey = (filePath: string) => {
        const extractDir = getProjectConfig().extractDir;
    
        const basePath = path.resolve(process.cwd(), extractDir);
    
        const relativePath = path.relative(basePath, filePath);
    
        const names = slash(relativePath).split(\'/\');
        const fileName = _.last(names) as any;
        let fileKey = fileName.split(\'.\').slice(0, -1).join(\'.\');
        const dir = names.slice(0, -1).join(\'.\');
        if (dir) fileKey = names.slice(0, -1).concat(fileKey).join(\'.\');
        return fileKey.replace(/-/g, \'_\');
    };
    

    (二)脚手架命令

    为了方便操作,我们提供了i18n-extract-cli脚手架命令,目前支持以下几种操作:

    • 初始化配置文件:执行npx i18n-extract-cli init ,会生成一份i18n.config.json配置文件,内容如下:
    {
      \"localeDir\": \"locales\",
      \"extractDir\": \"./\",
      \"importStatement\": \"import I18N from @/utils/i18n\",
      \"excludeFile\": [],
      \"excludeDir\": []
    }
    
    • 提取中文文本:运行npx i18n-extract-cli extract ,可以将extractDir目录下的中文文本提取到localeDir/zh-CN中。
    • 检查提取情况:使用npx i18n-extract-cli extract:check命令,能检查extractDir文件夹中的中文是否提取完全,需要注意的是,console中的中文也会被检查。
    • 清理未使用的国际化文案:执行npx i18n-extract-cli extract:clear,可以清理extractDir尚未使用的国际化文案。但要注意,该脚本是按每个文件路径作为key来判断当前文件中的sortKey是否使用,所以必须保证每个文件中使用的keyfileKey + sortKey,否则脚本会失效。

    通过上述基于AST的国际化文本提取方法,配合Babel工具以及相关的脚手架命令,开发者能够高效地实现项目的国际化文本提取工作。希望本文能为大家在项目国际化开发过程中提供帮助。

微信扫一扫

支付宝扫一扫

版权: 转载请注明出处:https://www.zuozi.net/6888.html

管理员

相关推荐
2025-08-06

文章目录 一、Promise基础回顾 二、Promise 与 axios 结合使用场景及方法 (一)直接返回 axios …

269
2025-08-06

文章目录 一、模块初始化时的内部机制 二、常见导出写法的差异分析 (一)写法一:module.exports…

107
2025-08-06

文章目录 一、ResizeObserver详解 (一)ResizeObserver是什么 (二)ResizeObserver的基本用法 …

683
2025-08-06

文章目录 一、前期准备工作 (一)下载相关文件 (二)安装必要工具 二、处理扣子空间生成的文件…

338
2025-08-06

文章目录 一、官方文档 二、自动解包的数据类型 ref对象:无需.value即可访问 reactive对象:保持…

371
2025-08-06

文章目录 一、Hooks的工作原理 二、在if语句中使用Hook会出什么岔子? 三、React官方的Hook使用规…

843
发表评论
暂无评论

还没有评论呢,快来抢沙发~

助力内容变现

将您的收入提升到一个新的水平

点击联系客服

在线时间:08:00-23:00

客服QQ

122325244

客服电话

400-888-8888

客服邮箱

122325244@qq.com

扫描二维码

关注微信客服号