80

实现全文机器翻译

 5 years ago
source link: http://lisongfeng.cn/post/full-text-machine-translation-in-zcfy-cc.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

众成翻译三期计划实现全文机器翻译功能,背后的需求是把长期未被译者认领的文章,通过谷歌高级翻译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的架构图也摘自这篇文章:

vaIzEzN.png!web

经分析,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})

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

yAzyEnM.png!web

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

See Vue.js’ installation page for more info.

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

  1. See
  2. Vue.js’ installation page
  3. for more info.

翻译结果组合起来就是:

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

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

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

显然好多了。

为此,还要利用marked第二个可以利用的切入点,尝试解决这个问题。想知道marked的第二个切入点是什么,如何满足需求同时解决问题?敬请期待下一篇文章。

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

  1. 前面示例代码中的await sleep(1),并非Node.js内置的方法;
  2. 方案的完整实现应该包括错误补偿,即加上 所谓的“指数退避”策略

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK