帶你認識 flask 國際化和本地化
- 2019 年 11 月 24 日
- 筆記
01
flask-babel 簡介
你猜對了,Flask-Babel正是用於簡化翻譯工作的。可以使用pip命令安裝它:
(venv) $ pip install flask-babel
Flask-Babel的初始化與之前的插件類似:
app/__init__.py
: Flask-Babel實例
# ...from flask_babel import Babel app = Flask(__name__)# ...babel = Babel(app)
作為本章的一部分,我將向你展示如何將應用翻譯成西班牙語,因為我碰巧會這種語言。我當然也可以與翻譯機制合作來支持其他語言。為了跟蹤支持的語言列表,我將添加一個配置變量:
config.py:支持的語言列表
class Config(object): # ... LANGUAGES = ['en', 'es']
我為本應用使用雙字母代碼來表示語言種類,但如果你需要更具體,還可以添加國家代碼。例如,你可以使用en-US
,en-GB
和en-CA
來支持美國、英國和加拿大的英語以示區分。
Babel
實例提供了一個localeselector
裝飾器。為每個請求調用裝飾器函數以選擇用於該請求的語言:
app/__init__.py
:選擇最匹配的語言
from flask import request # ... @babel.localeselectordef get_locale(): return request.accept_languages.best_match(app.config['LANGUAGES'])
這裡我使用了Flask中request
對象的屬性accept_languages
。 request
對象提供了一個高級接口,用於處理客戶端發送的帶Accept-Language頭部的請求。該頭部指定了客戶端語言和區域設置首選項。該頭部的內容可以在瀏覽器的首選項頁面中配置,默認情況下通常從計算機操作系統的語言設置中導入。大多數人甚至不知道存在這樣的設置,但是這是有用的,因為應用可以根據每個語言的權重,提供優選語言的列表。為了滿足你的好奇心,下面是一個複雜的Accept-Languages
頭部的例子:
Accept-Language: da, en-gb;q=0.8, en;q=0.7
這表示丹麥語(da
)是首選語言(默認權重= 1.0),其次是英式英語(en-GB
),其權重為0.8,最後是通用英語(en
),權重為0.7
要選擇最佳語言,你需要將客戶請求的語言列表與應用支持的語言進行比較,並使用客戶端提供的權重,查找最佳語言。這樣做的邏輯有點複雜,但它已經全部封裝在best_match()
方法中了,該方法將應用提供的語言列表作為參數並返回最佳選擇
02
標記文本以在Python源代碼中執行翻譯
好吧,壞消息來了。支持多語言的常規流程是在源代碼中標記所有需要翻譯的文本。文本標記後,Flask-Babel將掃描所有文件,並使用gettext工具將這些文本提取到單獨的翻譯文件中。不幸的是,這是一個繁瑣的任務,並且是啟用翻譯的必要條件。
我將在這裡向你展示標記操作的幾個示例,你也可以從下載包獲取本章完整的更改集,當然,也可以直接查看GitHub的頁面。
為翻譯而標記文本的方式是將它們封裝在一個函數調用中,該函數調用為_()
,僅僅是一個下劃線。最簡單的情況是源代碼中出現的字符串。下面是一個flask()
語句的例子:
from flask_babel import _# ...flash(_('Your post is now live!'))
_()
函數用於原始語言文本(在這種情況下是英文)的封裝。該函數將使用由localeselector
裝飾器裝飾的選擇函數,來為給定客戶端查找正確的翻譯語言。 _()
函數隨後返回翻譯後的文本,在本處,翻譯後的文本將成為flash()
的參數。
但是不可能每個情況都這麼簡單,試想如下的另一個flash()
調用:
flash('User {} not found.'.format(username))
該文本具有一個安插在靜態文本中間的動態組件。 _()
函數的語法支持這種類型的文本,但它基於舊版本的字符串替換語法:
flash(_('User %(username)s not found.', username=username))
還有更難處理的情況。有些字符串文字並非是在發生請求時分配的,比如在應用啟動時。因此在評估這些文本時,無法知道要使用哪種語言。一個例子是與表單字段相關的標籤,處理這些文本的唯一解決方案是找到一種方法來延遲對字符串的評估,直到它被使用,比如有實際上的請求發生了。Flask-Babel提供了一個稱為lazy_gettext()
的_()
函數的延遲評估的版本:
from flask_babel import lazy_gettext as _l class LoginForm(FlaskForm): username = StringField(_l('Username'), validators=[DataRequired()]) # ...
在這裡,我正在導入的這個翻譯函數被重命名為_l()
,以使其看起來與原始的_()
相似。這個新函數將文本包裝在一個特殊的對象中,這個對象會在稍後的字符串使用時觸發翻譯
Flask-Login插件只要將用戶重定向到登錄頁面,就會閃現消息。此消息為英文,來自插件本身。為了確保這個消息也能被翻譯,我將重寫默認消息,並用_l()
函數進行延遲處理:
login = LoginManager(app)login.login_view = 'login'login.login_message = _l('Please log in to access this page.')
03
標記文本以在模板中翻譯
在前面的章節中,你已經看到了如何在Python源代碼中標記可翻譯的文本,但這只是該過程的一部分,因為模板文件也包含文本。 _()
函數也可以在模板中使用,所以過程非常相似。例如,參考來自404.html的這段HTML代碼:
<h1>File Not Found</h1>
啟用翻譯之後的版本是:
<h1>{{ _('File Not Found') }}</h1>
請注意,除了用_()
包裝文本外,還需要添加{{...}}
來強制_()
進行翻譯,而不是將其視為模板中的文本字面量。
對於具有動態組件的更複雜的短語,也可以使用參數:
<h1>{{ _('Hi, %(username)s!', username=current_user.username) }}</h1>
_post.html中的一個特別棘手的案例讓我花了一些時間才理順:
{% set user_link %} <a href="{{ url_for('user', username=post.author.username) }}"> {{ post.author.username }} </a>{% endset %}{{ _('%(username)s said %(when)s', username=user_link, when=moment(post.timestamp).fromNow()) }}
這裡的問題是我希望username
是一個超鏈接,指向用戶的個人主頁,而不僅僅是名字,所以我必須使用set
和endset
模板指令創建一個名為user_link
的中間變量 ,然後將其作為參數傳遞給翻譯函數
正如我上面提到的,你可以下載該版本的應用,其中的Python源代碼和模板中都已被標記成可翻譯文本
04
提取文本進行翻譯
一旦應用所有_()
和_l()
都到位了,你可以使用pybabel
命令將它們提取到一個*.pot文件中,該文件代表可移植對象模板*。這是一個文本文件,其中包含所有標記為需要翻譯的文本。這個文件的目的是作為一個模板來為每種語言創建翻譯文件。
提取過程需要一個小型配置文件,告訴pybabel哪些文件應該被掃描以獲得可翻譯的文本。下面你可以看到我為這個應用創建的babel.cfg:
babel.cfg:PyBabel配置文件
[python: app/**.py][jinja2: app/templates/**.html]extensions=jinja2.ext.autoescape,jinja2.ext.with_
前兩行分別定義了Python和Jinja2模板文件的文件名匹配模式。第三行定義了Jinja2模板引擎提供的兩個擴展,以幫助Flask-Babel正確解析模板文件。
可以使用以下命令來將所有文本提取到* .pot *文件:
(venv) $ pybabel extract -F babel.cfg -k _l -o messages.pot .
pybabel extract
命令讀取-F
選項中給出的配置文件,然後從命令給出的目錄(當前目錄或本處的.
)掃描與配置的源匹配的目錄中的所有代碼和模板文件。默認情況下,pybabel
將查找_()
以作為文本標記,但我也使用了重命名為_l()
的延遲版本,所以我需要用-k _l
來告訴該工具也要查找它 。 -o
選項提供輸出文件的名稱
我應該注意,messages.pot文件不需要合併到項目中。這是一個只要再次運行上面的命令,就可以在需要時輕鬆地重新生成的文件。因此,不需要將該文件提交到源代碼管理
05
生成語言目錄
該過程的下一步是在除了原始語言(在本例中為英語)之外,為每種語言創建一份翻譯。我要從添加西班牙語(語言代碼es
)開始,所以這樣做的命令是:
(venv) $ pybabel init -i messages.pot -d app/translations -l escreating catalog app/translations/es/LC_MESSAGES/messages.po based on messages.pot
pybabel init
命令將messages.pot文件作為輸入,並將語言目錄寫入-d
選項中指定的目錄中,-l
選項中指定的是翻譯語言。我將在app/translations目錄中安裝所有翻譯,因為這是Flask-Babel默認提取翻譯文件的地方。該命令將在該目錄內為西班牙數據文件創建一個es子目錄。特別是,將會有一個名為app/translations/es/LC_MESSAGES/messages.po的新文件,是需要翻譯的文件路徑。
如果你想支持其他語言,只需要各自的語言代碼重複上述命令,就能使得每種語言都有一個包含messages.po文件的存儲庫。
在每個語言存儲庫中創建的messages.po
文件使用的格式是語言翻譯的事實標準,使用的格式為gettext。以下是西班牙語messages.po開頭的若干行:
# Spanish translations for PROJECT.# Copyright (C) 2017 ORGANIZATION# This file is distributed under the same license as the PROJECT project.# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.#msgid ""msgstr """Project-Id-Version: PROJECT VERSIONn""Report-Msgid-Bugs-To: EMAIL@ADDRESSn""POT-Creation-Date: 2017-09-29 23:23-0700n""PO-Revision-Date: 2017-09-29 23:25-0700n""Last-Translator: FULL NAME <EMAIL@ADDRESS>n""Language: esn""Language-Team: es <[email protected]>n""Plural-Forms: nplurals=2; plural=(n != 1)n""MIME-Version: 1.0n""Content-Type: text/plain; charset=utf-8n""Content-Transfer-Encoding: 8bitn""Generated-By: Babel 2.5.1n" #: app/email.py:21msgid "[Microblog] Reset Your Password"msgstr "" #: app/forms.py:12 app/forms.py:19 app/forms.py:50msgid "Username"msgstr "" #: app/forms.py:13 app/forms.py:21 app/forms.py:43msgid "Password"msgstr ""
如果你跳過首段,可以看到後面的是從_()
和_l()
調用中提取的字符串列表。對每個文本,都會展示其在應用中的引用位置。然後,msgid
行包含原始語言的文本,後面的msgstr
行包含一個空字符串。這些空字符串需要被編輯,以使目標語言中的文本內容被填充。
有很多翻譯應用程序與.po
文件一起工作。如果你擅長編輯文本文件,量少的時候也就罷了,但如果你正在處理大型項目,可能會推薦使用專門的編輯器。最流行的翻譯應用程序是開源的poedit,可用於所有主流操作系統。如果你熟悉vim,那麼po.vim 插件會提供一些鍵值映射,使得處理這些文件更加輕鬆。
在添加翻譯後,你可以在下面看到一部分西班牙語messages.po:
#: app/email.py:21msgid "[Microblog] Reset Your Password"msgstr "[Microblog] Nueva Contraseña" #: app/forms.py:12 app/forms.py:19 app/forms.py:50msgid "Username"msgstr "Nombre de usuario" #: app/forms.py:13 app/forms.py:21 app/forms.py:43msgid "Password"msgstr "Contraseña"
本章的下載包中包含所有翻譯,此文件當然也在其中,所以你不必擔心這部分的翻譯工作。
messages.po文件是一種用於翻譯的源文件。當你想開始使用這些翻譯後的文本時,這個文件需要被編譯成一種格式,這種格式在運行時可以被應用程序使用。要編譯應用程序的所有翻譯,可以使用pybabel compile
命令,如下所示:
(venv) $ pybabel compile -d app/translationscompiling catalog app/translations/es/LC_MESSAGES/messages.po toapp/translations/es/LC_MESSAGES/messages.mo
此操作在每個語言存儲庫中的messages.po旁邊添加messages.mo文件。 .mo文件是Flask-Babel將用於為應用程序加載翻譯的文件。
在為西班牙語或任何其他添加到項目中的語言創建messages.mo文件之後,可以在應用中使用這些語言。如果你想查看應用程序以西班牙語顯示的方式,則可以在Web瀏覽器中編輯語言配置,以將西班牙語作為首選語言。對Chrome,這是設置頁面的高級部分:

如果你不想更改瀏覽器設置,另一種方法是通過使localeselector
函數始終返回一種語言來強制實現。對西班牙語,你可以這樣做:
app/__init__.py
:選擇最佳語言
@babel.localeselectordef get_locale(): # return request.accept_languages.best_match(app.config['LANGUAGES']) return 'es'
使用為西班牙語配置的瀏覽器運行該應用或返回es
的localeselector
函數,將使所有文本在使用該應用時顯示為西班牙文
06
更新翻譯
處理翻譯時的一個常見情況是,即使翻譯文件不完整,你也可能要開始使用翻譯文件。這是非常好的,你可以編譯一個不完整的messages.po文件,任何可用的翻譯都將被使用,而任何缺失的部分將使用原始語言。隨後,你可以繼續處理翻譯並再次編譯,以便在取得進展時更新messages.mo文件。
如果在添加_()
包裝器時錯過了一些文本,則會出現另一種常見情況。在這種情況下,你會發現你錯過的那些文本將保持為英文,因為Flask-Babel對他們一無所知。當你檢測到這種情況時,會想要將其用_()
或_l()
包裝,然後執行更新過程,這包括兩個步驟:
(venv) $ pybabel extract -F babel.cfg -k _l -o messages.pot .(venv) $ pybabel update -i messages.pot -d app/translations
extract
命令與我之前執行的命令相同,但現在它會生成messages.pot的新版本,其中包含所有以前的文本以及最近用_()
或_l()
包裝的文本。 update
調用採用新的messages.pot
文件並將其合併到與項目相關的所有messages.po文件中。這將是一個智能合併,其中任何現有的文本將被單獨保留,而只有在messages.pot中添加或刪除的條目才會受到影響
messages.po文件更新後,你就可以繼續新的測試了,再次編譯它,以便對應用生效
07
翻譯日期時間
現在,我已經為Python代碼和模板中的所有文本提供了完整的西班牙語翻譯,但是如果你使用西班牙語運行應用並且是一個很好的觀察者,那麼會注意到還有一些內容以英文顯示。我指的是由Flask-Moment和moment.js生成的時間戳,顯然這些時間戳並未包含在翻譯工作中,因為這些包生成的文本都不是應用程序源代碼或模板的一部分
moment.js庫確實支持本地化和國際化,所以我需要做的就是配置適當的語言。Flask-Babel通過get_locale()
函數返回給定請求的語言和語言環境,所以我要做的就是將語言環境添加到g
對象,以便我可以從基礎模板中訪問它:
app/routes.py:存儲選擇的語言到flask.g中
# ...from flask import gfrom flask_babel import get_locale # ... @app.before_requestdef before_request(): # ... g.locale = str(get_locale())
Flask-Babel的get_locale()
函數返回一個本地語言對象,但我只想獲得語言代碼,可以通過將該對象轉換為字符串來獲取語言代碼。現在我有了g.locale
,可以從基礎模板中訪問它,並以正確的語言配置moment.js:
app/templates/base.html:為moment.js設置本地語言
...{% block scripts %} {{ super() }} {{ moment.include_moment() }} {{ moment.lang(g.locale) }}{% endblock %}
現在所有的日期和時間都與文本使用相同的語言了。你可以在下面看到西班牙語的外觀:

08
命令行增強
你可能會同意我的看法,pybabel命令有點長,難以記憶。我將利用這個機會向你展示如何創建與flask
命令集成的自定義命令。到目前為止,你已經看到我使用Flask-Migrate擴展提供的flask run
、flask shell
和幾個flask db
子命令。將應用特定的命令添加到flask
實際上也很容易。所以我現在要做的就是創建一些簡單的命令,並用這個應用特有的參數觸發pybabel
命令。我要添加的命令是:
flask translate init LANG
用於添加新語言flask translate update
用於更新所有語言存儲庫flask translate compile
用於編譯所有語言存儲庫
babel export
步驟不會設置為一個命令,因為生成messages.pot文件始終是運行init
或update
命令的先決條件,因此這些命令的執行將會生成翻譯模板文件作為臨時文件。
Flask依賴Click進行所有命令行操作。像translate
這樣的命令是幾個子命令的根,它們是通過app.cli.group()
裝飾器創建的。我將把這些命令放在一個名為app/cli.py的新模塊中:
app/cli.py:翻譯命令組
from app import app @app.cli.group()def translate(): """Translation and localization commands.""" pass
該命令的名稱來自被裝飾函數的名稱,並且幫助消息來自文檔字符串。由於這是一個父命令,它的存在只為子命令提供基礎,函數本身不需要執行任何操作。
update
和compile
很容易實現,因為它們沒有任何參數:
app/cli.py:更新子命令和編譯子命令:
import os # ... @translate.command()def update(): """Update all languages.""" if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'): raise RuntimeError('extract command failed') if os.system('pybabel update -i messages.pot -d app/translations'): raise RuntimeError('update command failed') os.remove('messages.pot') @translate.command()def compile(): """Compile all languages.""" if os.system('pybabel compile -d app/translations'): raise RuntimeError('compile command failed')
請注意,這些函數的裝飾器是如何從translate
父函數派生的。這似乎令人困惑,因為translate()
是一個函數,但它是Click構建命令組的標準方式。與translate()
函數相同,這些函數的文檔字符串在--help
輸出中用作幫助消息。
你可以看到,對於所有命令,運行它們並確保返回值為零(這意味着命令沒有返回任何錯誤)。如果命令錯誤,那麼我會引發一個RuntimeError
,這會導致腳本停止。 update()
函數在同一個命令中結合了extract
和update
步驟,如果一切都成功的話,它會在更新完成後刪除messages.pot文件,因為當再次需要這個文件時,可以很容易地重新生成
init
命令將新的語言代碼作為參數。這是其執行流程:
app/cli.py:Init子命令
import click @translate.command()@click.argument('lang')def init(lang): """Initialize a new language.""" if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'): raise RuntimeError('extract command failed') if os.system( 'pybabel init -i messages.pot -d app/translations -l ' + lang): raise RuntimeError('init command failed') os.remove('messages.pot')
該命令使用@click.argument
裝飾器來定義語言代碼。Click將命令中提供的值作為參數傳遞給處理函數,然後將該參數併入到init
命令中
啟用這些命令的最後一步是導入它們,以便註冊命令。我決定在頂級目錄的microblog.py文件中執行此操作:
microblog.py:註冊命令
from app import cli
這裡我唯一需要做的就是導入新的cli.py模塊,不需要做任何事情,因為導入操作會導致命令裝飾器運行並註冊命令
此時,運行flask --help
將列出translate
命令作為選項。 flask translate --help
將顯示我定義的三個子命令:
(venv) $ flask translate --helpUsage: flask translate [OPTIONS] COMMAND [ARGS]... Translation and localization commands. Options: --help Show this message and exit. Commands: compile Compile all languages. init Initialize a new language. update Update all languages.
所以現在工作流程就簡便多了,而且不需要記住長而複雜的命令。要添加新的語言,請使用:
(venv) $ flask translate init <language-code>
在更改_()
和_l()
語言標記後更新所有語言:
(venv) $ flask translate update
在更新翻譯文件後編譯所有語言:
(venv) $ flask translate compile