pybabelとJinja2でwebapp2アプリケーションを多言語化する


pybabelとJinja2で多言語対応のwebapp2アプリケーションを作ってGAEへデプロイする手順。

- webapp2: Python製Webアプリフレームワーク。
	- https://webapp2.readthedocs.io/en/latest/

- Jinja2: Python製テンプレートエンジン。
	- http://jinja.pocoo.org/docs/2.9/
- pybabel(Babel): 翻訳箇所のロケール別カタログを管理するツール。紛らわしいが、JSコンパイラの[Babel](https://babeljs.io/)ではない。
	- http://babel.pocoo.org/en/latest/cmdline.html

### 環境

- OS X 10.12.3
- python 2.7.12
- pip 9.0.1
- pybabel 2.3.4
- デプロイ環境: GAE


### ディレクトリ構成


最終的にはこうなる。

```
$ tree
.
├── app.yaml
├── appengine_config.py
├── appengine_config.pyc
├── babel.cfg
├── lib
│   ├── Babel-2.3.4.dist-info
│   ├── babel
│   ├── pytz
│   └── pytz-2016.10.dist-info
├── locale
│   ├── ja_JP
│   │   └── LC_MESSAGES
│   │       ├── messages.mo
│   │       └── messages.po
│   └── message.pot
├── main.py
├── main.pyc
├── requirements.txt
└── templates
    └── index.html

33 directories, 1395 files
```

参考のためリポジトリを作成した。

- https://github.com/satzz/webapp2-i18n-example


### アプリケーション作成

```
$ mkdir webapp2-example && cd webapp2-example
```

### Babel設定
webapp2 i18n拡張が使うBabelのライブラリをプロジェクト内にインストールする

```requirements.txt 
Babel==2.3.4
```

```
$ pip install -t lib -r requirements.txt 
Collecting Babel==2.3.4 (from -r requirements.txt (line 1))
  Using cached Babel-2.3.4-py2.py3-none-any.whl
Collecting pytz>=0a (from Babel==2.3.4->-r requirements.txt (line 1))
  Using cached pytz-2016.10-py2.py3-none-any.whl
Installing collected packages: pytz, Babel
Successfully installed Babel-2.3.4 pytz-2016.10
```

```appengine_config.py
from google.appengine.ext import vendor
vendor.add('lib')
```

### GAE設定

```app.yaml 
application: webapp2-example
version: 1
runtime: python27
api_version: 1
threadsafe: yes

handlers:
- url: .*
  script: main.app

libraries:
- name: webapp2
  version: "2.5.2"
- name: jinja2
  version: latest
```

### アプリケーション
ロケールの取得はどうやってもよいが、ここではリクエストパラメータから読むようにする

```main.py
import webapp2

from webapp2_extras import i18n
import os

import jinja2
JINJA_ENVIRONMENT = jinja2.Environment(
    loader=jinja2.FileSystemLoader(os.path.dirname(__file__)),
    extensions=['jinja2.ext.i18n', 'jinja2.ext.autoescape'],
    autoescape=True)
JINJA_ENVIRONMENT.install_gettext_translations(i18n)

class HelloWorldHandler(webapp2.RequestHandler):
    def get(self):
        locale = self.request.GET.get('locale', 'en_US')
        i18n.get_i18n().set_locale(locale)
        print(locale)

        template_values = {}
        template = JINJA_ENVIRONMENT.get_template('templates/index.html')
        self.response.write(template.render(template_values))


app = webapp2.WSGIApplication([
    ('/', HelloWorldHandler),
], debug=True)

def main():
    app.run()

if __name__ == '__main__':
    main()

```

### テンプレート
```
$ mkdir templates
```

プレースホルダーを使うケースを書いてみる。

- New style gettext
	- http://jinja.pocoo.org/docs/2.9/extensions/#newstyle-gettext


`_`は`gettext`のエイリアスになるのでどちらを使ってもよい。

- Jinja2 i18n
	- http://jinja.pocoo.org/docs/2.9/templates/#i18n



```templates/index.html
<html>
  <body>
    {{ _("Hello, %(username)s", username='satzz') }}
  </body>
</html>
```

### ローカルでの動作確認
まだ多言語化していないが、いったんこの段階でGAE開発サーバーで動作確認する。

- Google Cloud SDK Documentation
	- https://cloud.google.com/sdk/docs/
- Using the Local Development Server
	- https://cloud.google.com/appengine/docs/standard/python/tools/using-local-server

開発サーバー起動


```
$ dev_appserver.py . 
INFO     2017-03-17 06:07:29,597 sdk_update_checker.py:229] Checking for updates to the SDK.
INFO     2017-03-17 06:07:30,185 api_server.py:204] Starting API server at: http://localhost:63035
INFO     2017-03-17 06:07:30,189 dispatcher.py:197] Starting module "default" running at: http://localhost:8080
INFO     2017-03-17 06:07:30,192 admin_server.py:118] Starting admin server at: http://localhost:8000

```
moduleが起動しているhttp://localhost:8080 へブラウザアクセス


![Screen Shot 2017-03-17 at 14.27.33.png](https://qiita-image-store.s3.amazonaws.com/0/281/e4be8e44-e636-b752-264b-5e35e3c9a4a7.png "Screen Shot 2017-03-17 at 14.27.33.png")


### GAEでの動作確認
i18nから少し逸れるがGAEでも動作確認しておく。

- https://console.cloud.google.com

`app.yaml`に書いたのと同じ名前でプロジェクト作成する。

![Screen Shot 2017-03-17 at 15.08.51.png](https://qiita-image-store.s3.amazonaws.com/0/281/165dd631-e1d2-2c2b-2226-8fa8b913c45f.png "Screen Shot 2017-03-17 at 15.08.51.png")

アプリケーション作成

- https://cloud.google.com/sdk/gcloud/reference/app/create


![Screen Shot 2017-03-17 at 15.11.23.png](https://qiita-image-store.s3.amazonaws.com/0/281/0fe1474e-c8ad-14be-965e-4d94cbdfa806.png "Screen Shot 2017-03-17 at 15.11.23.png")


![Screen Shot 2017-03-17 at 15.14.42.png](https://qiita-image-store.s3.amazonaws.com/0/281/45a85760-f29a-a494-98bc-7bd88338cc13.png "Screen Shot 2017-03-17 at 15.14.42.png")

インストールしたGoogleAppEngineLauncherを開いてローカルで作ったアプリケーションを指定。Application IDは空でok


![Screen Shot 2017-03-17 at 15.13.17.png](https://qiita-image-store.s3.amazonaws.com/0/281/bbe32695-b4cf-0e09-dd21-170836330d97.png "Screen Shot 2017-03-17 at 15.13.17.png")

追加を確認（2行目）
![Screen Shot 2017-03-17 at 15.13.39.png](https://qiita-image-store.s3.amazonaws.com/0/281/4a351889-eec5-be0f-ac19-45bc410b8065.png "Screen Shot 2017-03-17 at 15.13.39.png")

Cmd+Dでデプロイしてログに`Deployment successful`と出るのを待つ。
![Screen Shot 2017-03-17 at 15.14.20.png](https://qiita-image-store.s3.amazonaws.com/0/281/78018409-e94d-228c-806a-d845974a6b5a.png "Screen Shot 2017-03-17 at 15.14.20.png")

- https://webapp2-example.appspot.com/

で動作確認できる。

### ロケール追加
ここから本題のi18n。カタログを作成するためのpybabelコマンドをインストールする。

```
$ pip install babel
Collecting babel
  Using cached Babel-2.3.4-py2.py3-none-any.whl
Requirement already satisfied: pytz>=0a in /System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python (from babel)
Installing collected packages: babel
Successfully installed babel-2.3.4
```

多言語化対象などを設定

- Extraction Method Mapping and Configuration
	- http://babel.pocoo.org/en/latest/messages.html#extraction-method-mapping-and-configuration

```babel.cfg 
[jinja2: templates/**.html]
encoding = utf-8
```

```
$ mkdir locale
```

カタログ作成の流れは

- Jinja2 template(html) -> POT(PO Template) -> PO(Portable Object) -> MO(Machine Object)

となる。

- Introduction
	- http://babel.pocoo.org/en/latest/messages.html#introduction


Jinja2テンプレートをもとにPOTを作成する。

```
$ pybabel extract -F ./babel.cfg -o ./locale/message.pot .

extracting messages from templates/index.html (encoding="utf-8")
writing PO template file to ./locale/message.pot
```

POTを開くと、msgidとそれがどこで使われているのかが自動でリストされている。このPOTはテンプレートなのでいじらなくてもよい。

```locale/message.pot
#: templates/index.html:3
#, python-format
msgid "Hello, %(username)s"
msgstr ""
```

POTをもとにロケールPOを作成する。(PO作成済の場合は`pybabel update`)

```
$ pybabel init -l ja_JP -d ./locale -i ./locale/message.pot
creating catalog ./locale/ja_JP/LC_MESSAGES/messages.po based on ./locale/message.pot
```
ここでできたPOファイルを編集する。プレースホルダーを使う場合はこんな感じ。

```locale/ja_JP/LC_MESSAGES/messages.po
msgid "Hello, %(username)s"
msgstr "%(username)sさん、こんにちは"
```

コンパイルする。作成されるMOファイルはバイナリファイルになる。


```
$ pybabel compile -f -d ./locale
compiling catalog ./locale/ja_JP/LC_MESSAGES/messages.po to ./locale/ja_JP/LC_MESSAGES/messages.mo
```

リクエストパラメータにロケールをつけてアクセスみる。

- http://localhost:8080/?locale=ja_JP



![Screen Shot 2017-03-17 at 14.35.30.png](https://qiita-image-store.s3.amazonaws.com/0/281/12e3cded-9f4c-361e-262b-38e57dc511ee.png "Screen Shot 2017-03-17 at 14.35.30.png")


再度デプロイするとGAEでも挙動が確認できる。

- https://webapp2-example.appspot.com/?locale=ja_JP

この後は以下を繰り返してカタログを育てていく。

- 翻訳箇所やmsgidが更新 -> `pybabel extract`でPOT更新
- POTが更新 -> `pybabel update`でPO更新
- ロケール追加 -> `pybabel init`でPO作成
- POが作成・更新 -> `pybabel compile`でMO作成・更新