# Flask實作_ext_17_Flask_babel_多語系
###### tags: `flask` `flask_ext` `python` `babel`
在[Flask實作_ext_16_Flask_babel](https://hackmd.io/s/Sy0bRGaWX)我們介紹了應用在不同語系時的`datetime`格式化,這篇要介紹的是應用於多語系的文字翻譯。
一間跨國企業的官方網站一定會有著英文、繁體中文、簡體中文....等多國語言,因為你的客戶來自國際,所以你需要讓他們看的懂你的產品,比較笨一點的方法當然可以一個語系一個頁面,但是當你的版有一個小變動的時候你就需要變更多個頁面了,這時候就可以利用`babel`來幫我們處理這部份的問題了。
## 說明
透過`flask_babel`做多語系之流程大致如下:
1. 新增一個配置文件
* `babel.cfg`
2. 執行指令
* `pybabel extract -F babel.cfg -o messages.pot .`
3. 建立語系翻譯
* `pybabel init -i messages.pot -d translations -l zh_TW`
4. 編譯
* `pybabel compile -d translations`
5. 後續有更新
* `pybabel extract -F babel.cfg -o messages.pot .`
6. 重新產生`messages.pot`
* `pybabel update -i messages.pot -d translations`
7. 更新後重新compile
* `pybabel compile -d translations`
需要認識的function有三個:
1. `gettext`
* 標記字串做為翻譯對象
2. `ngettext`
* 基本同`gettext`
* `ngettext(singular, plural, num)``
* 當num為複數的時候則回傳複數單字,但限制上是必需為英文或是只有一種複數形式的語言。
3. `lazy_gettext`
* 理論跟`SQLAlchemy`一樣,在需要的時候再翻譯,多搭配Form使用。
## 範例
透過說明,我們絕對不可能理解究竟多語系在做些什麼事,所以我們要來實際操作一次,就可以從很抽象的說明變成原來如此我懂了。
**『>>>』代表命令列直接執行**
```python=
>>>number_of_pages=100
>>>ngettext(u'%(num)s page', u'%(num)s pages', number_of_pages)
'100 pages'
```
上面的案例是用來了解`ngettext`的應用,如果我們網頁的頁數有10頁,那就呈現複數的pages,如果只有1頁,那就呈現單數的page。
接著,我們要來建置一個Python文件,如下:
```python=
from flask import Flask, render_template
from flask_babel import Babel, lazy_gettext, gettext, ngettext, refresh
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, IntegerField
app = Flask(__name__)
app.config['SECRET_KEY'] = 'development'
app.config['BABEL_DEFAULT_LOCALE'] = 'zh'
app.config['BABEL_DEFAULT_TIMEZONE'] = 'UTC'
babel = Babel(app)
class testForm(FlaskForm):
name = StringField(gettext('name'))
age = IntegerField(gettext('age'))
submit = SubmitField(gettext('submit'))
@app.route('/index')
def index():
refresh()
form = testForm()
return render_template('index.html', form=form)
if __name__ == '__main__':
app.run(debug=True)
```
第1~4行:import需求套件
第6~11行:初始化跟設置套件
第13~16行:建置一個簡單的form,並且利用`gettext`來包住欄位名稱
第18~22行:設置一個路由,並且`refresh`語系。
接著,我們在`templates`內加入一個`index.html`,內容很簡單,如下:
```htmlmixed=
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>{{form.name.label}}{{form.name}}</div>
<div>{{form.age.label}}{{form.age}}</div>
<div>{{form.submit.label}}{{form.submit}}</div>
</body>
</html>
```
### 實作流程
現在,我們要來執行實作多語系的流程:
#### 1.新增一個配置文件
配置文件本身必需跟專案初始化的Python文件置於同一資料夾,文件名稱設置為`babel.cfg`
```
[python: app.py]
[jinja2: **/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_
```
前兩行是讓`babel`知道要翻譯的文件在那,而第三行是應用在`jinja2`模板渲染上,後面我們再回頭說明。
範例上我故意給了檔名`app.py`,避免連一堆venv的Python文件都掃描了,否則一般可能使用`**.py`
#### 2.執行指令
```shell
pybabel extract -F babel.cfg -o messages.pot .
```
>最後的『.』很重要,不是打錯,一定要有
如果有使用`lazy_gettext`的話,則改執行如下指令
```shell
pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot .
```

參數說明:
* -F:配置檔
* -O:輸出檔案名稱
* .代表當前目錄
執行之後可以看的到多了一個`messages.pot`的檔案

內容如下:
```
# 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-06-28 21:54+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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"
#: app.py:15
msgid "name"
msgstr ""
#: app.py:16
msgid "age"
msgstr ""
#: app.py:17
msgid "submig"
msgstr ""
```
可以看到我們設置在form內的三個欄位名稱有順利的被標記到!
#### 3.建立語系翻譯
```shell
pybabel init -i messages.pot -d translations -l zh
```

執行之後,專案內多了一個`translations`資料夾

打開`messages.po`,然後將翻譯輸入相對應的地方
```
# Chinese (Traditional, Taiwan) translations for PROJECT.
# Copyright (C) 2018 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2018-06-28 21:54+0800\n"
"PO-Revision-Date: 2018-06-28 22:08+0800\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: zh_Hant_TW\n"
"Language-Team: zh_Hant_TW <LL@li.org>\n"
"Plural-Forms: nplurals=1; plural=0\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"
#: app.py:15
msgid "name"
msgstr "姓名"
#: app.py:16
msgid "age"
msgstr "年齡"
#: app.py:17
msgid "submig"
msgstr "提交"
```
#### 4.編譯
```shell
pybabel compile -d translations
```

執行之後,資料夾內多了一個`messages.mo`的檔案

到這邊,我們可以來測試一下專案,但是我們發現到沒有翻譯成功?
http://127.0.0.1:5000/index

失敗了?這是因為這form的生成並不在請求上下文中生成,所以我們無法直接使用`gettext`來處理,必需透過`lazy_gettext`在需要的時候再做翻譯,讓我們來調整一下表單,如下:
```python=
class testForm(FlaskForm):
name = StringField(lazy_gettext('name'))
age = IntegerField(lazy_gettext('age'))
submit = SubmitField(lazy_gettext('submit'))
```
第2~4行:調整為`lazy_gettext`
調整之後重新執行專案,這時候已經順利的取得翻譯了

#### 5.後續有更新
在文件中的『`extensions=jinja2.ext.autoescape,jinja2.ext.with_`』用途是應用在模板內的擴展。讓我們來簡單調整一下`index.html`,如下:
```htmlmixed=
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>{{form.name.label}}{{form.name}}</div>
<div>{{form.age.label}}{{form.age}}</div>
<div>{{form.submit.label}}{{form.submit}}</div>
<div>{{ _('Hello World!') }}</div>
</body>
</html>
```
第11行:『_』就代表著`gettext`標記著這是一個要翻譯的部位,等一下記得掃描一下我嘿。
接著在Command執行下面的語法:
```shell
pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot .
```

執行之後的`messages.pot`如下:
```=
# 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-06-30 07:34+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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"
#: app.py:14
msgid "name"
msgstr ""
#: app.py:15
msgid "age"
msgstr ""
#: app.py:16
msgid "submit"
msgstr ""
#: templates/index.html:11
msgid "Hello World!"
msgstr ""
```
第33~34行:新增了『Hello World!』的待翻譯字串
#### 6.產生文件
```shell
pybabel update -i messages.pot -d translations
```

```=
# Chinese translations for PROJECT.
# Copyright (C) 2018 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2018-06-30 07:34+0800\n"
"PO-Revision-Date: 2018-06-30 07:32+0800\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: zh\n"
"Language-Team: zh <LL@li.org>\n"
"Plural-Forms: nplurals=1; plural=0\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"
#: app.py:14
msgid "name"
msgstr "姓名"
#: app.py:15
msgid "age"
msgstr "年齡"
#: app.py:16
msgid "submit"
msgstr "提交"
#: templates/index.html:11
msgid "Hello World!"
msgstr ""
```
在我們執行`update`之後可以看的到,原始有翻譯的部份是沒有被空值覆蓋,僅`Hello World!`是待翻譯的,這簡化我們不少工作。
將`Hello World`的翻譯補上之後,重新`compile`
#### 7.更新後重新compile
```shell
pybabel compile -d translations
```

最後再執行一次專案,已經成功的翻譯了

## 總結
我們成功的在幾個簡單的步驟下設計出多語系的版面,並且了解到在非上下文的一個請求中必需使用`lazy_gettext`才有辦法成功取得翻譯,而且在jinja2模板上我們也可以透過`{{_('要翻譯的文字')}}`這樣的表達式來讓系統知道這邊也有一個需要翻譯的文字。
後續你只需要初始化不同語系的文件出來將翻譯資料補上,你的系統就可以直接滿足更多的多國語系應用,但是不要誤會一件事,**資料庫內**所帶出來的資料並不會翻譯。
## 延伸閱讀
一直無法成功取得翻譯後的資料,這時候可以先確認路徑取得是否正常,`flask_babel`在取得翻譯文件的資料夾路徑的邏輯如下:
1. app.config['BABEL_TRANSLATION_DIRECTORIES']
* 如果沒有設置就取`translations`
2. flask.root_path
* 初始化flask的地方
