Django 對於 internationalization 與 localization 已提供成熟的解決方案,官方 文件 也寫得十分詳細。
而 Flask 作為一個輕量化的 Web Framework ,並沒有內建 i18n & l10n 的功能,不過只要安裝 Flask-Babel 也可以做到一樣的事情,剛好整合過程也十分簡單,特此紀錄一篇。
本文環境
- Python 3.6.5
- Flask 1.0.2
- Flask-Babel 0.12.2
專案結構
以下為本文的專案結構,用以模擬 1 個很簡單的網站,最後完成會長這樣子:
├── app.py
├── babel.cfg
├── locales
│ ├── en
│ │ └── LC_MESSAGES
│ │ ├── messages.mo
│ │ └── messages.po
│ └── zh_TW
│ └── LC_MESSAGES
│ ├── messages.mo
│ └── messages.po
└── templates
└── index.html
其中 babel.cfg
與 locales/
是本文的主角。
babel.cfg
存放著 Babel 的設定,讓 Babel
套件知道如何從檔案中取得所有需要的字串,讓我們能夠將這些字串交給翻譯人員進行翻譯。
locales
則用以存放翻譯完成後的 messages.po
檔,以及經過 Babel
編譯後的 messages.mo
檔,供 Flask 載入使用。
從頭來過
首先,建立個資料夾存放 Flask 專案,並進入該資料夾中:
$ mkdir i18n-demo; cd i18n-demo
接著新增 1 個檔案 app.py
$ touch app.py
假設此網站支援 英文(en) 與 繁體中文(zh_TW) 2 種語系,因此需要 1 個資料夾存放 2 種語系的翻譯字串,Babel 預設用 translations
作為資料夾名稱(而本文用 locales
作為資料夾名稱),另外該資料夾底下的子資料夾必須用語系名稱作為資料夾名稱,每個語系底下也都必須有個名稱為 LC_MESSAGES
的資料夾,如此才能讓 Babel 載入相對應的語系。
因此用以下指令一次新增 2 個語系的資料夾:
$ mkdir -pv ./i18n/{en,zh_TW}/LC_MESSAGES/
p.s. 語系名稱請不要含有 -
字元,該字元會導致 Babel 執行錯誤(因此本文用 zh_TW
取代 zh-TW
作為語系名稱)
簡單的 app.py
以下是 app.py 的內容:
# -*- coding: utf-8 -*-
from flask import Flask
from flask import request
from flask import g
from babel.support import Translations
from flask import Flask
from flask import render_template
from flask_babel import Babel
def create_app():
flask_app = Flask(__name__)
flask_app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'locales'
return flask_app
app = create_app()
babel = Babel(app)
@babel.localeselector
def get_locale():
return getattr(g, 'LOCALE', 'en')
@app.route('/<locale>', methods=['GET'])
def index(locale):
if locale not in ('en', 'zh_TW'):
locale = 'en'
g.LOCALE = locale
request.babel_translations = Translations.load('locales', [locale])
return render_template('index.html')
由於上述 Flask APP 用 index.html
作為樣板,因此新增 1 個 templates
資料夾存放 index.html
樣板:
$ mkdir templates
$ touch templates/index.html
接著在 index.html
中放入以下內容:
{{ _('Hello World!') }}
{% raw %} {{ }} {% endraw %}
是 Jinja2 語法,而 _('Hello World!')
為我們多國語系的字串,其中底線 _
其實是一個名稱為 _
的函數,該函數對應的真正函數為 gettext ,多國語系就是透過此函數在執行時將字串換成翻譯後的字串。
至於 Flask 的運作就不多加贅述,以下僅解說相對重要的幾行程式:
flask_app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'locales'
由於本文並非用 translations
作為存放多國語系翻譯的資料夾名稱,因此在 config 中將 BABEL_TRANSLATION_DIRECTORIES
設定改為 locales
代表多國語系翻譯的資料夾名稱改為 locales
。
顯示多國語系字串時,我們需要告訴 Babel 用何種語系顯示給使用者,因此需要實作 babel.localeselector
,讓 Babel 在執行時知道使用何種語系(以下程式也可以是情況改為讀取資料庫等方式):
@babel.localeselector
def get_locale():
return getattr(g, 'LOCALE', 'en')
上述的程式則是將需要顯示的語系放在 Flask 的 g 中,如果沒有 g.LOCALE
這個值,我們就用 en
語系顯示給使用者。
也因此,我們必須將語系放到 g
中:
@app.route('/<locale>', methods=['GET'])
def index(locale):
if locale not in ('en', 'zh_TW'):
locale = 'en'
g.LOCALE = locale
request.babel_translations = Translations.load('locales', [locale])
return render_template('index.html')
因此在 index 這個 view 接到 request 時,我們會用 g.LOCALE = locale
將語系資料先存到 g
中,後續 Babel 自然就能夠取得語系資料。
知道要用何種語系之後, Babel 仍須載入對應的翻譯,而 Flask-Babel
預設會讀取 request.babel_translations
中的翻譯,因此用 Translations.load('locales', [locale])
事先為其載入,如此一來就整合好多國語系的功能了。
執行 app.py
上述 Flask APP 僅包含 1 個 view ,可用以下指令執行:
$ env FLASK_APP=app.py flask run
執行之後,可以用瀏覽器瀏覽以下 2 個網址看不同語系的字串:
- http://127.0.0.1:5000/en
- http://127.0.0.1:5000/zh_TW
此時會發現,上述 2 個網址顯示的字串都是 Hello World!
,是還未翻譯前的字串。這是因為尚未將各語系需要的 messages.mo
檔編譯好的緣故。
接下來進行 messages.mo
的編譯。
編譯 messages.mo
編譯 messages.mo
的過程大致上分為 3 步驟:
- 擷取程式碼中的字串
- 翻譯字串
- 將翻譯編譯成
messages.mo
檔
擷取程式碼中的字串
字串並不是手動ㄧ句一句整理而成的,而是透過程式擷取,擷取之前需要透過設定檔,告訴 Babel 哪些檔案可以擷取字串,所以我們新增 Babel 的設定檔 babel.cfg
,並填入以下內容:
[jinja2: templates/**.html] extensions=jinja2.ext.autoescape,jinja2.ext.with_ keywords=_,gettext,ngettext,lazy_gettext
上述的內容指的是從 templates/**.html
中用 jinja2
解析方式取得字串,使用 extensions jinja2.ext.autoescape
與 jinja2.ext.with_
,並且將 _
, gettext
, ngettext
, lazy_gettext
等關鍵字中的字串擷取出來。
所以 {% raw %} {{ _('Hello World!') }} {% endraw %}
中的 Hello World!
將會被擷取。
設定檔完成之後,可以用以下指令開始擷取字串:
$ pybabel extract -F babel.cfg -o messages.pot .
上述指令會將所有字串擷取到 messages.pot
中,以下為 messages.pot
的內容,可以看到 msgid "Hello World!"
被擷取成功:
# Translations template for PROJECT.
# Copyright (C) 2018 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2018-12-08 16:28+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
#: templates/index.html:1
msgid "Hello World!"
msgstr ""
翻譯字串
有了 messages.pot
之後,用以下指令分別將字串再編成各語系資料夾內的 messages.po
:
$ pybabel init -l zh_TW -d ./locales -i messages.pot
$ pybabel init -l en -d ./locales -i messages.pot
如果是初次新增 messages.po
請用 pybabel init
,如果是後續的字串更新則使用 pybabel update
:
$ pybabel update -l zh_TW -d ./locales -i messages.pot
$ pybabel update -l en -d ./locales -i messages.pot
成功後可以在各語系的 LC_MESSAGES
資料夾內發現 messages.po
檔,接著用 POEDIT 打開這些檔案,開始進行翻譯:
編譯 messages.mo
完成翻譯後,最後就能編譯 messages.mo
囉!
用以下指令進行編譯:
pybabel compile -f -d ./locales
成功執行後,就可以在各語系的 LC_MESSAGES
資料夾內發現 messages.mo
檔。
接著打開 http://127.0.0.1:5000/zh_TW 就會出現翻譯字串囉!
結語
基本上 Python 的 i18n 實作都是大同小異,只要實際做過一次,換到什麼 Framework 應該都能夠很快上手。
以上, Happy Coding!
References
http://babel.pocoo.org/en/latest/index.html
https://docs.python.org/3/library/gettext.html#module-gettext
https://pythonhosted.org/Flask-Babel/