Ajax
這將是國際化和本地化的最後一篇文章,咱們將會盡所能使得 microblog 應用程序對非英語用戶可用和更加友好。javascript
不知道你們平時有沒有見過網站上有一個 「翻譯」 的連接,點擊後會把翻譯後的內容在旁邊顯示給用戶,這些連接觸發一個實時自動翻譯的內容。谷歌顯示這個 「翻譯」 連接是爲了可以顯示外國語言的搜索結果。Facebook 顯示它爲了可以翻譯 blog 內容。今天咱們將要添加一樣的功能的 「翻譯」 連接到咱們的 microblog!html
客戶端 VS 服務器端
在傳統的沿用至今的服務器端的模型中,有一個客戶端(用戶的瀏覽器)發送請求到咱們的服務器上。一個請求可以簡單地請求一個頁面,像當你點擊 「你的信息」 連接,或者它可以讓咱們執行一個動做,像當用戶編輯他的或者她的用戶信息而且點擊提交的按鈕。在這兩種類型的請求中服務器經過發送一個新的網頁到客戶端,直接或經過發出一個重定向的請求來完成此次請求。客戶端接着使用新頁代替目前的頁面。這個循環就會重複只要用戶停留在咱們的網頁上。咱們叫這種模式爲服務器端,由於服務器作了全部的工做而客戶端只是在它們收到頁面的時候顯示出來。java
在客戶端模式中,咱們有一個網頁瀏覽器,再次發送請求到服務器上。服務器會像服務器端模式同樣迴應一個網頁,可是不是全部的頁面數據都是 HTML,一樣也有代碼,典型的就是用 Javascript 編寫的。一旦客戶端接收到頁面會把它顯示出來而且會執行攜帶的代碼。今後,你有一個活躍的客戶端,能夠作本身的工做,沒有接觸外面的服務器。在嚴格的客戶端,整個應用程序被下載到客戶端在初始頁面請求中,而後應用程序運行在客戶端上不會刷新頁面,只有向服務器獲取或存儲數據。這種類型的應用稱爲 單頁應用 或者 SPAs。python
大多數應用是這兩種模式的結合體。咱們的 microblog 應用程序是一個徹底的服務器端應用,可是如今咱們想要添加些客戶端行爲。爲了實現實時翻譯用戶的 blog 內容,客戶端瀏覽器將會發送一個請求到服務器,可是服務器將會迴應一個翻譯文本並且不須要頁面刷新。客戶端將會動態地插入翻譯到當前頁面。這種技術叫作 Ajax,這是 Asynchronous Javascript and XML 的簡稱。jquery
翻譯用戶生成內容
多虧了 Flask-Babel 咱們如今比較好的支持了多語言。假設咱們能找到願意幫助咱們的翻譯器,咱們能夠在儘量多的語言中發佈咱們的應用程序。數據庫
可是還有一個遺漏問題。由於有不少各類語言的用戶使用系統,那麼用戶發佈的 blog 內容的語言也是多種的。可能不是本語種的用戶不知道內容的含義,若是咱們可以提供一種自動翻譯的服務這種會不會更好?json
這是一個用 Ajax 服務來實現的理想的功能。咱們的首頁能夠顯示不少的 blog,它們中的一些多是不一樣語言的。若是咱們使用傳統的服務器端模式來實現翻譯的話,一個翻譯的請求可能會讓原始頁面被新的只翻譯了選擇的 blog 的頁面替換。在用戶讀完翻譯後,咱們必須點擊回退鍵去獲取 blog 列表。事實上請求一個翻譯並非須要更新所有的頁面,這個功能讓應用程序更好,由於翻譯的文本是動態地插入到原始的文本下面,其餘的內容不會發生變化。所以,今天咱們將會實現咱們的 Ajax 服務。flask
實現實時翻譯須要幾個步驟。首先,咱們須要肯定要翻譯的文本的原語言類型。一旦咱們知道原語言類型咱們也就知道需不須要對一個給定的用戶翻譯,由於咱們也知道這個用戶選擇的語言類型。當翻譯被提供,用戶但願看到它的時候,將會調用 Ajax 服務。最後一步就是客戶端的 javascript 代碼將會動態地把翻譯文本插入到頁面中。windows
肯定 blog 語言
咱們首先的問題就是肯定 blog 撰寫的語言。這不是一門精確的科學,它不會老是可以檢測的語言的類型,因此咱們只能盡最大努力去作。咱們將會使用 guess-language Python 模塊。所以,請安裝這個模塊。針對 Linux 和 Mac OS X 用戶:api
flask/bin/pip install guess-language
針對 Windows 用戶:
flask\Scripts\pip install guess-language
有了這個模塊,咱們將會掃描每一篇 blog 的內容試着猜想它的語言種類。由於咱們不想一遍一遍地掃描同一篇 blog,咱們將會針對每一篇 blog 只作一次,當用戶提交 blog 的時候就去掃描。咱們將會把每一篇 blog 的語言種類存儲在數據庫中。
所以讓咱們開始在咱們的 Post 表中添加一個 language 字段:
class Post(db.Model): __searchable__ = ['body'] id = db.Column(db.Integer, primary_key = True) body = db.Column(db.String(140)) timestamp = db.Column(db.DateTime) user_id = db.Column(db.Integer, db.ForeignKey('user.id')) language = db.Column(db.String(5))
每一次修改數據庫,咱們都須要作一次遷移:
$ ./db_migrate.py New migration saved as microblog/db_repository/versions/005_migration.py Current database version: 5
如今咱們已經在數據庫中有了存儲 blog 內容語言類型的地方,所以讓咱們檢測每個 blog 語言種類:
from guess_language import guessLanguage @app.route('/', methods = ['GET', 'POST']) @app.route('/index', methods = ['GET', 'POST']) @app.route('/index/<int:page>', methods = ['GET', 'POST']) @login_required def index(page = 1): form = PostForm() if form.validate_on_submit(): language = guessLanguage(form.post.data) if language == 'UNKNOWN' or len(language) > 5: language = '' post = Post(body = form.post.data, timestamp = datetime.utcnow(), author = g.user, language = language) db.session.add(post) db.session.commit() flash(gettext('Your post is now live!')) return redirect(url_for('index')) posts = g.user.followed_posts().paginate(page, POSTS_PER_PAGE, False) return render_template('index.html', title = 'Home', form = form, posts = posts)
若是語言猜想不能工做或者返回一個非預期的結果,咱們會在數據庫中存儲一個空的字符串。
顯示 「翻譯」 連接
接下來一步就是在 blog 旁邊顯示 「翻譯」 連接(文件 app/templates/posts.html):
{% if post.language != None and post.language != '' and post.language != g.locale %} <div><a href="#">{{ _('Translate') }}</a></div> {% endif %}
這個連接須要咱們添加一個新的翻譯文本, 「翻譯」(‘Translate’) 是須要被包含在翻譯文件裏面,這裏須要執行前面一章介紹的更新翻譯文本的流程。
咱們如今還不清楚如何觸發這個翻譯,所以如今連接不會作任何事情。
翻譯服務
在咱們的應用可以使用實時翻譯以前,咱們須要找到一個可用的服務。
如今有不少可用的翻譯服務,可是不少是須要收費的。
兩個主流的翻譯服務是 Google Translate 和 Microsoft Translator。二者都是有償服務,但微軟提供的是免費的入門級的 API。在過去,谷歌提供了一個免費的翻譯服務,但已不存在。這使咱們很容易選擇翻譯服務。
使用 Microsoft Translator 服務
爲了使用 Microsoft Translator,這裏有一些流程須要完成:
- 應用的開發者須要在 Azure Marketplace 上註冊 Microsoft Translator app。這裏能夠選擇服務級別(免費的選項在最下面)。
- 接着開發者須要 註冊應用。註冊應用將會得到客戶端 ID 以及客戶端密鑰代碼,這些用於發送請求的一部分。
一旦註冊部分完成,接下來處理請求翻譯的步驟以下:
這聽起來很複雜,所以若是不須要關注細節的話,這裏有一個作了不少「髒」工做而且把文本翻譯成別的語言的函數(文件 app/translate.py):
try: import httplib # Python 2 except ImportError: import http.client as httplib # Python 3 try: from urllib import urlencode # Python 2 except ImportError: from urllib.parse import urlencode # Python 3 import json from flask.ext.babel import gettext from config import MS_TRANSLATOR_CLIENT_ID, MS_TRANSLATOR_CLIENT_SECRET def microsoft_translate(text, sourceLang, destLang): if MS_TRANSLATOR_CLIENT_ID == "" or MS_TRANSLATOR_CLIENT_SECRET == "": return gettext('Error: translation service not configured.') try: # get access token params = urlencode({ 'client_id': MS_TRANSLATOR_CLIENT_ID, 'client_secret': MS_TRANSLATOR_CLIENT_SECRET, 'scope': 'http://api.microsofttranslator.com', 'grant_type': 'client_credentials'}) conn = httplib.HTTPSConnection("datamarket.accesscontrol.windows.net") conn.request("POST", "/v2/OAuth2-13", params) response = json.loads (conn.getresponse().read()) token = response[u'access_token'] # translate conn = httplib.HTTPConnection('api.microsofttranslator.com') params = {'appId': 'Bearer ' + token, 'from': sourceLang, 'to': destLang, 'text': text.encode("utf-8")} conn.request("GET", '/V2/Ajax.svc/Translate?' + urlencode(params)) response = json.loads("{\"response\":" + conn.getresponse().read().decode('utf-8') + "}") return response["response"] except: return gettext('Error: Unexpected error.')
這個函數從咱們的配置文件中導入了兩個新值,id 和密鑰代碼(文件 config.py):
# microsoft translation service MS_TRANSLATOR_CLIENT_ID = '' # enter your MS translator app id here MS_TRANSLATOR_CLIENT_SECRET = '' # enter your MS translator app secret here
上面的 ID 和密鑰代碼是須要本身去申請,步驟上面已經講了。即便你只但願測試應用程序,你也能免費地註冊這項服務。
由於咱們又添加了新的文本,這些也是須要翻譯的,請從新運行 tr_update.py,poedit 和 tr_compile.py 來更新翻譯的文件。
讓咱們翻譯一些文本
所以咱們該怎樣使用翻譯服務了?這實際上很簡單。這是例子:
$ flask/bin/python Python 2.6.8 (unknown, Jun 9 2012, 11:30:32) >>> from app import translate >>> translate.microsoft_translate('Hi, how are you today?', 'en', 'es') u'¿Hola, cómo estás hoy?'
服務器上的 Ajax
如今咱們能夠在多種語言之間翻譯文本,所以咱們準備把這個功能整合到咱們應用程序中。
當用戶點擊 blog 旁的 「翻譯」 連接的時候,會有一個 Ajax 調用發送到咱們服務器上。咱們將看看這個調用是如何生產的, 如今讓咱們集中精力實現服務器端的 Ajax 調用。
服務器上的 Ajax 服務像一個常規的視圖函數,不一樣的是不返回一個 HTML 頁面或者重定向,它返回的是數據,典型的格式化成 XML 或者 JSON。由於 JSON 對 Javascript 比較友好,咱們將使用這種格式(文件 app/views.py):
from flask import jsonify from translate import microsoft_translate @app.route('/translate', methods = ['POST']) @login_required def translate(): return jsonify({ 'text': microsoft_translate( request.form['text'], request.form['sourceLang'], request.form['destLang']) })
這裏沒有多少新內容。這個路由處理一個攜帶要翻譯的文本以及原語言類型和要翻譯的語言類型的 POST 請求。由於這是個 POST 請求,咱們獲取的是輸入到 HTML 表單中的數據,請直接使用 request.form 字典。咱們用這些數據調用咱們的一個翻譯函數,一旦咱們獲取翻譯的文本就用 Flask 的 jsonify 函數把它轉換成 JSON。客戶端看到的這個請求響應的數據相似這個格式:
{ "text": "<translated text goes here>" }
客戶端上的 Ajax
如今咱們須要從網頁瀏覽器上調用 Ajax 視圖函數,由於咱們須要回到 post.html 子模板來完成咱們最後的工做。
首先咱們須要在模版中加入一個有惟一 id 的 span 元素,以便咱們在 DOM 中能夠找到它而且替換成翻譯的文本(文件 app/templates/post.html):
<p><strong><span id="post{{post.id}}">{{post.body}}</span></strong></p>
一樣,咱們須要給一個 「翻譯」 連接一個惟一的 id,以保證一旦翻譯顯示咱們能隱藏這個連接:
<div><span id="translation{{post.id}}"><a href="#">{{ _('Translate') }}</a></span></div>
爲了作出一個漂亮的而且對用戶友好的功能,咱們將會加入一個動態的圖片,開始的時候是隱藏的,僅僅出現當翻譯服務運行在服務器上,一樣也有惟一的 id:
<img id="loading{{post.id}}" style="display: none" src="/static/img/loading.gif">
目前咱們有一個名爲 post<id> 的元素,它包含要翻譯的文本,還有一個名爲 translation<id> 的元素,它包含一個 「翻譯」 連接可是不久就會被翻譯後的文本代替,也有一個 id 爲 loading<id> 的圖片,它將會在翻譯服務工做的時候顯示。
如今咱們須要在 「連接」 連接點擊的時候觸發 Ajax。與直接從連接上觸發調用相反,咱們將會建立一個 Javascript 函數,這個函數作了全部工做,由於咱們有一些事情在那裏作而且不但願在每一個模板中重複代碼。讓咱們添加一個對這個函數的調用當 「翻譯」 連接被點擊的時候:
<a href="javascript:translate('{{post.language}}', '{{g.locale}}', '#post{{post.id}}', '#translation{{post.id}}', '#loading{{post.id}}');">{{ _('Translate') }}</a>
變量看起來有些多,可是函數調用很簡單。假設有一篇 id 爲 23,使用西班牙語寫的 blog,用戶想要翻譯成英語。這個函數的調用以下:
translate('es', 'en', '#post23', '#translation23', '#loading23')
最後咱們須要實現的 translate(),咱們將不會在 post.html 子模板中編寫這個函數,由於每一篇 blog 內容會有些重複。咱們將會在基礎模版中實現這個函數,下面就是這個函數(文件 app/templates/base.html):
<script> function translate(sourceLang, destLang, sourceId, destId, loadingId) { $(destId).hide(); $(loadingId).show(); $.post('/translate', { text: $(sourceId).text(), sourceLang: sourceLang, destLang: destLang }).done(function(translated) { $(destId).text(translated['text']) $(loadingId).hide(); $(destId).show(); }).fail(function() { $(destId).text("{{ _('Error: Could not contact server.') }}"); $(loadingId).hide(); $(destId).show(); }); } </script>
這段代碼依賴於 jQuery,須要詳細瞭解上述幾個函數的話,請查看 jQuery。
結束語
近來當使用 Flask-WhooshAlchemy 爲全文搜索的時候,會有一些數據庫的警告。在下一章的時候,咱們針對這個問題來說講 Flask 應用程序的調試技術。