Try   HackMD

Flask實作_ext_17_Flask_babel_多語系

tags: flask flask_ext python babel

Flask實作_ext_16_Flask_babel我們介紹了應用在不同語系時的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使用。

範例

透過說明,我們絕對不可能理解究竟多語系在做些什麼事,所以我們要來實際操作一次,就可以從很抽象的說明變成原來如此我懂了。
『>>>』代表命令列直接執行

>>>number_of_pages=100 >>>ngettext(u'%(num)s page', u'%(num)s pages', number_of_pages) '100 pages'

上面的案例是用來了解ngettext的應用,如果我們網頁的頁數有10頁,那就呈現複數的pages,如果只有1頁,那就呈現單數的page。

接著,我們要來建置一個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,內容很簡單,如下:

<!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.執行指令

pybabel extract -F babel.cfg -o messages.pot .

最後的『.』很重要,不是打錯,一定要有

如果有使用lazy_gettext的話,則改執行如下指令

pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot .

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

參數說明:

  • -F:配置檔
  • -O:輸出檔案名稱
  • .代表當前目錄

執行之後可以看的到多了一個messages.pot的檔案

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

內容如下:

# 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.建立語系翻譯

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.編譯

pybabel compile -d translations

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

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

失敗了?這是因為這form的生成並不在請求上下文中生成,所以我們無法直接使用gettext來處理,必需透過lazy_gettext在需要的時候再做翻譯,讓我們來調整一下表單,如下:

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,如下:

<!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執行下面的語法:

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.產生文件

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

pybabel compile -d translations

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

總結

我們成功的在幾個簡單的步驟下設計出多語系的版面,並且了解到在非上下文的一個請求中必需使用lazy_gettext才有辦法成功取得翻譯,而且在jinja2模板上我們也可以透過{{_('要翻譯的文字')}}這樣的表達式來讓系統知道這邊也有一個需要翻譯的文字。

後續你只需要初始化不同語系的文件出來將翻譯資料補上,你的系統就可以直接滿足更多的多國語系應用,但是不要誤會一件事,資料庫內所帶出來的資料並不會翻譯。

延伸閱讀

一直無法成功取得翻譯後的資料,這時候可以先確認路徑取得是否正常,flask_babel在取得翻譯文件的資料夾路徑的邏輯如下:

  1. app.config['BABEL_TRANSLATION_DIRECTORIES']
    • 如果沒有設置就取translations
  2. flask.root_path
    • 初始化flask的地方