Flaskアプリでi18nをしてみた作業メモ

**注意:** これはただのメモです。有用な情報がなくても許してくださぃ。

---

## はじめに

趣味でFlaskのアプリを書いているわけですが、できれば職場でも役立てたいと思ってます。
が、そこで問題になるのが日本語です。
GitHubとかGitLabとかで公開するのには英語の方が良いわけですが、職場で使おうとすると英語だと使っていただけないという悲しい現実が待ち構えているわけです。

そこで、i18nです。たぶん。
i18nは、ご存知のとおりInternationalizationの略(?)です。
ちなみに、ワタクシはHyper Nikki Systemが好きでしたが、公式サイトのドメインの h14m.org はそこから来てるんですよね? (脱線)
ひとまず、i18nってどうやるのか以前から興味はあったので、これを機会に試してみた、というのが本記事の内容となります。

## 準備

この記事は、基本的には [こちらのサイト(英語)][link-1] と [こちらのサイト(日本語,Qiita)][link-3] を参考にさせていただきました。
PythonのFlaskでi18nな事をするためには、 [Babel][link-2] を使うのが一般的(と言ってよいのかな?)なようです。
babelっていうと、Emacs使いの人やJSの人は別のものを思い浮かべるかと思いますが(脱線)。
というか、ややこしくなりそうなのでタグにBabelって書けませんでした……!!

まずは、Babel自体をインストールしておきます。

```
% pip3 install --user Babel flask_babel
```

ってな具合でした。

## まずはどんなものか一度試してみる

[こちらのサイト(日本語,Qiita)][link-3] のgithubのリポジトリをクローンして動かしてみます。
まずは、なにもかんがえずにtelnetで話しかけてみる。

```
% telnet localhost 5000
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET / HTTP/1.1

HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 11
Server: Werkzeug/0.12.2 Python/3.6.1+
Date: Wed, 14 Jun 2017 11:10:09 GMT

Hello WORLDConnection closed by foreign host.
```

いんぐりっしゅでした。はろー。
順番が逆になりますが、言語の切替はhttpのリクエストヘッダでAccept-Languageを送ることによって行なうことができるようです。
ということで、今度はみんな大好き日本語で返ってくるようにお願いしてみました。

```
% telnet localhost 5000
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET / HTTP/1.1
Accept-Language: ja

HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 21
Server: Werkzeug/0.12.2 Python/3.6.1+
Date: Wed, 14 Jun 2017 11:14:39 GMT

こんにちわ世界Connection closed by foreign host.
```

予定通り、日本語で返事をしてくれました。なるほど、こういう仕組みなのですね。
なお、Accept-Languageは重み付きのリストで渡すこともできるようです。

## 自分のFlaskアプリにBabelをつっこんでみる

では、いよいよi18n対応への第一歩を踏み出してみます。
Flaskアプリに以下の記述を追加しました。
[参考サイト(英語)][link-1] では `__init__.py` に書いてますが、どれがよいのざましょ。

```app/hoge.py
app = Flask(__name__)
app.config.from_object(__name__)

from flask_babel import Babel
babel = Babel(app)

from config import LANGUAGES

@babel.localeselector
def get_locale():
    return request.accept_languages.best_match(LANGUAGES.keys())
```

つづけて、以下のファイルを用意しました。

```config.py
# -*- coding: utf-8 -*-
#
# available languages
LANGUAGES = {
    'en': 'English',
    'ja': '日本語'
}
```

このようにlocaleselector デコレータを使うと Accept-Language に反応できるようになるようです。
そしてもうひとつ、Babel自体の設定ファイルも必要のようなので、以下のように作成しました。
1,2行目は翻訳対象となるファイル、3行目はBabelの拡張の有効化みたいです。
拡張について詳細は調べてません。

```babel.cfg
[python: **.py]
[jinja2: **/templates/**.tpl.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_
```

## 実際に翻訳してみる

### テンプレートをさわる

まずは、テンプレートの方をいじってみます。この場合、 `_()` が `gettext()` のエイリアスになってるようなので、以下のように、英語では "Name" となるところを細工しました。

```show_person.tpl.html
      <tr>
        <td>{{ _('Name') }}</td>
        <td>{{ person.name }}</td>
      </tr>
```

### Pythonコードをさわる (2017-06-17追記)

flashされるメッセージを翻訳する場合、Python側のコードをいじらないといけないのですが、以下のようにして実装できました。

```hoge.py
from flask import flash

from flask_babel import gettext as _

@app.route('/logout')
def logout():

    session.pop('logged_in', None)
    flash(_("You were logged out"), 'success')

    return redirect(url_for('show_hoge'))
```

上記では、テンプレート側と揃えるために、gettextを_としてimportしてます。
なお、数字等を入れたい場合は

```hoge.py
        flash(_("No Such Item with ID %(item_id)d", item_id=item_id), 'error')
```

としてやると良いようです。

### 翻訳ファイルを作成する

次に、以下のコマンドを走らせました。

```
% pybabel extract -F babel.cfg -o message.pot hoge
```

これにより、テンプレートファイルができあがるようです。
出力された message.pot の中身を見てみると

```message.pot
# Translations template for PROJECT.
# Copyright (C) 2017 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2017-06-14 20:54+0900\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.4.0\n"

#: hoge/templates/show_person.tpl.html:10
msgid "Name"
msgstr ""
```

といった感じでした。
msgidのところに、先程gettextをかました "Name" がいる模様。
さらに、このファイルをベースにして翻訳ファイルを作るようです。
具体的には、以下のコマンドを叩きました。

```
% pybabel init -i message.pot -d hoge/translations -l ja
creating catalog hoge/translations/ja/LC_MESSAGES/messages.po based on message.pot
```

poファイルは、[参考サイト(英語)][link-1] にて紹介されている[Poedit][poedit]を使うと簡単にできそうでした。
生成されたmessage.poを開いて、以下のように"Name"は"名前"と入れてあげました。

![poedit.png](https://qiita-image-store.s3.amazonaws.com/0/122416/48717ad9-bad0-5a36-542d-bd8f98dd4d7f.png)

編集されたファイルを見ると、更新日が書きかわり、もちろんmsgstrも書きかわり、X-Generatorで自己主張なさっておられました。
本来は、このあと `pybabel compile` とやるようですが、Poeditを使うと、保存した際に自動的にコンパイルされたmoファイルを作成してくれるようでした(使用バージョン、1.8.11)。

この状態にて、ブラウザでアプリの表示をさせると、無事に ”Name” のところが ”名前” と置き換わりました。
さらに、ブラウザの設定で英語の優先順位を上げると再び "Name" と表示されるのも確認できました。

### その後の更新は?

更新する場合は、以下の手順でpoファイルを更新するようです。

```
pybabel extract -F babel.cfg -o message.pot hoge
pybabel update -i message.pot -d hoge/translations
```

## 感想

文章とかになるとかなり大変そうな感じですが、単語程度であれば意外にラクチンにできちゃうんですね、というのが感想です。
更新時にコマンドたたくのが面倒そうですが、なんか良い手があるのかな?

とにもかくにも、これで職場利用もGitHub公開も問題ないですね!!
まぁ、最大の問題はアプリ自体の開発の方が進まないと意味がないってことなんですけども!!


## 参考文献

* [The Flask Mega-Tutorial, Part XIV: I18n and L10n][link-1]
* [Babel][link-2]
* [flask-babelを使って多言語化サイトを作ってみる][link-3]

[link-1]:https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xiv-i18n-and-l10n
[link-2]:http://babel.pocoo.org/en/latest/
[link-3]:http://qiita.com/shibacow/items/b29a16ab69f8453eb159
[poedit]:https://poedit.net/