Electron+Vue从零开始打造一个本地文件翻译器

今天星期五,又一个愉快的周末快到了,当我脚步轻快的回到家,准备拥抱女友的时候,却发现,女友愁眉苦脸,望着电脑上一堆英文文件夹,不断的长吁短叹并嘟喃道:"好累啊"。因而,我凑近一看,只见她电脑上一堆英文文件夹,不断的重复着复制文件名,而后放百度翻译里翻译成中文,而后又把翻译后的结果给文件重命名,这也太难受了吧。想到女友有难,我做为她的程序猿男友,怎么能袖手旁观,我陷入了沉思,忽然想到用Node不就能作到她手上的那些操做吗,因而说作就作,我立马把女友抛在身后,打开电脑开始行动,毕竟女人只会影响我拔剑的速度。javascript

项目搭建

项目搭建依旧使用的是老组合,Electron + Vue + Node,此次就不讲怎么整合electron和vue了,具体可看 Electron+vue从零开始打造一个本地音乐播放器这篇文章。懒人可经过克隆个人模板文件直接开发,戳这里戳这里.html

项目功能

明确要解决的几个痛点:vue

  • 要能自动翻译
  • 要能将翻译好的名字自动重命名
  • 要能批量翻译
  • 但愿能操做简单,能拖拽一个目录,或拖拽文件

需求明确了,下面我们一步一步来逐个解决。实现效果java

D3CNdJ.gif

项目实现

项目实现并不复杂,逐一解决,有的放矢。node

自动翻译

测试过有道翻译,百度翻译,谷歌翻译。通过对比,最终选择了百度翻译,百度翻译的通用翻译每个月200万字符的免费,(QPS=10)仍是很符合个人需求的。 DMVOc6.pnggit

注册申请翻译服务

要使用翻译服务须要去百度翻译开放平台申请使用权限,选择通用翻译服务就能够了,注册成为开发者后,新建项目获取appid,这个appid很重要,决定了是否能正确发起翻译请求。github

D1LVAK.png

拼接翻译API

经过查看文档可知,通用翻译API HTTP地址为web

https://api.fanyi.baidu.com/api/trans/vip/translateapi

可携带下面这些参数数组

D1jFns.png

因为安全限制,须要生成签名,签名须要 按照 appid+q+salt+密钥 的顺序拼接获得字符串,而后对字符串进行md5加密

const salt = parseInt(Math.random() * 1000000000);
  const sign = md5(globalData.appid + q + salt + globalData.key);
复制代码

生成签名后拼接请求的URL

const params = encodeURI(
    `q=${q}&from=auto&to=${globalData.to}&appid=${globalData.appid}&salt=${salt}&sign=${sign}`
  );
复制代码

翻译功能代码

const md5 = require("md5");
var rp = require("request-promise");
const { globalData } = require("./config.js");

function translate(msg) {
  const q = msg;
  const salt = parseInt(Math.random() * 1000000000); //加盐
  const sign = md5(globalData.appid + q + salt + globalData.key); //生成签名
  const params = encodeURI(
    `q=${q}&from=auto&to=${globalData.to}&appid=${globalData.appid}&salt=${salt}&sign=${sign}`
  );
  const options = {
    uri: `https://fanyi-api.baidu.com/api/trans/vip/translate?${params}`,
  };
  return rp(options).then((res) => JSON.parse(res).trans_result);
}

module.exports = {
  translate,
};

复制代码

主体功能实现

主体功能分为:

  • 批量翻译,支持向下递归翻译
  • 拖拽不定量文件,或者拖拽文件夹翻译
  • 重命名

批量翻译

要实现批量须要获取到目标文件夹路径,而后经过 fs.readdir 读取到目录下文件信息,遍历文件信息,若是是文件,对文件名和后缀进行分割,而后再进行翻译操做,若是是目录,就执行递归操做。

// 读取目录中的全部文件/目录
      fs.readdir(dirPath, (err, files) => {
        if (err) {
          // throw err;
            dialog.showMessageBox({
            type: 'info',
            title: '确认',
            message: '请确认是否选择了目录',
          });
          this.loading = false;
          throw err;
        }
        files.forEach((fileItem) => {
          //遍历文件
		  fs.stat(fullPath, (err, stat) => {
            if (err) {
              throw err;
            }
            // 判断是否为文件
            if (stat.isFile()) {
				//处理文件名
            } else if (stat.isDirectory()) {
              // 递归翻译
              this.startTrans(fullPath);
            }
          });
        });
      });
复制代码
获取文件夹路径

获取文件夹路径有两种方式获取:

  1. 设置input webkitdirectory directory属性,而后监听change事件获取到所选择文件夹的路径
<input @change="getFile" id="attach-project-file" type="file" webkitdirectory  directory />

getFile(e) {
   this.path = e.target.files[0].path;
},
复制代码
  1. 经过H5的拖拽API监听drop事件,获取到 DataTransfer 对象,DataTransfer 对象用于保存拖动而且放下的数据。
<div class="home" @drop.prevent="addFile" @dragover.prevent>
  </div>

    addFile(e) {
      // 将伪数组转换成数组
      this.droppedFiles = [...e.dataTransfer.files];
      // 处理多个文件一块儿拖拽的状况
      if (this.droppedFiles.length > 1) {
        // 只有在同一个目录下才能多选,因此获取到第一个的父级目录就能够了
        this.path = path.dirname(this.droppedFiles[0].path);
        // 标记是不是多选
        this.isDropMulti = true;
      } else {
        this.path = this.droppedFiles[0].path;
      }
    },
复制代码
分割文件名和后缀

因为是翻译文件名,因此须要经过 path.extname 将文件名与后缀分割开来,翻译后再将文件名从新组织,须要注意的是这里须要处理下文件名中的特殊字符,特殊字符会影响翻译结果,可能会致使翻译失败。

// 获取文件的后缀格式
const suffixName = path.extname(fileItem);

 // 获取前缀
const initSubFileName = this.removeSymbol(
	fileItem.split(suffixName)[0]
);
// 移除文件名中的特殊字符
removeSymbol(fileName) {
	const reg = /[`~_!@#$^&*%()=|{}':;',.<>\\/?~!@#¥……&*()——|{}';:""'。,、?\s]/g;
	const newFile = fileName.replace(reg, " ");
	return newFile;
},
复制代码
翻译文件名

对分割好的文件名进行翻译,而后从新新的文件名称,这里要注意的是,因为百度翻译限制了(QPS=10),因此须要添加对翻译请求节流,限制其每秒不能超过10次。

节流函数

const { globalData } = require("./config.js");

const throttle = (function(delay = 1500) {
  const wait = [];
  let canCall = true;
  return function throttle(callback) {
    if (!canCall) {
      if (callback) wait.push(callback);
      return;
    }

    callback();
    canCall = false;
    setTimeout(() => {
      canCall = true;
      if (wait.length) {
        throttle(wait.shift());
      }
    }, delay);
  };
})(globalData.delay);

module.exports = {
  throttle
};
复制代码

翻译后重组文件名

throttle(() => {
        translate(initSubFileName).then(res => {
          if (res) {
            // 若是有【】保留文件名,若是没有就加上【】
            const target = this.checkName(res[0].dst);

            // 拼接带后缀的文件名
            const fullSuffixName = `${target}${suffixName}`;

            // 翻译后的文件路径
            const newPath = path.resolve(dirPath, fullSuffixName);

          } else {
            // 翻译失败
            console.log("翻译接口服务出错");
              dialog.showMessageBox({
              type: "error",
              title: "错误",
              message: "翻译接口服务出错"
            });
          }
        });
      });
复制代码

重命名

重命名使用node自带的 fs.rename

fs.rename(oldPath, newPath, err => {
              if (err) {
                dialog.showErrorBox("错误", "翻译失败,请关闭软件重试");
                this.loading = false;
                throw err;
              }
              console.log(`${initSubFileName} 已翻译成 ${fullSuffixName}`);
              this.path = `${initSubFileName} 已翻译成 ${fullSuffixName}`;
            });
复制代码

最后

终于完成了,我伸了伸懒腰,我兴高采烈的准备去女友那邀功,不当心摔了一跤,我猛地起来,啊,还好,原来是一场梦!不对!按这么说,个人女友。。。。我忽然伤感起来,悲伤的打开了网抑云:

生不出人,我很抱歉!

原来这一切都是假的!!!那晚,泪浸湿了个人枕头!!!源码在这

相关文章
相关标签/搜索