文章轉自 :https://github.com/WapeYang/The-Flask-Mega-Tutorial/blob/master/i18n.rst
感謝原作者的付出
轉載時間爲:2014-05-06
國際化和本地化
今天的文章的主題是國際化和本地化,通常簡稱 I18n 和 L10n。我們想要我們的 microblog 應用程序被儘可能多的用戶使用,因爲我們不能忘記有許多人是不是講英文的,或者會說英文,但是更願意講本國語言。
爲了使得我們的應用程序便於外國訪問者,我們將要使用 Flask-Babel 擴展,它提供了一種簡單使用的框架用來把應用程序翻譯成不用的應用。
如果你還沒有安裝 Flask-Babel,現在是時候安裝。對於 Linux 和 Mac 用戶:
flask/bin/pip install flask-babel
對於 Windows 用戶:
flask\Scripts\pip install flask-babel
配置
Flask-Babel 可以簡單地通過創建 Babel 類的一個實例並且傳入 Flask 應用對象給它來初始化(文件 app/__init__.py):
from flask.ext.babel import Babel babel = Babel(app)
我們也需要決定我們將要提供翻譯的語言種類。現在我們將要開始一個西班牙版本,因爲我們有一個西語的翻譯器在手上,以後添加其它語言的版本也是很容易的。支持語言的列表被添加到配置文件中(文件 config.py):
# -*- coding: utf-8 -*- # ... # available languages LANGUAGES = { 'en': 'English', 'es': 'Español' }
LANGUAGES 字典有可用語言代碼的鍵,以及可打印的語言名稱作爲值。我們使用短的語言代碼,但是要指明語言和地域的話,也可能使用長代碼。比如,如果我們要支持美國和英國英語的話,我們的字典裏面可以有 'en-US' 和 'en-GB'。
注意因爲 Español 有一個外來字符,我們必須在 Python 源代碼文件頂部中添加 coding 聲明,告訴 Python 解釋器我們是使用 UTF-8 編碼 不是 ASCII,因爲 ASCII 編碼缺少 ñ 字符。
接下來的一個配置就是我們需要一個 Babel 用於決定使用哪種語言的函數(文件 app/views.py):
from app import babel from config import LANGUAGES @babel.localeselector def get_locale(): return request.accept_languages.best_match(LANGUAGES.keys())
這個函數有一個 localeselector 裝飾器,它被調用在請求之前爲了當產生響應的時候給我們機會選擇使用的語言。現在爲止我們做的是簡單的,我們只要讀取瀏覽器發送的 HTTP 請求中的 Accept-Languages 頭並且從我們支持的語言列表中選擇最匹配的語言。這個過程實際上也相當簡單,best_match 方法爲我們做了所有工作。
Accept-Languages 頭在大多數瀏覽器上被默認配置成操作系統層的所選擇的語言,但是所有的瀏覽器給我們機會選擇其它的語言。用戶可以提供語言列表,每一個都有權重。作爲例子,下面是複雜的 Accept-Languages 頭:
Accept-Language: da, en-gb;q=0.8, en;q=0.7
上面的頭信息表示最佳的語言是丹麥語(默認權重爲 1),接着是英國英語(權重是 0.8)以及最後一個選項是通用英語(權重是 0.7)。
最後一項配置是我們需要一個 Babel 配置文件,它告訴 Babel 在我們代碼和模板中的哪裏去尋找翻譯的文本(文件 babel.cfg):
[python: **.py] [jinja2: **/templates/**.html] extensions=jinja2.ext.autoescape,jinja2.ext.with_
最前面的兩行告訴 Babel 我們的 Python 代碼以及模版的文件名模式。第三行是告訴 Babel 啓用一些擴展使得它能夠在 Jinja2 模版中尋找翻譯的文本。
標記翻譯文本
現在到了這個任務最繁瑣的地方。我們需要檢查所有的代碼和模版標記所有需要翻譯的英文文本以便 Babel 能夠找到它們。例如,看看從 after_login 函數中代碼片段:
if resp.email is None or resp.email == "": flash('Invalid login. Please try again.') redirect(url_for('login'))
這裏有一個閃現消息需要翻譯。爲了使得 Babel 知道這個文本,只要把這個字符串傳入到 gettext 函數:
from flask.ext.babel import gettext # ... if resp.email is None or resp.email == "": flash(gettext('Invalid login. Please try again.')) redirect(url_for('login'))
在模板中我們必須做一些類似的工作,但是我們使用 _() 來簡化 gettext()。比如,在我們基礎模版中的鏈接的文本 Home:
<li><a href="{{ url_for('index') }}">Home</a></li>
能夠被標記翻譯如下:
<li><a href="{{ url_for('index') }}">{{ _('Home') }}</a></li>
不幸地是,不是所有我們要翻譯的文本像上面一樣的簡單。作爲一個例子,考慮下來自我們的 post.html 子模板中的如下的片段:
<p><a href="{{url_for('user', nickname = post.author.nickname)}}">{{post.author.nickname}}</a> said {{momentjs(post.timestamp).fromNow()}}:</p>
這裏我們要翻譯的結構式:“<nickname> 說 <when>:”。一種嘗試就是隻標記翻譯 “說”,因爲我們不確定在這一句中姓名以及時間組合的次序在所有語言中是一樣的。正確的辦法是標記整個語句並且使用對姓名與時間使用佔位符,這樣翻譯器會在必要的時候改變次序。更復的雜情況是,名稱裏面內嵌了一個超鏈接。
gettext 函數是支持使用 %(name)s 語法佔位符,這也是我們最好的解決辦法。下面是一個類似情況的佔位符的例子:
gettext('Hello, %(name)s', name = user.nickname)
回到我們的例子,這裏是怎樣標記文本翻譯:
{% autoescape false %} <p>{{ _('%(nickname)s said %(when)s:', nickname = '<a href="%s">%s</a>' % (url_for('user', nickname = post.author.nickname), post.author.nickname), when = momentjs(post.timestamp).fromNow()) }}</p> {% endautoescape %}
因爲我們在 nickname 佔位符上放入了 HTML,我們需要關閉自動轉義。但是關閉自動轉義是一個很冒險的行爲,渲染用戶的輸入並且不進行轉義是很不安全的。
賦值給 when 佔位符的文本是安全的,因爲它是我們的 momentjs() 封裝函數生成的文本。但是 nickname 佔位符的文本是來自我們User 模型中的 nickname 字段,這是來自數據庫中並且完全由用戶輸入。如果用戶在這個字段中輸入特定意義的 HTML 或者 Javascript 腳本,我們沒有對這些進行轉義,可能我們會執行這些代碼,這也許是一個後門。我們不能允許這樣的事情,因此我們需要避免這種情況。
最有效的解決方案就是對 nickname 字段中使用的字符進行嚴格的限制。我們開始創建一個函數轉換一個無效的 nickname 成一個有效(文件 app/models.py):
import re class User(db.Model): #... @staticmethod def make_valid_nickname(nickname): return re.sub('[^a-zA-Z0-9_\.]', '', nickname)
這裏我們只是從 nickname 字段中移除非字母,數字,.,_ 的字符。
當一個用戶在頁面註冊,我們從 OpenID 提供商接收到他或者她的 nickname,因此我們必須確保轉換這個 nickname (文件app/views.py):
@oid.after_login def after_login(resp): #... nickname = User.make_valid_nickname(nickname) nickname = User.make_unique_nickname(nickname) user = User(nickname = nickname, email = resp.email, role = ROLE_USER) #...
同樣在編輯用戶信息的表單中,那裏可以修改 nickname,我們需要在那裏加強驗證不允許非法字符(文件 app/forms.py):
class EditForm(Form): #... def validate(self): if not Form.validate(self): return False if self.nickname.data == self.original_nickname: return True if self.nickname.data != User.make_valid_nickname(self.nickname.data): self.nickname.errors.append(gettext('This nickname has invalid characters. Please use letters, numbers, dots and underscores only.')) return False user = User.query.filter_by(nickname = self.nickname.data).first() if user != None: self.nickname.errors.append(gettext('This nickname is already in use. Please choose another one.')) return False return True
提取文本翻譯
這裏我不會列舉所有需要翻譯的代碼和模版。感興趣的讀者可以檢查 這裏。
因此讓我們假設我們已經發現所有文本並且把它們放入了 gettext() 或者 _() 調用中。那現在要幹什麼了?
現在我們運行 pybabel 提取文本到單獨的文件中:
flask/bin/pybabel extract -F babel.cfg -o messages.pot app
Windows 用戶使用這個命令:
flask\Scripts\pybabel extract -F babel.cfg -o messages.pot app
pybabel extract 命令會讀取給定的配置文件,接着掃描在給定參數(在我們的例子中爲 app)目錄下的所有的代碼和模版,當它發現標記翻譯的文本就會把它拷貝到 messages.pot 文件。
messages.pot 文件是一個模板文件,其中包含所有需要翻譯的文本。這個文件是用來作爲一種生成語言文件的模型。
生成一個語言目錄
這個過程的下一步就是爲一個新語言創建翻譯。我們說過我們要做西班牙版本(語言代碼爲 es),因此這是添加西班牙語到我們應用程序的命令:
flask/bin/pybabel init -i messages.pot -d app/translations -l es
pybabel init 命令把 .pot 文件作爲輸入,生成一個新語言目錄,以 -d 選項指定的目錄爲新語言的目錄,以 -l 指定的語言爲想要翻譯成的語言類型。默認情況下,Babel 希望翻譯的語言在與模版相同目錄級別的 translations 文件夾中,因此我們把它們放在這裏。
在你運行上述命令後,一個目錄 app/translations/es 是創建了。在它裏面有另一個名爲 LC_MESSAGES 的目錄,在它裏面有一個messages.po 文件。
下面就是翻譯成西班牙語的截圖:
一旦文本翻譯完成並且保存成 messages.po 文件,還有另外一個來發布這些文本:
flask/bin/pybabel compile -d app/translations
pybabel compile 這一步會讀取 .po 文件的內容並且會在相同的目錄下生成一個名爲 .mo 的編譯的版本。這個文件以一種優化的格式包含了翻譯的文本,應用程序可以更高效地使用它。
翻譯已經準備好被使用了。爲了驗證它你可以修改瀏覽器上的語言設置讓西班牙語爲最佳語言,或者你可以直接修改 get_locale 函數(文件 app/views.py):
@babel.localeselector def get_locale(): return "es" #request.accept_languages.best_match(LANGUAGES.keys())
更新翻譯
如果 messages.po 文件不完整會發生些什麼,比如某些文本忘記了翻譯?不會發生什麼異常,應用程序會運行的好好的,只是這些文本不會被翻譯繼續顯示成英文。
如果在我們的代碼或者模版中丟失了一些英文文本的話會發生些什麼?任何沒有放入 gettext() 或者 _() 的字符串都不會在翻譯文件中,因此 Babel 不會感知這些,它們依然保持英文。一旦我們把丟失的文本添加進 gettext(),運行如下命令可以升級翻譯文件:
flask/bin/pybabel extract -F babel.cfg -o messages.pot app flask/bin/pybabel update -i messages.pot -d app/translations
extract 命令與前面用過的是一樣的,它只是生成一個更新的 messages.pot 文件,文件裏添加了新的文本。update 調用會把更新的文件加入到所有翻譯的語言中。
一旦每一個語言文件夾的 messages.po 文件被更新了,我們可以運行 poedit 查看更新的文本,接着重複 pybabel compile 命令使得新的文本對應用程序可用。
翻譯 moment.js
目前爲止,代碼以及模版中的文本都已經翻譯成西班牙版本,可以運行應用程序看看。
但是此時我們會發現時間戳仍然是英語的。我們使用的渲染日期和時間的 moment.js 沒有並通知到需要一個不同語言的版本。
從 moment.js 的 文檔 我們發現 moment.js 有多語言版本可用。因此我們下載了西班牙語版本的 moment.js,並把它放在 static/js文件夾中命名爲 moment-es.min.js。我們將會按照這種方式,把不同語言的 moment.js 以 moment-<language>.min.js 形式存入static/js 中,以便以後我們可以自動地選擇正確的版本。
爲了能夠在模版中加載正確語言版本的 moment.js,我們需要把語言的代碼加入到 Flask 全局變量,跟記錄登錄用戶是相同的方式(文件 app/views.py):
@app.before_request def before_request(): g.user = current_user if g.user.is_authenticated(): g.user.last_seen = datetime.utcnow() db.session.add(g.user) db.session.commit() g.search_form = SearchForm() g.locale = get_locale()
接着需要在基礎模版中修改引用 moment.js 的代碼(文件 app/templates/base.html):
{% if g.locale != 'en' %} <script src="/static/js/moment-{{g.locale}}.min.js"></script> {% endif %}
惰性求值
當我們繼續把玩着我們的西班牙語版本的應用程序,發現了一個問題。當我們登出並且嘗試重新登錄的時候,出現一個英語的閃現消息 “請登錄後訪問本頁。” 。這是哪裏的消息?我們並沒有加入這個消息,它是 Flask-Login 擴展做的。
Flask-Login 允許用戶配置這個消息,因此我們要充分利用不會改變消息只是翻譯這一點。因此,我們進行第一次嘗試(文件app/__init__.py):
from flask.ext.babel import gettext lm.login_message = gettext('Please log in to access this page.')
但是它並不工作。gettext 必須在請求的內容中使用纔會產生翻譯信息。如果我們在請求之外的地方使用,它不會翻譯只會給我們英語版本的默認文本。
幸好,Flask-Babel 提供另外一個函數 lazy_gettext,它不會像 gettext() 和 _() 一樣立即翻譯,相反它會推遲翻譯直到字符串實際上被使用的時候纔會翻譯。這個函數就可以應用到這裏:
from flask.ext.babel import lazy_gettext lm.login_message = lazy_gettext('Please log in to access this page.')
最後,當使用 lazy_gettext 的時候,pybabel extract 命令需要一個額外的 -k 的選項指明是 lazy_gettext 函數:
flask/bin/pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot app
接下來的事情就跟上面更新翻譯一樣。依次 pybabel update,poedit,pybabel compile。
快捷方式
因爲 pybabel 命令是又長又難記,我們可以編寫一個快速的以及乾淨的小腳本來替代前面我們使用的命令。
第一個腳本就是添加語言到翻譯目錄(文件 tr_init.py):
#!flask/bin/python import os import sys if sys.platform == 'wn32': pybabel = 'flask\\Scripts\\pybabel' else: pybabel = 'flask/bin/pybabel' if len(sys.argv) != 2: print "usage: tr_init <language-code>" sys.exit(1) os.system(pybabel + ' extract -F babel.cfg -k lazy_gettext -o messages.pot app') os.system(pybabel + ' init -i messages.pot -d app/translations -l ' + sys.argv[1]) os.unlink('messages.pot')
接着一個腳本就是更新語言目錄(文件 tr_update.py):
#!flask/bin/python import os import sys if sys.platform == 'wn32': pybabel = 'flask\\Scripts\\pybabel' else: pybabel = 'flask/bin/pybabel' os.system(pybabel + ' extract -F babel.cfg -k lazy_gettext -o messages.pot app') os.system(pybabel + ' update -i messages.pot -d app/translations') os.unlink('messages.pot')
最後,就是編譯目錄的腳本(文件 tr_compile.py):
#!flask/bin/python import os import sys if sys.platform == 'wn32': pybabel = 'flask\\Scripts\\pybabel' else: pybabel = 'flask/bin/pybabel' os.system(pybabel + ' compile -d app/translations')
這些腳本會讓工作變得更加簡單些!
結束語
今天我們實現一個網頁應用程序很容易忽略的東西。用戶希望在本地語言下使用,因此必須讓我們的應用程序支持多種語言。
在接下來的文章中,我們將看看可能是國際化和本地化最複雜的方面,就是用戶產生的內容的實時自動翻譯。我們將會利用這個機會給我們的應用程序添加些 Ajax 的魔力。
如果你想要節省時間的話,你可以下載 microblog-0.14.zip。
我希望能在下一章繼續見到各位!