国际化上篇:如何开发一个vscode版的多语言插件

626 阅读12分钟

写在前面

本篇文章所讲内容需要对vscode插件有一定的了解,因篇幅所限,所以某些地方本人讲的不是很详细,也希望大家提出宝贵的意见

背景

去年年底的时候,领导接到上面的一个任务,需要将页面中所有的中文都进行国际化,原因是客户不想在页面上看到中文,这个时候我们就犯了难,因为现有程序中有的做了国际化的也有没做国际化,并且需要翻译的中文实在是太多了,而且步骤大抵相同:

  1. 复制中文,粘贴到百度
  2. 点击翻译,将中文翻译成英文或者是其他语言
  3. 将翻译好的英文加入多语言文件(一般是src/locales文件夹),并取一个牛逼的key
  4. 前端页面展示:intl.getStr('牛逼的key')

那么有没有一款插件可以实现这几步操作,答案是:有地,而且还很多

vite-plugin-auto-i18n (全量翻译之后拦截替换,只针对vite)

i18n-ally (自己提取中文,放入插件帮忙翻译)

easy-i18n-helper (右键vscode编辑器,逐个页面翻译)

得出的结论是没有非常符合我们当前场景的插件,我们的场景如下

  1. 可以自己修改'牛逼的key'
  2. 可以操作现有的多语言树节点
  3. 可以按照现有多语言树的结构生成新的语言文件
  4. 对冗余的管理,手动同步

这里先给大家展示下完成的效果:

mentions2.gif

流程概要

右键vscode代码 -> 筛选中文 -> 通过第三方(百度api)翻译 -> 选定插入翻译树的节点 -> 插入翻译 -> 点击保存回填代码块

主要步骤及实现

俗话说,我们之所以获得现在的成就,是站在巨人的肩膀上,因为我们并不需要全量,也不想自己提取中文,那么就借鉴一下easy-i18n-helper的操作习惯,在把我们想做的功能加进去就好,大体流程如下:

  1. 右键面板,在package.json中加入配置:
"menus": {
      "editor/context": [
        {
          "when": "resourceLangId == javascript || resourceLangId == javascriptreact || resourceLangId == typescript || resourceLangId == typescriptreact",
          "command": "multilingual-extension.translate", // 与extension的入口对应
          "group": "navigation@6"
        }
      ]
    }
    
    "contributes": {
    "commands": [
      {
        "command": "multilingual-extension.translate",
        "title": "翻译此页面"
      }
    ],
    }
  1. 获取vscode的settings配置(包括百度翻译的配置和用户项目的配置)
package.json"configuration": {
      "title": "multilingual-extension",
      "properties": {
        "multilingual-extension.APP ID": {
          "type": "string",
          "default": "xxx",
          "description": "百度翻译申请的APP ID."
        },
        "multilingual-extension.token": {
          "type": "string",
          "default": "ppp",
          "description": "百度翻译申请的密钥."
        },
        "multilingual-extension.itemLanguages": {
          "type": "array",
          "default": [
            "zh:zh-CN",
            "en:en-US"
          ],
          "items": {
            "type": "string"
          },
          "description": "语言列表对应文件名称."
        },
        "multilingual-extension.Locales Path": {
          "type": "string",
          "default": "./src/locales",
          "description": "多语言文件存放位置."
        },
        "multilingual-extension.Import Code": {
          "type": "string",
          "default": "import intl from '@/utils/intl';\n",
          "description": "多语言引入语句."
        },
      }
    },
// 代码获取
const res = workspace.getConfiguration().get('multilingual-extension');
const appId = res['APP ID']; // appid
const token = res['token']; // token
const localesPath = res['Locales Path'] // 多语言路径
  1. 右键拦截项目中展示的中文

这里简单介绍下babel: 因为本人使用babel主要是封装一些公共的方法和类,babel插件的形式并不符合我的使用场景所以安装了这三个包: @babel/generator@babel/parser@babel/traverse,对应babel编译的三个流程:

① 将文件内容转成ast树,生成的ast可以访问ast.net

import { parse } from '@babel/parser';
let ast = parse(fileContent, {
    sourceType: 'unambiguous', // 自动识别是否解析模块语法
    plugins: ['jsx', 'typescript'] // 解析react和typescript
});

② 拦截ast中的某个类型的节点,并用正则识别出中文

traverse(ast, {
            // 字符串字面量:let aaa = '哈哈哈'
            StringLiteral: (path) => {
                if(/[\u4e00-\u9fa5]/.test(path.node.value)) {
                    // 记录位置loc预备替换,id标识,
                    words.push({
                        loc: path.node.loc,
                        value: path.node.value,
                        id: uuid(),
                        // 表示是否是jsx的属性
                        // <div attr='哈哈哈'></div> ==> <div attr={intl.getStr('haha')}></div>
                        isJSXAttribute: path.parent.type === 'JSXAttribute',
                    });
                }
            },
            // 模板字符中有中文 let aaa = `哈哈哈`
            TemplateElement: (path) => {
                if(/[\u4e00-\u9fa5]/.test(path.node.value.raw)) {
                    words.push({
                        loc: path.node.loc,
                        // 砍掉所有的\n \t等符号
                        value: path.node.value.cooked.trim().replace(/\s/, ''),
                        id: uuid(),
                        // { raw: '  测试中文 \\t\n\n', cooked: '  测试中文 \t\n\n' }
                        originalValue: path.node.value,
                        isTemplate: true,
                    });
                }
            },
            // 识别react jsx
            JSXText: (path) => {
                if(/[\u4e00-\u9fa5]/.test(path.node.value)) {
                    words.push({
                        loc: path.node.loc,
                        value: path.node.value.trim(),
                        id: uuid(),
                        originalValue: path.node.value,
                        isJSXText: true,
                    });
                }
            }
        });
  1. 将得到的中文喂给百度api,这里的步骤和数据格式可以参考百度翻译开放平台,当然其他的第三方翻译也都可以
const appId = res['APP ID']; // appid
const salt = (new Date).getTime(); // 随机数
const from = 'zh';
const to = lang;
const transWords = words.join('\n'); // 翻译多个需要用\n拼接
const code = `${appId + transWords + salt + res['token']}`;
const sign = crypto.MD5(code).toString();
const params = querystring.stringify({
  q: transWords,
  appid: appId,
  salt,
  from,
  to,
  sign
});
const options = {
  host: 'api.fanyi.baidu.com',
  port: 80,
  path: `/api/trans/vip/translate?${params}`,
  method: 'GET',
};
// 发起HTTP GET请求
const req = http.request(options, (res) => {
    let data = '';
    res.on('data', (chunk) => {
      data += chunk;
    });
    res.on('end', () => {
      console.log(`Response body: ${data}`, typeof data);
      try {
          const res = JSON.parse(data);
          if(res.trans_result.length > 0) {
              // 结果在这里需要自行处理解析
              console.log(res.trans_result);
          }
      } catch(err) {
          console.error('err----', err);
      }
  });
})
  1. 需要一个vscode的交互面板,展示我们的中文和翻译结果,这里可以参考这位大佬的流程,也介绍了如何跟页面进行通信

image.png

这里有一个实现细节,右键翻译的时候如何定位我们的项目?

这里给出两个思路:

① 用package.json文件位置来寻找,向外层遍历文件

// 根据路径查找项目
export function getProjectPathByPackage(filePath: string, packagePath: string = 'package.json'): string {
  // 递归地查找 package.json 文件
  function findPackageJson(directory) {
      let newDirPath = directory.split('/').slice(0,-1).join('/');
      if(fse.statSync(newDirPath).isDirectory()) {
          const files = fse.readdirSync(newDirPath);
          if(files.includes(packagePath)) {
              return newDirPath;
          } else {
              const result = findPackageJson(`${newDirPath}`);
              if(result) {
                  return result;
              }
          }
      } else {
          // 一般肯定是项目文件夹,不是就在往上找
          findPackageJson(directory);
      }
      return null;
  }
  // 调用函数进行查找
  return findPackageJson(filePath);
}

② 根据vscode的activeTextEditor寻找工作空间目录

// 获取当前项目工作路径
let editor = window.activeTextEditor;
const currentDocumentUri = editor.document.uri;
const selectedWorkspaceFolder = vscode.workspace.getWorkspaceFolder(currentDocumentUri);
let channelPath = selectedWorkspaceFolder && selectedWorkspaceFolder.uri.fsPath; 

编辑中的定制化功能

locales文件树

点击编辑按钮,进入到定制化的页面 QQ_1725341168515.png

可以看到左上角是src/locales下的所有多语言树

QQ_1725346630720.png

那么问题来了,我们怎么能获取到这个对象呢(这个问题当初做的时候很让我头秃,尝试了N多方法)

  1. 直接require或者import():
let path = '/users/project/xxx/src/locales/zh-CN.js'; // 项目路径
import(path); // 不支持

因为Import是js编译阶段运行,所以必须指定具体路径,不能是变量,而且据本人观察vscode的require底层调用的也是import,所以也是不行的

  1. 写脚本,然后通过tsx执行:

编译的时候获取不到变量,那么就在运行的时候获取,用ts执行一下js脚本

① 创建一个文件a.js,内容如下

init();
async function init(){
  let path = '/Users/project/company/xxx/src/locales/index';
  const data = await import(path);
  console.log('default-----', data.default.default);
}

② 在package.json中加入命令然后执行npm run ppp

"scripts": {
    "ppp": "npx tsx ./a.js",
}

执行可以拿到导出的export结果,但还是有缺陷:

tsx命令是用来替代原生 node 指令执行 TS 文件的,node20以上才支持,而且必须在运行项目的package.json中加入 type: "module"

如果多语言文件数据量大一些,脚本执行的时间就会成倍的增加。

  1. 将src/locales通过文件流导入一个临时目录,在展示的webview中import这个目录
// html页面中
import '/tmp/locales'

这个方案我在本地调试的时候非常顺利,一点问题都没有,但是打包之后就发现另一个问题:

/tmp/locales文件内容都变了,但是import导入的还是原来的对象,原因也不难理解:

vscode插件打包的时候会将路径中的对象当成快照存入插件的某个变量中,说到底还是因为在webview import的变量都是编译时候静态查找的

如果静态读取行不动,那能不能写入临时目录之后vscode端动态读取,然后在通过通信的方式传给webview呢,答案是可以,如下所示:

  1. 读写文件,根据文件内容自己拼装成对象树,分为以下几步:

① 将读取的文件内容通过babel转换成ast

const data = await fse.readFileSync('/Users/project/company/xxx/src/locales/index', 'utf8');
const ast = getTransAst(data);

② 拦截属性节点筛选出是对象节点还是普通的key

traverse(ast, {
    // 属性
      Property(path, state) {
          if(path.get('value').isObjectExpression()) { // 节点是对象
          } else {
              // 节点是单个属性
          }
      }
})

③ 如果是对象,通过start和end的位置来确定父子关系,构造一个栈,栈里最后一个start位置小于当前节点并且end位置大于当前节点的就是它的父节点(因为babel是按照从上到下依次输出的)

ExportDefaultDeclaration: {
    enter(path, state) {
      // 栈的初值是最外层default根节点
      stack.push({
        startPos: path.node.start,
        endPos: path.node.end,
        name: 'default'
      });
    }
}

const { start, end } = path.get('value').node;
let handlePropertyArr = stack.filter((item) => item.startPos <= start && item.endPos >= end).slice(1); // 去掉最外层default
if(path.get('value').isObjectExpression()) {
  currentWorkingPropertyObj = {}; // 每次对象都需要根据栈重新构建,因为不确定具体的包含关系
  handlePropertyArr.push({
    startPos: start,
    endPos: end,
    name: name,
  });
  stack.push({
    startPos: start,
    endPos: end,
    name: name,
  });
  for(let i = handlePropertyArr.length - 1;i >= 0;i--) {
    currentWorkingPropertyObj = {[handlePropertyArr[i].name]: currentWorkingPropertyObj}; // 顺序是由内向外,所以反向遍历栈
  }
}

④ 普通属性直接拦截合并

let schema = currentWorkingPropertyObj;
const { value } = path.get('value').node;
let name = path.get('key').node.name;
// 正常的obj -> { aaa: 111 }
if(value) {
// 只有最外层default
if(handlePropertyArr.length === 0) {
  schema[name] = value;
} else {
  // 将{a: {b: {c: {}}}} -> {a: {b: {c: {d: 123}}}}
  for(let i = 0;i < handlePropertyArr.length - 1;i++) {
    schema = schema[handlePropertyArr[i].name];
  }
  // 合并属性
  schema[handlePropertyArr[handlePropertyArr.length - 1].name] = {...schema[handlePropertyArr[handlePropertyArr.length - 1].name], [name]: value};
}

⑤ 将每层结果合并

propertysResult = deepMerge(propertysResult, currentWorkingPropertyObj)

为什么用deepMerge而不用Object.assign?

因为Object.assign只能进行浅合并,无法解决这种情况:

Object.assign({aaa: 123}, {aaa: {bbb: 456}}) // 会丢掉123只剩下 {bbb:456}

⑥ 放入临时目录中 注意,这里的临时目录并不是固定路径,而是node的os模块提供的临时目录路径

临时目录的目的是为了我们可以定制化自己的文件目录和数据格式(例如src/locales下没有index.js的情况)

import fse from 'fs-extra';
targetPath: string = `${os.tmpdir()}/locales`; // 临时文件存储路径
fse.copy(srcPath, targetPath).then(() => { // 将src/locales下的文件copy到临时目录中
      instance[callback] && instance[callback](); // 之后渲染插件页面
  });

可编辑key

因为table展示是自己画的table所以借助antd的可编辑输入框就能实现

QQ_1725352108460.png

冗余检查

如下图,值是否存在就是冗余检查,举个例子:

QQ_1725352511506.png 如果我要添加的对象是它的中文是'测试',我会搜索整个多语言树里的测试,如果有多个就全都筛出来,让用户来判断用谁的key,在同步之后还可以撤回

QQ_1725352564884.png 解决办法就是把多语言中所有的中文都打平成一个map:

{
    aaa: {
        bbb: '测试'
    }
}
// 转换为
let obj = { 'aaa.bbb': '测试' }

这样判断Object.values(obj)是否包含'测试'就可以了

保存功能

QQ_1725358957938.png 我把保存功能单拉出来说一下,是因为保存分为以下几个小功能

  1. 将代码里的中文替换为intl.getStr('xxKey')
  2. 将key:value添加到正确的位置
  3. 检测是否有intl的引入,如果没有需要加入import代码
  4. 检测settings中配置的语言是否有对应的国际化文件,如果没有需要按照现有中文的结构全量翻译

代码替换

使用vscode相关api来实现,与直接使用node进行文件读写的区别是需要自己点ctrl + s保存

  1. 先把代码置为编辑态
// 开启编辑态
await window.showTextDocument(window.activeTextEditor?.document.uri);
  1. 在前文拦截中文的时候记录下中文的位置loc
const ast = this.getTransAst();
traverse(ast, {
    // 字符串字面量:let aaa = '哈哈哈'
    StringLiteral: (path) => {
        if(/[\u4e00-\u9fa5]/.test(path.node.value)) {
            // 记录位置loc预备替换,id标识,
            words.push({
                loc: path.node.loc,
                value: path.node.value,
                id: uuid(),
                // 表示是否是jsx的属性
                // <div attr='哈哈哈'></div> ==> <div attr={intl.getStr('haha')}></div>
                isJSXAttribute: path.parent.type === 'JSXAttribute',
            });
        }
    },
    // 模板字符中有中文 let aaa = `哈哈哈`
    TemplateElement: (path) => {
        if(/[\u4e00-\u9fa5]/.test(path.node.value.raw)) {
            words.push({
                loc: path.node.loc,
                // 砍掉所有的\n \t等符号
                value: path.node.value.cooked.trim().replace(/\s/, ''),
                id: uuid(),
                // { raw: '  测试中文 \\t\n\n', cooked: '  测试中文 \t\n\n' }
                originalValue: path.node.value,
                isTemplate: true,
            });
        }
    },
    JSXText: (path) => {
        if(/[\u4e00-\u9fa5]/.test(path.node.value)) {
            words.push({
                loc: path.node.loc,
                value: path.node.value.trim(),
                id: uuid(),
                originalValue: path.node.value,
                isJSXText: true,
            });
        }
    }
});

  1. 遍历words数组,替换为对应的模板字符串
async replaceEditorText(): Promise<void> {
        await window?.activeTextEditor.edit(editBuilder => {
            words.forEach((element) => {
                const { loc } = element as any;
                const startPosition = new Position(loc.start.line - 1, loc.start.column);
                const endPosition = new Position(loc.end.line - 1, loc.end.column);
                const selection = new Range(startPosition, endPosition);
                if(!selection) {
                    return;
                }
                editBuilder.replace(selection, `intl.getStr(${element.key})`);
            });
        });
    }

将key:value添加到多语言树的对应位置

在上文用户已经选择了具体的树节点: QQ_1725418419120.png 记录下用户选择,用vscode的通信机制传到插件端就好,我们重点来说下如何根据节点查找到文件的具体位置:

  1. 首先需要找到所有语种的文件路径例如src/locales/zh-CN.jssrc/locales/en-US.js,读取文件内容
  2. 拿到用户选择的节点路径,例如数组arr = [license,testLicTips]
  3. 以语言文件作为递归的根节点,使用babel筛选节点的属性,如果能匹配上,那么将这个属性从路径数组中移除
  4. 直到路径数组长度为0,停止查找,并记录下此时节点的位置:node.loc
  5. 遍历属性,如果遇到import导入其他文件的情况,继续第一步读取文件内容
let loc = {}; // 要插入的具体位置
const filePath = importPath.resolve(localesPath, fileRelativePath); // 要插入的文件路径
try {
  // 读取文件
  const data = await fse.readFileSync(filePath, 'utf8');
  const ast = getTransAst(data);
  let importMap = {};
  let fileNodeName: any = {};
  traverse(ast, {
    // 用户选择的路径只有一层根节点
    ExportDefaultDeclaration: (nodePath) => {
      if(arr.length === 0) {
        loc = nodePath.node.loc;
        nodePath.stop();
      }
    },  
    Program: {
        enter(nodePath, state) {
            nodePath.traverse({
                ImportDeclaration(path) {
                    // nodePath.node拿到整个语句
                    const { specifiers, source: { value } } = path.node;
                    if(specifiers && specifiers.length > 0) {
                      specifiers.forEach((node) => {
                        // import aaa from path
                        if(['ImportSpecifier', 'ImportDefaultSpecifier', 'ImportNamespaceSpecifier'].includes(node.type)) {
                          importMap[node.local.name] = value;
                        }
                      });
                    }
                },
            });
        }
    }
  });
  traverse(ast, {
    'Property|SpreadElement': (path) => {
      path.traverse({
        Identifier:(p) => {
          const { node } = p;
          // 1.匹配名字
          // 2.arr的长度为0代表找到了位置和路径
          let name = node.key ? node.key.name : node.name;
          let value = node.value ? node.value.name : node.name;
          if(name === copyArr[0]) {
            copyArr.shift();
            if(copyArr.length === 0) {
              // 记录loc
              loc = node.loc;
              path.stop();
              p.stop();
            } else if(Object.keys(importMap).includes(value)) {
              // 3.如果匹配到importMap读文件继续查找
              isRecursion = true;
              fileNodeName = name;
            }
          }
        }
      });
    }, 
  });
  if(isRecursion) {
    // 如果有import导入文件的情况,继续递归,然后再次查找
    return await recursion(localesPath, importMap[fileNodeName], copyArr);
  } else {
    return {
      filePath,
      loc,
    };
  }
} catch(err) {
  console.log('error-----', err);
}
  1. 拿到具体位置loc,在这个位置上插入key: value 读取的语言文件是个大的字符串,要想插入必须得打散,用什么打散呢? 用换行
import fse from 'fs-extra';
let data = fse.readFileSync(filePath, 'utf8').split(/\r\n|\n|\r/gm);

这样可以得到多个字符串片段 QQ_1725421982061.png

还有一个问题,我们得到的其实是个{'key': 'value'} 这样的对象,要怎么加工成 key: value这样的字符串呢?

JSON.stringify({aaa: 'value'}, null, 4).slice(2,-2) // 结果是 "aaa": 111

我们现在有要插入的位置loc,要插入的对象data,插入内容"aaa": 111,是不是直接用data.splice(loc.start, 0 , "aaa": 111) 这样就可以了呢,其实这里还有个问题: 在babel中拦截对象的字符串key和普通key用的不是一种类型,如下所示:

QQ_1725422865733.png 大家可以尝试一下

所以在插入之前需要对对象的key去除引号:

// 去除引号
function getNoQuoteObjByCode(code) {
  return code.replace(/("(\\[^]|[^\\"])*"(?!\s*:))|"((\\[^]|[^\\"])*)"(?=\s*:)/g, '$1$3');
}
export async function writeWordsToLocalesFile(filePath, value, loc){
  let data = fse.readFileSync(filePath, 'utf8').split(/\r\n|\n|\r/gm); // 用换行拆分
  data.splice(loc.start.line, 0, `${getNoQuoteObjByCode(JSON.stringify(value, null, 4)).slice(2,-2)},`); // 这里根据路径所对应的函数来插入
  fse.writeFileSync(filePath, data.join('\r\n')); // 拼接换行写入文件
}

检测intl的引入

  1. 首先在配置中插入需要的代码片段:
"configuration": {
      "title": "multilingual-extension",
      "properties": {
          "multilingual-extension.Import Code": {
          "type": "string",
          "default": "import intl from '@/utils/intl';\n",
          "description": "多语言引入语句."
        },
        "multilingual-extension.Import Name": {
          "type": "string",
          "default": "intl",
          "description": "引入语句的引入变量."
        },
      }
}
  1. 在vscode插件中拿到:
const res = workspace.getConfiguration().get('multilingual-extension');
let importCodes = res['Import Code'];
let methodName = res['Import Name'];
  1. 使用babel拦截import,检测intl是否存在,如果不存在插入importCodes语句
let isImported = false;
const ast = this.getTransAst();
traverse(ast, {
    Program: {
        enter(nodePath, state) {
            nodePath.traverse({
                // 获取intl
                ImportDefaultSpecifier: (path, state) => {
                    if (path.node.local.name === methodName) {
                        isImported = true;
                        path.stop();
                    }
                },
                // import { intl } from '@/utils/intl'获取intl
                ImportSpecifier: (path) => {
                    if (path.node.local.name === methodName) {
                        isImported = true;
                        path.stop();
                    }
                },
            });
        }
    }
});
// 这里走vscode添加逻辑
if (!isImported) {
    // nodejs读取文件插入
    // importCode = generate(template('import intl from "@/utils/intl"')()).code
    // const { code, map } = generate(ast);
    // let content = `${importCode}\n${code}`
    // fse.writeFileSync(path.join('./sourceCode.js'), content);
    await window?.activeTextEditor?.edit(editBuilder => {
        editBuilder.insert(new Position(0, 0), importCodes); // 通过vscode的api插入importCodes
    });
}

检查国际化文件,没有就自动生成

  1. 拿到用户配置的语言列表
"multilingual-extension.itemLanguages": {
  "type": "array",
  "default": [
    "zh:zh-CN",
    "en:en-US",
    "kor:kor-KOR"
  ],
  "items": {
    "type": "string"
  },
  "description": "语言列表对应文件名称."
},

const res = workspace.getConfiguration().get('multilingual-extension');
this.languages = res['itemLanguages']
  1. 判断用户配置的语言列表是否在locales文件夹里
import fse from 'fs-extra'this.languages.forEach((item) => {
        const isFileExists = fse.existsSync(`${this.localesFullPath}/${item.localFileName}.${this.fileType}`);
        if(!isFileExists) {
            this.readyGenerateLangs.push(item);
        } else {
            this.existLangs.push(item);
        }
    });

根据readyGenerateLangs长度是否为0判断语言文件是否能对齐,如果没对齐询问用户是否需要生成新的文件:

QQ_1725426111601.png 如果用户选择是,我们需要把中文的多语言对象树打平成Map格式,并喂给百度翻译

private async handleGenarateLangs(targetZHLocales) {
        const { valuesObj } = convertToChildren(targetZHLocales, []);
        const len = Object.values(valuesObj).length;
        const generateKeys = Object.keys(valuesObj);
        for(let i = 0;i < this.readyGenerateLangs.length;i++) {
            let langGenRes = {};
            const { langType, localFileName } = this.readyGenerateLangs[i];
            let langGenerateArr = [];
            let transPromises = [];
            // 每次翻译上限大约390个
            for(let j = 1;j <= Math.ceil(len / 390); j++) {
                const transPromise = transWordsByLang(Object.values(valuesObj).slice((j - 1) * 390, j * 390) as string[], langType);
                transPromises.push(transPromise);
            }
            Promise.all(transPromises).then(res => {
                for(let i = 0;i < res.length;i++) {
                    let transResult = res[i];
                    transResult.forEach((ite, idx) => {
                        ite.genKey = generateKeys[i * 390 + idx];
                    });
                    langGenerateArr = [...langGenerateArr, ...transResult];
                }
                langGenerateArr.forEach((item) => {
                    const itemArr = item.genKey.split('.').reverse();
                    let currentItem = {[itemArr[0]]: item.dst};
                    for(let k = 1;k < itemArr.length;k++) {
                        currentItem = {[itemArr[k]]: currentItem};
                    }
                    langGenRes = deepMerge(langGenRes, currentItem);
                });
                let exportStr = 'export default ' + getNoQuoteObjByCode(JSON.stringify(langGenRes, null, 4)) + ';\n';
                fse.writeFileSync(`${this.localesFullPath}/${localFileName}.${this.fileType}`, exportStr);
        }
    }

这里有个需要注意的点,百度翻译每次的上限大概是390个词条,超过了就会报错,所以将每批词条数量控制在390,然后把map中的key item.genKey拆解成对象合并到最后的结果langGenRes中,写入到多语言文件中

总结和展望

至此,插件的所有功能都已讲述完成,当然还有一些缺陷和改进:

  1. 支持vue文件的解析
  2. 设计上无法支持以下情况
// zh-CN.js中
{aaa: '将在【{remainDay}】天后过期'}
// react代码中动态注入变量
{intl.getStr('license.testLicTips.inDate.main', {
  remainDay: licenseInfo?.remainDay,
})}
  1. 无法兼容seo
  2. 都是对代码进行操作,真正用起来冲突的地方会很多,需要人工merge
  3. 插件自动翻译之后,如果跟需求方(pm)有冲突,还需要再进行补充和修改
  4. 插件地址在此,如果需要可以下载

所以未来能不能有一个平台,来完美解决现有的这些问题呢?如果有下篇的话这些都会在下篇为大家揭晓

当然以后还会写一些vscode插件开发相关的文章,水平有限,希望大家可以提一些宝贵的意见和建议