Djangoでユーザ毎に言語設定できるようにしてみた

やったこと Link to heading

  1. ブラウザの言語設定に応じた言語を表示する。
    • 日本語設定のブラウザなら日本語で、英語設定のブラウザなら英語でWebサイトを表示する。
    • 技術的にはDjangoの国際化(i18n)を利用してAccept-Languageの優先順位が高い言語でページを生成する。
  2.  Djangoのアカウント機能と合わせて、各ユーザが指定した言語に合わせてページを表示させる。
    • 技術的にはDjangoのセッション情報(session['_language'])を任意のタイミングで書き換える。

ちなみにソースコードはGitHubにもあります。

目次 Link to heading

  • 環境の準備
  • ブラウザの言語設定に応じたページ表示
  • アカウント認証機能の追加
  • ユーザ毎に言語設定機能の追加

環境の準備 Link to heading

本記事は以下の環境で実行した。

  • OS: CentOS7.6
  • Python 3.6.7
  • Django 2.1

以下のコマンドで諸々インストール

$ sudo yum install -y https://centos7.iuscommunity.org/ius-release.rpm
$ sudo yum install -y python36u python36u-libs python36u-devel python36u-pip
$ sudo pip3.6 install django==2.1

ブラウザの言語設定に応じたページ表示 Link to heading

Djangoプロジェクトを作成する。

$ cd ~
$ django-admin startproject i18n
$ cd i18n

i18n/settings.pyを編集する。(ファイル名は~/i18n/からの相対パス。以後同様)

# どのホストからのアクセスも受け付ける
ALLOWED_HOSTS = ['*']

# 国際化のミドルウェアを追加
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware',  # 追加
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates'),],  # 変更
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

# デフォルトの言語を日本語にする
LANGUAGE_CODE = 'ja'

# localeフォルダの位置を指定する
LOCALE_PATHS = (
    os.path.join(BASE_DIR, 'locale'),
)

# 選択できる言語を設定する
from django.utils.translation import ugettext_lazy as _
LANGUAGES = [
    ('en', _('English')),
    ('ja', _('Japanese')),
]

i18n/urls.pyを編集する。

from django.contrib import admin
from django.urls import path
from . import views # 追加
 
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.index, name='index'), # 追加
]

i18n/views.pyを作成する。

from django.shortcuts import render
 
def index(request):
    return render(request, 'index.html')

templatesフォルダを作成する。

templates/index.htmlを作成する。

{% load i18n %}
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>{% trans "Djangoのテストページ" %}</title>
    </head>
    <body>
        <h1 id="title">{% trans "Djangoのテストページ" %}</h1>
        <button type="button" onclick='alert("{% trans "こんにちは" %}")'>{% trans "ここをクリック" %}</button>
    </body>
</html>

localeフォルダを作成する。

以下のコマンドで翻訳用ファイルを作成する。

$ django-admin makemessages -l en

locale/en/LC_MESSAGES/django.poを編集する。

# : i18n/templates/index.html:6 i18n/templates/index.html:9
msgid "Djangoのテストページ"
msgstr "Django test page" # 変更
 
# : i18n/templates/index.html:10
msgid "こんにちは"
msgstr "Hello" # 変更
 
# : i18n/templates/index.html:10
msgid "ここをクリック"
msgstr "Click here" # 変更

以下のコマンドで翻訳ファイルをコンパイルする。

$ django-admin compilemessages

Webサーバ起動

$ python3.6 manage.py runserver 0.0.0.0:8000

ブラウザでhttp://(仮想マシンのIPアドレス):8000にアクセスし、ブラウザの言語設定に応じたページが表示されることを確認する。

ブラウザの言語設定に応じたページ(日本語)

ブラウザの言語設定に応じたページ(英語)

アカウント認証機能の追加 Link to heading

i18n/settings.pyを編集してログインURL設定を追加する。

LOGIN_URL = '/login/'

i18n/urls.pyにログインとログアウトのエンドポイントを追加する。
※:ログインにはdjango.contrib.auth.views.LoginViewを使うのが簡単なのだが、今回はアカウント毎に言語を設定する処理を追加するため低水準関数のdjango.contrib.authauthenticateloginを利用する。

from django.contrib import admin
from django.urls import path
from . import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.index, name='index'),
    path('login/', views.login_view, name='login'),  # ログイン用エンドポイント追加
    path('logout/', views.logout_view, name='logout'),  # ログアウト用エンドポイント追加
]

i18n/views.pyにログインとログアウトの処理を追加する。

from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.contrib.auth import authenticate, login
from django.contrib.auth import views as auth_views

@login_required  # トップページもログインしないと入れないようにしてみる
def index(request):
    return render(request, 'index.html')

# ログイン用view関数を追加
def login_view(request):
    if request.method == 'GET':
        next = request.GET.get('next', '/')
        return render(request, 'login.html', {'next': next})
    elif request.method == 'POST':
        username = request.POST.get('username', '')
        password = request.POST.get('password', '')
        next = request.POST.get('next', '/')
        user = authenticate(username=username, password=password)
        if user is not None:
            login(request, user)
            return redirect(next)
        else:
            return render(request, 'login.html', {'next': next, 'error': True})

# ログアウト用view関数を追加
@login_required
def logout_view(request):
    return auth_views.logout_then_login(request)

トップページtemplates/index.htmlにログアウトボタンを追加する。

{% load i18n %}
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>{% trans "Djangoのテストページ" %}</title>
    </head>
    <body>
        <h1 id="title">{% trans "Djangoのテストページ" %}</h1>
        <button type="button" onclick='alert("{% trans "こんにちは" %}")'>{% trans "ここをクリック" %}</button>
        <!-- ここを追加 -->
        <form action="{% url 'logout' %}">
            <input type="submit" value="{% trans "ログアウト" %}" />
        </form>
        <!-- 追加ここまで -->
    </body>
</html>

ログイン用テンプレートtemplates/login.htmlを作成

{% load i18n %}
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>{% trans "ログイン" %}</title>
    </head>
    <body>
        <h1 id="title">{% trans "ログイン" %}</h1>
        {% if error %}
            <p>{% trans "ユーザ名もしくはパスワードが間違っています。もう一度試してください。" %}</p>
        {% endif %}

        <form method="post" action=".">
            {% csrf_token %}
            <table>
                <tr><td>{% trans "ユーザ名" %}: </td><td><input type="text" name="username"></td></tr>
                <tr><td>{% trans "パスワード" %}: </td><td><input type="password" name="password"></td></tr>
            </table>
            <input type="hidden" name="next" value="{{ next }}">
            <input type="submit" value="{% trans "ログイン" %}" />
        </form>
    </body>
</html>

翻訳用ファイルを更新する。

$ django-admin makemessages -l en

locale/en/LC_MESSAGES/django.poを編集する。

# : templates/index.html:14
msgid "ログアウト"
msgstr "Log out" # 変更

# : templates/login.html:6 templates/login.html:10 templates/login.html:23
msgid "ログイン"
msgstr "Log in" # 変更

# : templates/login.html:12
msgid "ユーザ名もしくはパスワードが間違っています。もう一度試してください。"
msgstr "Your username and password didn't match. Please try again." # 変更

# : templates/login.html:18
msgid "ユーザ名"
msgstr "Username" # 変更

# : templates/login.html:20
msgid "パスワード"
msgstr "Password" # 変更

翻訳ファイルをコンパイルする。

$ django-admin compilemessages

以下の2つのコマンドでDBマイグレーションを実施する。

$ python3.6 manage.py makemigrations
$ python3.6 manage.py migrate

adminユーザを作成する。

$ python3.6 manage.py createsuperuser
ユーザー名 (leave blank to use 'root'): admin
メールアドレス: (空欄でも可)
Password: (パスワード)
Password (again): (パスワード)
Superuser created successfully.

Webサーバ起動

$ python3.6 manage.py runserver 0.0.0.0:8000

ブラウザでhttp://(仮想マシンのIPアドレス):8000にアクセスし、以下を確認する。

  • ログインページにリダイレクトされる。

ログインページ(日本語)

ログインページ(英語)
  • ログインするとトップページに遷移する。

ログイン後のトップページ(日本語)

ログイン後のトップページ(英語)
  • 「ログアウト」をクリックするとログアウト処理が行われ、再度ログインページにリダイレクトされる。

ユーザ毎に言語設定機能の追加 Link to heading

i18n/models.pyを作成してDBモデルを追加する。
※:アカウント毎の言語を設定できるようにするため、Userテーブルと1対1関係にあるProfileテーブルを作成してProfileに言語を格納する。

from django.db import models
from django.contrib.auth.models import User

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    language = models.CharField(max_length=128, default='', blank=True)  # ユーザの設定した言語を格納する

    def __str__(self):
        return self.user.username

管理画面でDB設定できるようにi18n/admin.pyを作成する。

from django.contrib import admin
from .models import Profile

# Register your models here.
@admin.register(Profile)
class Profile(admin.ModelAdmin):
    pass

i18n/settings.pyを編集して、Profileモデルを読み込めるようにする。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'i18n',  # 追加
]

i18n/urls.pyに言語設定用のエンドポイントを追加する。

from django.contrib import admin
from django.urls import path
from . import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.index, name='index'),
    path('login/', views.login_view, name='login'),
    path('logout/', views.logout_view, name='logout'),
    path('language/', views.language, name='language'),  # 追加
]

i18n/views.pyに言語設定の処理を追加する。
また、ログイン時に設定してある言語を適用する処理も追加する。

from django.shortcuts import render, redirect, get_object_or_404  # 便利な関数を追加
from django.contrib.auth.decorators import login_required
from django.contrib.auth import authenticate, login
from django.contrib.auth import views as auth_views
from .models import Profile  # 追加

@login_required
def index(request):
    return render(request, 'index.html')

# ログイン用view関数を編集
def login_view(request):
    if request.method == 'GET':
        next = request.GET.get('next', '/')
        return render(request, 'login.html', {'next': next})
    elif request.method == 'POST':
        username = request.POST.get('username', '')
        password = request.POST.get('password', '')
        next = request.POST.get('next', '/')
        print(next)
        user = authenticate(username=username, password=password)
        if user is not None:
            login(request, user)
            profile = get_object_or_404(Profile, user=request.user)  # Profileテーブルから該当のユーザを取得
            request.session['_language'] = profile.language  # この通信のsessionに言語を設定する
            return redirect(next)
        else:
            return render(request, 'login.html', {'next': next, 'error': True})

# ログアウト用view関数を編集
@login_required
def logout_view(request):
    del request.session['_language']  # このsessionの言語設定を削除する
    return auth_views.logout_then_login(request)

# 言語設定用view関数を追加
@login_required
def language(request):
    if request.method == 'GET':
        return render(request, 'language.html')
    elif request.method == 'POST':
        language = request.POST['language']
        profile = get_object_or_404(Profile, user=request.user)  # Profileテーブルから該当のユーザを取得
        profile.language = language  # Profileにユーザからのリクエスト言語を設定
        profile.save()  # DB更新
        request.session['_language'] = language  # この通信のsessionに言語を設定する
        return redirect('language')

request.session['_language']でセッション情報の言語設定を上書きすることがミソ。

トップページtemplates/index.htmlに言語設定ページへのリンクを追加する。

{% load i18n %}
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>{% trans "Djangoのテストページ" %}</title>
    </head>
    <body>
        <h1 id="title">{% trans "Djangoのテストページ" %}</h1>
        <button type="button" onclick='alert("{% trans "こんにちは" %}")'>{% trans "ここをクリック" %}</button>
        <!-- ここを追加 -->
        <br/>
        <a href="{% url 'language' %}">{% trans "言語設定" %}</a>
        <!-- 追加ここまで -->
        <form action="{% url 'logout' %}">
            <input type="submit" value="{% trans "ログアウト" %}" />
        </form>
    </body>
</html>

言語設定用テンプレートtemplates/language.htmlを作成する。

{% load i18n %}
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>{% trans "言語設定ページ" %}</title>
    </head>
    <body>
        <h1 id="title">{% trans "言語設定ページ" %}</h1>
        <form action="." method="post">
            {% csrf_token %}
            <select name="language">
                {% get_current_language as LANGUAGE_CODE %}
                {% get_available_languages as LANGUAGES %}
                {% get_language_info_list for LANGUAGES as languages %}
                {% for language in languages %}
                    <option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected{% endif %}> {{ language.name_local }} ({{ language.code }}) </option>
                {% endfor %}
            </select>
            <input type="submit" value="{% trans "変更" %}">
        </form>

        <a href="{% url 'index' %}">{% trans "トップページに戻る" %}</a>
    </body>
</html>

あとは翻訳系の作業。翻訳用ファイルを更新する。

$ django-admin makemessages -l en

locale/en/LC_MESSAGES/django.poを編集する。

# : templates/index.html:12
msgid "言語設定"
msgstr "Language setting" # 変更

# : templates/language.html:6 templates/language.html:9
msgid "言語設定ページ"
msgstr "Language setting page" # 変更

# : templates/language.html:22
msgid "変更"
msgstr "Change" # 変更

# : templates/language.html:25
msgid "トップページに戻る"
msgstr "Back to top page" # 変更

翻訳ファイルをコンパイルする。

$ django-admin compilemessages

DBマイグレーション

$ python3.6 manage.py makemigrations i18n
$ python3.6 manage.py migrate

Webサーバ起動

$ python3.6 manage.py runserver 0.0.0.0:8000

ブラウザで管理者ページhttp://(仮想マシンのIPアドレス):8000/adminにアクセスし、adminユーザのProfileを作成する。

管理者ページにアクセス。adminユーザでログインする。

このページでDBデータを操作できる。

Profileテーブルにadminユーザのデータを登録。Languageは空でOK。

管理者ページからログアウトし、トップページにアクセスして以下を確認する。(ログインは前項と同じなので省略)

  • 「言語設定」リンクがあること

    トップページに「言語設定」リンクが表示される。(日本語設定のブラウザの場合)
  • 「言語設定」リンクを押すと言語設定ページが表示されること

    言語設定ページで言語選択できる。
  • プルダウンから英語を設定して「変更」をクリックすると英語のページが表示されること

    言語設定を英語に変更すると英語表記のページが表示される
  • トップページに戻ると自分が設定した言語のまま

    トップページに戻っても英語のまま。
  • ログアウトするとブラウザ本来の言語でログインページが表示されること

    ログアウトするとブラウザ本来の言語に戻る。