微信号:gh_b206cf608d16

介绍:爆米兔,是一个H5营销在线制作工具.

实现全文机器翻译

2018-12-25 20:39 李松峰

编者按:本文转载自李松峰的博客,有少量改动,点击文末“阅读原文”查看原文。李松峰,资深技术图书译者,翻译出版过40余部技术及交互设计专著,现任360奇舞团高级前端开发工程师,360前端技术委员会委员、W3C AC代表。

众成翻译三期实现全文机器翻译功能,背后的需求是把长期未被译者认领的文章,通过谷歌高级翻译API自动翻译出来,然后人工编辑、审校后发布。这样一方面可以消化累积的原文,另一方面也能增加产出。

乍一看,实现自动全文翻译,无非就是把文章以段为单位发给翻译API,拿到译文后再逐段替换回去就行了。

其实不然。

并非所有内容都要翻译,比如代码。代码有代码段和文本内的(行内)代码,都不能翻译。因此简单以段为单位翻译替换的想法行不通。

不仅如此,原文是Markdown格式,所以文本中的网址、图片地址也不能走API,但是链接文本和图片说明又必须翻译。比如

[Please visit this link](http://www.very-good-domain-name.com/how-to-master-react-app-development.html)

这个链接,“Please visit this link”要翻译,但“very-good-domain-name”和“how-to-master-react-app-development”不能翻译。

细思恐极。怎么办?只能借助Markdown解析和编译工具,比如marked,它能把Markdown转换成HTML。关于marked作为编译器的结构和原理,可以参考这篇文章:探究JavaScript上的编译器 —— marked。下面关于marked的架构图也摘自这篇文章:

经分析,marked有两个切入点可资利用。本文先介绍利用第一个切入点的方案,而且这个切入点也正好能满足前述需求分析。

marked的第一个切入点是Renderer的text()方法:

Renderer.prototype.text = function(text){

   return text;

}

据观察,作为“span level renderer”的text()负责处理所有文本。换句话说,需要翻译所有内容都要经过它,而且不需要翻译的内容不会经过它!简直完美,哈哈。

这么说来,只要在调用marked的时候重写text(),把它接收到的每段英文翻译成中文再返回就可以了,如下面的伪代码所示:

// 创建自定义的Renderer对象

let renderer = new marked.Renderer()

renderer.text = function(text){

   //调用谷歌高级翻译API翻译

   let translation = googleTranslationPremium.translate(text,options)

   return translation;

}

然而不行!marked是一个同步库,而调用API取得结果是个异步过程,这样注入的并不是异步调用的返回结果,而是“[object Promise]”这个字符串!(谷歌高级翻译支持Promise方式调用。)

怎么办?可以把marked重写为异步版,太花时间(或许以后有时间可以考虑)。一个变通方案是“用空间来换时间”:就是运行两遍marked,第一遍只收集英文text,然后在marked外部调用API完成翻译,第二遍再利用翻译结果同步替换英文text

代码如下:

// 取得文章MD文本

let mdString = (await this.model('article').where({id:2254}).find()).content

// 全文翻译对象,以键-值对形式保存每段英文和译文

let translations = {}

// 创建自定义渲染器

let renderer = new marked.Renderer()

// 第一遍:获取文本

renderer.text = function(text) {

  // 初始化全文翻译对象,把每段文本的英文作为对象的键

  translations[text] = ''

  return text

};

// 无需保存结果

marked(mdString,{renderer:renderer})

// 中间处理:异步翻译

for(let text in translations){

  // 限制请求频率,每秒发送一次翻译请求

  await sleep(1)

  // 发送翻译请求,异步得到每段英文的译文

  let translation = await googleTranslationPremium.translate(text,options)

  // 取得译文,并作为全文翻译对象对应键的值

  translations[text] = translation[0]

  // 监控实时输出

  console.log(translation[0])

}

// 第二遍:生成HTML

renderer.text = function(text) {

  // 把英文替换为译文

  text = translations[text]

  return text

};

let html = marked(mdString,{renderer:renderer})

这个方案成功了,自动翻译结果如下:

但新问题又暴露出来了。比如这句英文:

See Vue.js’ installation page for more info.

因为中间有一个链接,所以在通过text()方法时,会分三次调用,分别翻译:

  • See

  • Vue.js’ installation page

  • for more info.

翻译结果组合起来就是:

看到Vue.js“安装页面 更多信息。

本来是完整的一句话,硬分成三个片段分别翻译,理论上会丢失上下文,翻译结果应该不是最佳的。比如,把这句话完整地发给谷歌高级翻译,得到的结果是:

有关详细信息,请参阅Vue.js的安装页面。

显然好多了。

为此,还要利用marked第二个可以利用的切入点,尝试解决这个问题。

关于这个方案的代码,需要补记两个问题,请读者注意:

  • 前面示例代码中的await sleep(1),并非Node.js内置的方法;

  • 方案的完整实现应该包括错误补偿,即加上所谓的“指数退避”策略

关于奇舞周刊

《奇舞周刊》是360公司专业前端团队「奇舞团」运营的前端技术社区。关注公众号后,直接发送链接到后台即可给我们投稿。


 
奇舞周刊 更多文章 前端埋点统计方案思考 NGINX 反向代理 奇舞周刊第 288 期:无题 你不知道的Chrome调试工具技巧 第一天:console中的'$' Node.js定时邮件的那些事儿
猜您喜欢 ANOVA 模型拟合(一) Go 1.7 正式发布 3q大战 安徽大学江淮学院首期课程置换综合实训课正式启动 【干货】PHP中9大缓存技术