前端国际化自动化插件(translate-cli)

macbook-459196_640.jpg

前言

最近在研究如何将一个英文的项目,自动做国际化,不需要手动做国际化,写了一个脚本,虽然并没有准确无误的完成国际化,但是实现了半自动化,记录一下方案。

开发cli插件步骤

  • 初始化一个项目npm init -y
  • 创建项目目录

image.png

  • 在package.json文件中加入bin

image.png

 "bin": {
    "translate-cli": "./bin/cli.js"
  },
复制代码
  • 执行npm link,translate-cli 会插入到环境变量中
  • translate-cli 脚本就可以执行啦。

lingui.js—react国际化库

这个库很好用,非常推荐这个库,他还支持javascript,typescript翻译,不需要手动写国际化的静态文件,他可以执行命令自动生成国际化的静态文件。而且官方文件特别详细。
官方文档:lingui.js.org/

jscodeshift—代码自动化重构利器

  • 他可以解析javascript,typescript代码
  • 将代码转换成AST语法树,然后根据节点批量查询,操作,输出
  • code->AST可视化网址:astexplorer.net/
  • 提供了api:find查找,replaceWith()替换,insertBefore()插入等。

image.png

国际化插件方案

  • 批量读取文件
  • 使用jscodeshift插件转化成AST语法树
  • 根据AST语法树查看需要做国际化的节点规则
  • 根据节点规则查找节点,判断是否做国际化
  • 将需要做国际化的节点的值替换成国际化代码

国际化插件主要逻辑

配置文件translate-cli.js

module.exports = {
  pathRoot: 'public',
  regRules: [
    {
      reg: 'public/**/*[!.test].tsx',
      exten: 'tsx',
      rules: [
        'ExpressionStatement:StringLiteral',
        'JSXElement:StringLiteral',
        'JSXText',
        'ClassProperty:Literal',
        'TemplateLiteral',
        'ObjectExpression:Literal'
      ],
    },
    {
      reg: 'public/**/*.ts',
      exten: 'ts',
      rules: [
        'ExpressionStatement:StringLiteral',
        'TemplateLiteral',
        'ObjectExpression:Literal'
      ],
    },
  ],
};

复制代码

1.根据配置文件translate-cli.js中的匹配文件路径匹配规则regRules,遍历regRules读取文件

// 遍历文件路径匹配规则
for (value of regRules) {
  mapFiles(value);
}
复制代码

2.glob扫描需要翻译的文件。之后遍历文件,使用fs.readFileSync()读取文件内容,将内容使用jscodeshift插件转为AST语法树,根据配置的需要做国际化的节点(AST语法树中的节点)rules,遍历规则做国际化。

/**
 * 遍历文件,读取文件,根据不同的规则,来替换国际化
 * @param {reg} 文件路径匹配规则
 * @param {exten} 文件后缀名
 * @param {rules} 该文件路径规则中的需要做国际化的节点
 */
const mapFiles = ({ reg, exten, rules }) => {
  const j = jscodeshift.withParser(exten);
  const files = glob.sync(path.join(process.cwd(), reg));
  for (value of files) {
    console.log(`${value}处理中。。。`);
    isFirstImport = false;
    const filedata = fs.readFileSync(value, 'utf-8');
    const ast = j(filedata);

    // 遍历一个文件中的不同节点
    for (rule of rules) {
      replaceIntl(j, ast, rule);
    }

    fs.writeFileSync(value, ast.toSource());
    console.log(`${value}国际化规则替换成功`);
  }
};
复制代码

3.之后使用ast.find()在AST语法树中提取节点,根据查找节点的值,根据总结出的是否需要做国际化的规则(需要做的国际化规则,是从项目中总结出来的,与项目强关联,如当值为string类型,且首字母是大写的时候),返回true,false。

/**
 * 国际化匹配规则
 * @param {*} rule 根据节点名称
 * @param {*} value 值
 * @returns
 */
const replaceRule = (rule, item, j) => {
  const { value } = item.value;
  // 不需要翻译的信息直接返回
  if (
    noIntl.includes(value) ||
    item.name === 'key' ||
    item?.parentPath?.value.type === 'TaggedTemplateExpression' ||
    item?.parentPath?.parentPath?.value?.callee?.name === 'navigationLogger'
  ) {
    return false;
  }

  // 当首字母为大写,的string类型的,返回true
  const reg =
    typeof value === 'string'
      ? /[A-Z]/.test(
        value.replace(/\n/g, '').replace(/\r/g, '').trim().slice(0, 1)
      )
      : false;
  switch (rule) {
    case 'JSXText':
    case 'ExpressionStatement:StringLiteral':
    case 'JSXElement:StringLiteral':
    case 'ClassProperty:Literal':
    case 'ObjectExpression:Literal':
      return reg;
    case 'TemplateLiteral':
      const { raw } = j(item).find(j.TemplateElement).at(0).get().value.value;
      console.log(rule, raw);

      return raw
        ? /[A-Z]/.test(
          raw.replace('\n', '').replace('\r', '').trim().slice(0, 1)
        )
        : false;
    default:
      return false;
  }
};
复制代码

3.当返回true的时候,将该节点使用ast.replaceWith()函数将内容退换成做国际化的代码,国际化的代码也是规矩不同的规则,返回国际化的模板,(如:在标签中写需要使用{},使用字符串模板,动态传值时)。

/**
 * 根据值中有单引号,或者双引号返回不同的模板
 * @param {*} value
 */
const templateScheme = (value) => {
  return `t\`${value
    .replace(/`/g, '\\`')
    .replace(/{{/g, "'{{")
    .replace(/}}/g, "}}'")
    .replace(/{}/g, "'{}'")}\``;
};

/**
 * 国际化替换模板
 * @param {*} rule 根据规则返回模板
 * @param {*} value 值
 * @returns
 */
const template = (rule, p, j) => {
  const value = p?.value?.value?.replace(/\n/g, '').replace(/\r/g, '').trim();

  const { type } = p.parentPath.value;

  switch (type) {
    case 'JSXAttribute':
      return `{${templateScheme(value)}}`; //  <div rule='Demo' ></div>
  }
  switch (rule) {
    case 'JSXText': // <div>Dddddd</div>
      return `{${templateScheme(value)}}`;
    case 'TemplateLiteral': // 字符串模板时
      return templateTemplateLiteral(p, j);
    case 'ExpressionStatement:StringLiteral': // 函数中国际化
    case 'JSXElement:StringLiteral': // <div>{Demo}</div>
    case 'ClassProperty:Literal': // 函数中国际化
    case 'ObjectExpression:Literal':
      return `${templateScheme(value)}`;
  }
};
/**
 * 当模板字符串时,返回国际化翻译
 * @param {*} p
 */
const templateTemplateLiteral = (p, j) => {
  let tel = '';
  const templateElements = j(p).find(j.TemplateElement);
  const identifiers = j(p).find(j.Identifier);

  templateElements.forEach((templateElement, index) => {
    const value = identifiers.at(index).length
      ? identifiers.at(index).get().value.name
      : '';
    tel = `${tel}${templateElement.value.value.raw}${
      value ? ` \${${value}} ` : ''
    }`;
  });

  return templateScheme(tel);
};
复制代码

4.读取文件时如果该文件中存在需要做国际化的节点时,在该文件的头部引入国际化库即在头部插入import { t } from "@lingui/macro"

/**
 * 在文件头部插入国际化
 */
const insertIntl = (j, ast) => {
  // 判断是否含有需要做国际化的节点,该文件是否已经插入过
  if (
    !ast
      .find(j.ImportDeclaration)
      .find(j.Identifier)
      .filter((item) => {
        return item.value.name === 't';
      }).length &&
    !isFirstImport
  ) {
    isFirstImport = true;
    ast.get().node.program.body.unshift("import { t } from '@lingui/macro';");
    console.log('import { t } from "@lingui/macro" 插入成功');
  }
};

复制代码

参考

总结

此文章中的国际化插件可能不适用其他项目,仅供参考。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享