在本章,我将使用一个第三方的免费翻译 API 服务和少许 JavaScript 代码来添加用户动态的实时语言翻译功能。
在本章中,我将从服务器端开发的“安全区域”脱离,研究与服务器端同样重要的客户端组件的功能。你是否看到过某些网站在用户生成的内容旁边显示的“翻译”链接? 这些链接会触发非用户本地语言内容的实时自动翻译。翻译的内容通常插入原始版本的下方。今天我将向你展示如何将相同的功能添加到 Microblog!
服务器端与客户端
迄今为止,在我遵循的传统服务器端模型中,有一个客户端(由用户驱动的 Web 浏览器)向应用服务器发出 HTTP 请求。请求可以简单地请求 HTML 页面,例如当你单击“个人主页”链接时,或者它可以触发一个操作,例如在编辑你的个人信息之后单击提交按钮。
在这两种类型的请求中,服务器通过直接发送新的网页或通过发送重定向来完成请求。然后客户端用新的页面替换当前页面。只要用户停留在应用的网站上,该周期就会重复。在这种模式下,服务器完成所有工作,而客户端只显示网页并接受用户输入。
有一种不同的模式,客户端扮演更积极的角色。在这个模式中,客户端向服务器发出一个请求,服务器响应一个网页,但与前面的情况不同,并不是所有的页面数据都是 HTML,页面中也有部分代码,通常用 Javascript 编写。一旦客户端收到该页面,它就会显示 HTML 部分,并执行代码。
从那时起,你就拥有了一个可以独立工作的活动客户端,而无需与服务器进行联系或只有很少联系。在严格的客户端应用中,整个应用通过初始页面请求下载到客户端,然后应用完全在客户端上运行,只有在查询或者变更数据时才与服务器联系。 这种类型的应用称为单页应用(SPAs)。
大多数应用是这两种模式的混合,并结合了两者的技术特点。 我的 Microblog 应用主要是服务器端应用,但今天我将添加一些客户端操作。为了实时翻译用户动态,客户端浏览器将异步请求发送到服务器,服务器将响应该请求而不会导致页面刷新。然后客户端将动态地将翻译插入当前页面。 这种技术被称为 Ajax
,这是 Asynchronous JavaScript 和 XML 的简称(尽管现在 XML 常常被 JSON 取代)。
实时翻译的工作流程
由于使用了 Flask-Babel
,本应用对外语有很好的支持,可以支持尽可能多的语言,只要我找到了对应的译文。但是遗漏了一个元素,用户将会用他们自己的语言发表动态,所以用户很可能会用应用未知的语言发表动态。 自动翻译的质量大多数情况下不怎么样,但在,如果你只想对另一种语言的文本了解其基本含义,这已经足够了。
这正是 Ajax 大展身手的好机会! 设想主页或发现页面可能会显示若干用户动态,其中一些可能是外语。如果我使用传统的服务器端技术实现翻译,则翻译请求会导致原始页面被替换为新页面。事实是,要求翻译诸多用户动态中的一条,并不是一个足够大的动作来要求整个页面的更新,如果翻译文本可以被动态地插入到原始文本下方,而剩下的页面保持原样,则用户体验更加出色。
实施实时自动翻译需要几个步骤。首先,我需要一种方法来识别要翻译的文本的源语言。我还需要知道每个用户的首选语言,因为我想仅为使用其他语言发表的动态显示“翻译”链接。当提供翻译链接并且用户点击它时,我需要将 Ajax 请求发送到服务器,服务器将联系第三方翻译 API。一旦服务器发送了带有翻译文本的响应,客户端 JavaScript 代码将动态地将该文本插入到页面中。
语言识别
要想翻译首先要做的是确定一条用户动态属于哪种语言。这不是一门精确的科学,因为不能确保监测结果绝对正确,但是对于大多数情况,自动检测的效果相当好。 这里我会使用 Python 的一个第三方库 langdetect
来完成这个认为。首先,我们先去安装它:
(venv) $ pip install langdetect
langdetect
的基本用法如下:
>>> from langdetect import detect
>>> detect("War doesn't show who's right, just who's left.")
'en'
>>> detect("Ein, zwei, drei, vier")
'de'
对于没法识别语言特征的字符串的,它会报错 :
langdetect.lang_detect_exception.LangDetectException: No features in text.
我的构想是在用户用户发布动态时自动检测语言,并写入到 post
表的 language
字段中。
首先在 app/models.py
中添加 language
字段到 Post
模型:
# app/models.py
class Post(db.Model):
# ...
language = db.Column(db.String(5))
当数据库模型发生变化时,都需要生成数据库迁移:
(venv) $ flask db migrate -m "add language to posts"
然后将迁移应用到数据库更新:
(venv) $ flask db upgrade
现在可以修改发布用户动态的视图函数,增加识别语言的功能:
# app\main\routes.py
from langdetect import detect
@main_routes.route('/', methods=['GET', 'POST'])
@main_routes.route('/index', methods=['GET', 'POST'])
@login_required
def index():
form = PostForm()
if form.validate_on_submit():
try:
language = detect(form.post.data)
except:
language = ''
post = Post(body=form.post.data, author=current_user, language=language)
db.session.add(post)
db.session.commit()
flash(_('Your post is now live!'))
return redirect(url_for('main.index'))
# ...
有了这个变更,每次发表动态时,都会通过 langdetect
检查文本来尝试确定语言。如果语言监测为未知,我会将一个空字符串保存到数据库中以安全地使用它。我将采用约定,将任何将把语言设置为空字符串的帖子假定为未知语言。
展示一个“翻译”链接
现在要做的是在不是当前用户的首选语言的用户动态下,添加一个“翻译”链接。
# app\templates\_post.html
{% if post.language and post.language[:2] != g.locale[:2] %}
<br><br>
<span id="translation{{ post.id }}">
<a href="#">{{ _('Translate') }}</a>
</span>
{% endif %}
我在 _post.html
子模板中执行此操作,以便此功能出现在显示用户动态的任何页面上。 翻译链接只会出现在检测到语言种类的动态下,并且必须满足的条件是,这种语言与用 Flask-Babel
的 localeselector
装饰器装饰的函数选择的语言不匹配。
回想一下上一章章所选语言环境存储为 g.locale
,所以我们用动态的 language
字段和 g.locale
相比较;注意:我们这里只比较语言代码的前面两个字母,这是因为语言代码还会在后面细分(如:zh_CN
、zh_TW
等等)。
现在我们可以看到在英文动态下出现了“翻译”的链接:
使用第三方翻译服务
当你在搜索引擎搜索翻译 API 时,你可以找到非常多的服务提供商,比如谷歌、百度、微软、网易等都提供这方面服务,但这些服务要么需要绑定信用卡后在提供免费服务要么干脆就是收费的,本系列文章旨在作为教学案例,使用付费服务显然不合适。
现在我找到一个有道翻译的 API,暂时能提供免费的翻译供大家作为学习使用。该 API 的 URL 示例:
http://fanyi.youdao.com/translate?&doctype=json&type=ZH_CNENA&i=你好
请求这个 API 我们可以得到这样的 JSON 返回:
{ "type": "ZH_CN2EN",
"errorCode": 0,
"elapsedTime": 1,
"translateResult": [[{"src": "你好","tgt": "hello"}]]
}
这个 API 参数的 i
表示要翻译的文本。type
表示翻译的原文语言和目标语言:
type 代码:
ZH_CN2EN 中文 » 英语
ZH_CN2JA 中文 » 日语
ZH_CN2KR 中文 » 韩语
EN2ZH_CN 英语 » 中文
JA2ZH_CN 日语 » 中文
KR2ZH_CN 韩语 » 中文
我们现在用这个 API 来构建用户动态的翻译功能。这个功能我们需要对 API 发起网络请求,Python 的 requests
包能帮助我们完成,现在先来安装它。
(venv) $ pip install requests
新增一个 app/translate.py
模块来放置动态翻译相关代码:
# app/translate.py
import requests
from flask_babel import _
def translate(text, source_language, dest_language):
source_language = 'ZH_CN' if source_language.upper()[:2] == 'ZH' else source_language.upper()
dest_language = 'ZH_CN' if dest_language.upper()[:2] == 'ZH' else dest_language.upper()
type = '{}2{}'.format(source_language, dest_language)
url = "http://fanyi.youdao.com/translate?&doctype=json&type={}&i={}".format(type, text)
r = requests.get(url)
if r.status_code != 200:
return _('Error: the translation service failed.')
return {'text': r.json()['translateResult'][0][0]['tgt']}
该函数以需要翻译的文本、源语言和目标语言为参数,并返回翻译后文本的 JSON。
requests.get()
方法返回一个响应对象,它包含了服务提供的所有细节。我首先需要检查和确认状态码是 200,这是成功请求的代码。如果我得到任何其他代码,我就知道发生了错误,所以在这种情况下,我返回一个错误字符串。 如果状态码是 200,那么响应的主体就有一个带有翻译的 JSON 编码字符串。
下面你可以看到一个 Python Shell 会话,我演示了如何使用新的 translate()
函数:
>>> from app.translate import translate
>>> translate('hello world', 'en', 'zh')
{'text': '你好,世界'}
来自服务器的 Ajax
我们先来理一下思路,当用户单击动态下方显示的翻译链接时,将通过浏览器的 Javascript 代码,向服务器(指的是本应用的服务器而非翻译 API)发出异步 HTTP 请求,服务器接收相应把数据返回到浏览器,浏览器获得数据后通过 javascript 代码把数据跟新到页面相应位置。Javascript 部分我将在下一节中向你展示。
异步(Ajax)请求类似于我在应用中创建的路由和视图函数,唯一的区别是它不返回 HTML 或重定向,而是返回数据,格式为 XML 或更常见的 JSON。你可以在下面看到翻译视图函数,该函数调用有道翻译的 API,然后返回 JSON 格式的翻译文本。
# app\main\routes.py
from flask import jsonify
@main_routes.route('/translate', methods=['POST'])
@login_required
def translate_text():
text = request.form['text']
source_language = request.form['source_language']
dest_language = request.form['dest_language']
result= translate(text, source_language, dest_language)
return jsonify(result)
如你所见,相当简单。 我以 POST
请求的形式实现了这条路由。 关于什么时候使用 GET
或 POST
(或者其他请求方法),真的没有绝对的规则。request.form
属性是 Flask 保存 POST
请求所携带的表单数据的参数。
在这个函数中我们调用上一节中的 translate()
函数,translate()
函数通过请求提交的三个参数 text
, source_language
, dest_language
,请求 翻译 API 返回一个保护翻译结果的字典。该字典作为参数传递给 Flask
的 jsonify()
函数,该函数将字典转换为 JSON 格式的有效载荷。jsonify()
返回的值是将被发送回客户端的 HTTP 响应。
来自客户端的 Ajax
现在我们能够通过向 '/translate'
URL 发送 POST
请求来得到翻译后的 JSON 文本。这里我先编写一个 Javascript 脚本演示一遍。
我们打开应用后,在控制台界面测试下列代码:
$.post('/translate', {
text: 'hello',
source_language: 'ZH',
dest_language: 'EN'
}).done(function(response) {
console.log(response);
}).fail(function() {
console.log('Could not contact server.');
})
// 输出结果:
{text: "hello"}text: "hello"__proto__: Object
这里的 $
符号是 jQuery 库提供的函数的名称。因为这个库被 Bootstrap
使用,所以使用 Flask-Bootstrap
后这个库也会被引入进来。上面代码我向 '/translate'
URL 发送了 POST
请求,附带上了 text
、source_language
、dest_language
三个参数,然后把得到的响应 JSON 打印出来。
当然,我们实际使用时候还需要考虑获取需翻译的文本,然后把响应内容显示到 HTML 文件合适的位置中。
为了能获取到用户动态所在的元素,我们给用户动态的 <span>
标签加上 id
参数:
<span id="post-{{ post.id }}">{{ post.body }}</span>
我的设想是用户点击“翻译”链接后,原来放置翻译链接的 <span>
标签内容会被替换为翻译后的内容,所以这个 <span>
我也会加上 id
:
<span id="translation-{{ post.id }}">
<a href="#">{{ _('Translate') }}</a>
</span>
现在 post
的 <span>
的 id
为 post-<id>
的形式, 比如,第一个 post
的 <span>
的 id
为 post-1
,第二个则是 post-2
。同理,翻译的 <span>
也是这种形式。
有了 id
我们就可以通过它来获取目标元素,我们使用 jQuery 获取元素的方法:
$('#post-1')
$('#translation-1')
#
标签代表 jQuery 选择器的一部分,用于通过 id
获取元素。
通过 text()
方法,我们还能获取 <span>
的内容:
$('#post-1').text()
下一步是编写一个可以完成所有翻译工作的函数。该函数将利用输入和输出 DOM 节点以及源语言和目标语言,向服务器发出携带必须的三个参数的异步请求,并在服务器响应后用翻译后的文本替换翻译链接。我们来实现:
# app\templates\base.html
{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{{ moment.lang(g.locale) }}
<script>
function translate(sourceElem, destElem, sourceLang, destLang) {
$.post('/translate', {
text: $(sourceElem).text(),
source_language: sourceLang,
dest_language: destLang
}).done(function(response) {
$(destElem).text(response['text']);
}).fail(function() {
$(destElem).text("{{ _('Error: Could not contact server.') }}");;
});
}
</script>
{% endblock %}
sourceElem
和 destElem
两个参数填写能查找到目标元素的选择器字符串,比如,目标元素为 id
为 1 的 post
,那么 sourceElem
就是 "#post-1"
,destElem
就是 "#translation-1"
。source_language
跟 dest_language
分别表示原语言和目标语言的代号。
当 POST
请求成功后 .done
函数块内的代码会被执行,请求返回的结果会被放在 response
变量中;如果请求失败,.fail
函数块内的代码就会被执行。
我们最后要做的就是通过用户点击翻译链接来触发具有正确参数的 translate()
函数。有若干方法可以做到这一点,我的方法是将该函数的调用嵌入链接的 href
属性中:
# app\templates\_post.html
{% if post.language and post.language[:2] != g.locale[:2] %}
<br><br>
<span id="translation-{{ post.id }}">
<a href="javascript:translate(
'#post-{{ post.id }}',
'#translation-{{ post.id }}',
'{{ post.language }}',
'{{ g.locale }}');"
>
{{ _('Translate') }}
</a>
</span>
{% endif %}
链接的 href
元素除了接收 URL 之外也还可以接受任何 JavaScript 代码,只要它带有 javascript:
前缀的话,那么这是一种方便的方式来调用翻译函数。
因为这个链接将在客户端请求页面时在服务器端渲染,所以我可以使用 {{ }}
表达式来为函数生成四个参数。每条用户动态都有自己的翻译链接,以及其唯一生成的参数。post-<ID>
和 translation-<ID>
需要渲染具体的 ID,它们都需要在被使用时加上 #
前缀。
现在实时翻译功能已经完成:
本文源码:https://github.com/SingleDiego/Flask-Tutorial-Source-Code/tree/SingleDiego-patch-14
网友评论