DjangoのユーザーにLanguageとTimezone設定をもたせて動的に言語を切り替えたり時刻表示を切り替える

# やりたいこと

これ
<img src="https://raw.githubusercontent.com/wiki/salexkidd/django-user-g11n/imgs/example.gif" width="1024px" style="border: 1px">


# ユーザーに言語やタイムゾーンを設定させたい

Djangoファンの皆さん、ドジャンゴー(挨拶)。

私達はDjangoがI18NやL10Nに対応していることをよく知っています。そして、いともたやすくそれらの力を利用できることを知っているはずです。

多言語翻訳が必要なら *USE_I18N = True* として *LANGUAGE_CODE*を指定、フォーマットローカライゼーションが必要なら *USE_L10N = True*、タイムゾーンをあわせたいなら *USE_TZ = True* として *TIMEZONE* を指定するだけなんですから。

そして、Google翻訳やDeepL翻訳にお願いして翻訳ファイルを作っていくだけでグローバルなアプリケーションを作り上げることができます。もしあなたがDjangoを使ってアプリケーションを作っているのなら、すぐにでも国際展開を念頭においたアプリケーションを作ることができます。

しかし、問題があります。

もし、国際展開を念頭においたアプリをあなたが作ったとして、日本人、アメリカ人、フランス人、アジア諸国の人が利用していたとして、その言語設定は一体どこで切り替えたら良いのでしょうか。

そう、**settings.py** です。しかし*LANGUAGE_CODE*の設定をいじってしまったら最後、指定した言語でDjangoは出力を行います。もし*LANGUAGE_CODE*を*ja*とした場合は、各国それぞれの人にDjangoは日本語で表示を行います。そうなると、一体なんのために国際化したのかよくわからなくなってしまいます。

個人的にもこれは不思議でなりませんが、Django標準ではLanguageはおろか、Timezoneの設定すらユーザー側では変更することができません。今回はそれをユーザーが自分で変更可能にするというお話です。

# 仕組み

## LANGUAGEについて

基本となるコードは[djangoのマニュアルから抜粋](https://docs.djangoproject.com/en/3.0/topics/i18n/translation/#explicitly-setting-the-active-language)これです。利用したい言語を*translation.activate*に渡し、クッキーに値をセットします。これでDjangoがこのセッション中で利用する言語のみを切り替えることができます。

```python
from django.conf import settings
from django.http import HttpResponse
from django.utils import translation
user_language = 'fr'
translation.activate(user_language)
response = HttpResponse(...)
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, user_language)
```

今回はユーザーモデルに対して


## TIMEZONEについて

基本となるコードは[djangoのマニュアルから抜粋](https://docs.djangoproject.com/en/3.0/topics/i18n/timezones/#selecting-the-current-time-zone)これです。
利用したいタイムゾーンを*timezone.activate*に渡します。

```python
import pytz

from django.utils import timezone

class TimezoneMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        tzname = request.session.get('django_timezone')
        if tzname:
            timezone.activate(pytz.timezone(tzname))
        else:
            timezone.deactivate()
        return self.get_response(request)
```

あとはこれらのコードを基礎として、ユーザーモデルをカスタマイズして言語設定とタイムゾーン設定を扱うフィールドを用意し、ユーザー自身がこれを変更可能にすることです。

というわけで、今回用意したのが[django-user-g11n](https://pypi.org/project/django-user-g11n/)です。


# django-user-g11n

django-user-g11nは、Djangoのカスタムユーザーモデルを用意し、そこに "TimeZoneAndUserLanguageSupportMixin"を継承することで*language*と*timezone*、2つのフィールドをユーザーモデルに持たせ、Middlewareを用いて設定された言語やタイムゾーンを動的に変更します。

ちなみに、g11nとは(Globalization)を省略したもので、I18n+L10Nを指します。

## 使い方

の前に、もしサンプルをすぐに試したい方は"サンプルを利用する"を参照してください。

まずはDjangoとdjango-user-g11nをインストールします。

```shell-session
$ pip install django django-user-g11n
```

次にdjango-admin.pyを用いてプロジェクトをスタートします。

```shell-session
$ django-admin.py startproject example
```

次に、カスタムユーザーモデルを作成します。ここでは便宜上*accounts*というアプリケーション名にします。

```shell-session
$ manage.py startapp accounts
```

アプリを作り終えたら、*accounts/models.py*を変更して、カスタムユーザーモデルを追加します。
その際に、*UserLanguageSupportMixin*と*UserTimeZoneSupportMixin*を継承します。

```python
from django.contrib.auth import models as auth_models
from user_g11n.models import UserLanguageSupportMixin, UserTimeZoneSupportMixin


class User(UserTimeZoneSupportMixin,
           UserLanguageSupportMixin,
           auth_models.AbstractUser):
    pass
```

次は*settings.py*の変更です。

## settings.pyの変更
*INSTALLED_APPS*に今回作成したカスタムユーザーモデル用のアプリと、user-g11nを追加します。

```python
INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    .
    .
    .
    'accounts',  # Your Custom user model application
    'user_g11n', # Add
)
```

次にMIDDLEWAREを変更し、*UserLanguageMiddleware*と*UserTimeZoneMiddleware*を追加します。

```python
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    .
    .
    .
    'user_g11n.middleware.UserLanguageMiddleware', # Add
    'user_g11n.middleware.UserTimeZoneMiddleware', # Add
]
```

そして、AUTH_USER_MODELを指定し、カスタムユーザーモデルを利用可能にします。

```python
AUTH_USER_MODEL = 'accounts.User'
```

次にI18N, L10N, TZ、TIME_ZONEをそれぞれ設定します。

```
USE_I18N = True

USE_L10N = True

USE_TZ = True

TIME_ZONE = "Asia/Tokyo" # Change to your local timezone
```

変更が完了したらmigrationを行いDBを作成します。

```
$ ./manage.py makemigrations && ./manage.py migrate
```

これで完了です。あとはsuperuserを作成し、ユーザーの管理画面で言語、タイムゾーンの変更をおこなうことで、リアルタイムに表示が切り替わります。

<img src="https://raw.githubusercontent.com/wiki/salexkidd/django-user-g11n/imgs/example.gif" width="1024px" style="border: 1px">


# サンプルを利用する

このDjangoアプリは[GitHub上にて](https://github.com/salexkidd/django-user-g11n/)開発を行っています。

プロジェクトをCloneしてもらい、

```
$ docker-compose up
```

したあと、[http://localhost:8000](http://localhost:8000)にアクセスすることで、サンプルアプリを試すことができます。


# まとめ

- Djangoの国際化機構最高
- 最初からこれ用意してくれよって思う。なぜないのか…
- DjangoおぢさんはDjangoが大好き。
- 今年(2020)はCovid-19の影響で[Django Congress](https://djangocongress.jp/)なくなっちゃったけど、俺らの心のなかでは開催されているのだ！毎日のように・・・！

# 2022/5 にUpdate

Django4 からpytzがtimezoneに置き換わることにより、動かなくなる事態が発生したのでUpdateしました。

対応したのは以下の通り

- Django3ならPytzを利用(timezoneは入れられれば使えるけど、面倒なので対応無し)
- Django4で 
  - ``USE_DEPRECATED_PYTZ``がFalseならばtimezoneを利用
  - ``USE_DEPRECATED_PYTZ``がTrueならばpytzを利用

以上