# 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 . ``` ![](https://i.imgur.com/3RCxyyi.png) 參數說明: * -F:配置檔 * -O:輸出檔案名稱 * .代表當前目錄 執行之後可以看的到多了一個`messages.pot`的檔案 ![](https://i.imgur.com/acI0vy4.png) 內容如下: ``` # 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 ``` ![](https://i.imgur.com/EFrBsJ2.png) 執行之後,專案內多了一個`translations`資料夾 ![](https://i.imgur.com/ilSmLWE.png) 打開`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 ``` ![](https://i.imgur.com/FyoZEw8.png) 執行之後,資料夾內多了一個`messages.mo`的檔案 ![](https://i.imgur.com/5msyFsY.png) 到這邊,我們可以來測試一下專案,但是我們發現到沒有翻譯成功? http://127.0.0.1:5000/index ![](https://i.imgur.com/ifT22e0.png) 失敗了?這是因為這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` 調整之後重新執行專案,這時候已經順利的取得翻譯了 ![](https://i.imgur.com/42mewYp.png) #### 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 . ``` ![](https://i.imgur.com/kT3zzsE.png) 執行之後的`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 ``` ![](https://i.imgur.com/ygtnfS0.png) ```= # 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 ``` ![](https://i.imgur.com/akYpKGb.png) 最後再執行一次專案,已經成功的翻譯了 ![](https://i.imgur.com/cmm3nY9.png) ## 總結 我們成功的在幾個簡單的步驟下設計出多語系的版面,並且了解到在非上下文的一個請求中必需使用`lazy_gettext`才有辦法成功取得翻譯,而且在jinja2模板上我們也可以透過`{{_('要翻譯的文字')}}`這樣的表達式來讓系統知道這邊也有一個需要翻譯的文字。 後續你只需要初始化不同語系的文件出來將翻譯資料補上,你的系統就可以直接滿足更多的多國語系應用,但是不要誤會一件事,**資料庫內**所帶出來的資料並不會翻譯。 ## 延伸閱讀 一直無法成功取得翻譯後的資料,這時候可以先確認路徑取得是否正常,`flask_babel`在取得翻譯文件的資料夾路徑的邏輯如下: 1. app.config['BABEL_TRANSLATION_DIRECTORIES'] * 如果沒有設置就取`translations` 2. flask.root_path * 初始化flask的地方 ![](https://i.imgur.com/cExTPwT.png)