DjangoとHTMXで超スムーズな言語スイッチャーの作り方

どこでも動作するスーパースムーズな言語スイッチャーが必要ですか?

Djangoにはset_languageという組み込みのヘルパー関数があり、与えられたURLを翻訳してユーザーをその翻訳したURLにリダイレクトします。とても便利です。少し作業をするだけで、HTMXともっと良く動作させることができます。

あなたが学ぶこと

このガイドでは以下を学びます:

  • Djangoの組み込みのset_language関数がどのように動作するのか、そしてそれをどのように使用するのか
  • prefix_default_language=Falsei18n_patternsに設定してset_languageを修正する方法
  • HTMXを使って、言語スイッチャーを超スムーズなにする方法

準備

始める前に、ウェブサイトをローカライゼーションの準備をする必要があります。Djangoのローカライゼーションフレームワークを設定するためのTestdrive.ioのガイドをご覧ください。このガイドの残りの部分は、ローカライズしたプロジェクトを持ち、基本的な使い方がわかり、ただ言語スイッチャーが必要であると仮定します。

HTMXの事項については、HTMXがセットアップされ、使用可能であることを仮定します(もしまだであれば、僕のHTMXとGRUGスタックの残りの設定する方法のガイドを参照してください)。

set_languageの使い方

set_languageに関するドキュメンテーション によると:

便宜上、Djangoにはビューdjango.views.i18n.set_language() が含まれており、これによりユーザーの言語設定を行い、指定されたURL にリダイレクトするか、デフォルトでは前のページに戻ることができます。

ということは、任意のページでset_languageを使用することができるということを意味しています。すばらしいですね。ドキュメンテーションにはまた、set_languageを使用するHTMLテンプレートの例も含まれています:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{% load i18n %}
<form action="{% url 'set_language' %}" method="post">{% csrf_token %}
    <input name="next" type="hidden" value="{{ redirect_to }}">
    <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="Go">
</form>

大事なポイントはいくつかあります:

set_languageがPOSTリクエストを必要とする

1
2
<!-- set_languageはPOSTリクエストで呼び出される必要があります。そうでないと何もしないまま現在のページにリダイレクトしてしまいます。 -->
<form action="{% url 'set_language' %}" method="post">

nextパラメータはオプショナル

1
2
3
<!-- フォーム内に"name"という名前のinputを含めている場合、
set_languageはその値にリダイレクトします。ただし、これはオプショナルです。 set_languageがリクエスト内でnextパラメータを取得しなかった場合、HTTP_REFERERを通じて現在のページを使用します。 -->
<input name="next" type="hidden" value="{{ redirect_to }}">

languageパラメータが設定される必要があります

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!-- set_languageは、LANGUAGES設定内の言語コードに設定された値を持つ"language"パラメータがリクエスト内に必要です。 -->
<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>

僕の使用法では、切り替える他の言語が1つしかないので、僕のテンプレートでは次のように使用しています:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{% get_current_language as CURRENT_LANGUAGE_CODE %}  
{% get_available_languages as AVAILABLE_LANGUAGES %}  
{% get_language_info_list for AVAILABLE_LANGUAGES as languages %}  
{% for lang in languages %}  
  {% if lang.code != CURRENT_LANGUAGE_CODE %}  
    <form id="language-switcher" 
          action="{% url 'set_language' %}"  
          method="post">  
      {% csrf_token %}  
      <input name="language" type="hidden" value="{{ lang.code }}">  
      <button class="fixed bottom-10 right-10 bg-sky-600 text-sky-50 px-5 py-2 hover:text-neutral-900 hover:bg-yellow-400 transition ease-in-out duration-300"  
              type="submit">{{ lang.name_local }}</button>  
    </form>  
  {% endif %}  
{% endfor %}

これは、settings.pyのLANGUAGESで言語のための各ボタンを生成します。ボタンは言語コードをlanguageパラメータの値として送信します。set_languageはその値を使用して、あなたのサイトのユーザーの言語設定を設定します。

僕はまた、ボタンを画面の右下に固定するためにそれをスタイルしました。

次に必要なことはset_languageビューが僕たちのプロジェクトレベルのurls.pyファイルにあることを確認することだけです:

1
2
# これがi18n_patterns()内部にないことを確認してください。そうでないと動作しません。
path("i18n/", include("django.conf.urls.i18n")),

これで、あなたのサイトの現在、そして未来の全てのページで動作する言語スイッチャーができ上がりました。

一つ問題があります

最初にset_languageを使用したとき、問題がありました。僕はデフォルト言語から切り替えることができましたが、それに戻すことができませんでした。問題は、僕がi18n_patternsprefix_default_language=Falseを設定していたことに関連していました。prefix_default_language=Falseと設定すると、ユーザーがサイトのデフォルト言語にいるとき、urlsにen/pathに追加されません(日本語に切り替えてみて、ドメインの後にja/がパスの先頭に追加されることに注目してください)。これは既知のバグです。

問題を修正する二つの方法があります:

  1. prefix_default_languageをTrue(デフォルトの値)に設定し、デフォルトの言語の言語コードが全てのURLに追加されることを受け入れる。
  2. set_languageを少し修正する。

set_languageprefix_default_language=Falseと共に動作するようにするためには、set_language関数をコピーして修正を追加します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
+ from django.conf import settings  
+ from django.http import HttpResponseRedirect  
+ from django.urls import translate_url  
+ from django.utils.http import url_has_allowed_host_and_scheme  
+ from django.utils.translation import check_for_language  
+ from urllib.parse import urlsplit

def set_language(request):  
    """  
    Djangoの組み込みset_languageの修正版。  
    選択した言語をセッション(有効な場合)とクッキーに設定しながら、指定されたURLにリダイレクトします。URLと言語コードはリクエストパラメータで指定する必要があります。
    このビューは、ユーザーがサイトの残りをどのように見るかを変更するため、POSTリクエストとしてのみアクセスする必要があります。GETリクエストとして呼び出された場合、状態を何も変更せずにリクエストのページ('next'パラメータ)にリダイレクトします。 
    """    
    next_url = request.POST.get("next", request.GET.get("next"))  

    if (  
            next_url or request.accepts("text/html")  
    ) and not url_has_allowed_host_and_scheme(  
        url=next_url,  
        allowed_hosts={request.get_host()},  
        require_https=request.is_secure(),  
    ):  
        next_url = request.META.get("HTTP_REFERER")  
        if not url_has_allowed_host_and_scheme(  
                url=next_url,  
                allowed_hosts={request.get_host()},  
                require_https=request.is_secure(),  
        ):  
            next_url = "/"  

+    # next_urlがHTTP_REFERERに設定されている場合、それはurlスキームとホストを含むでしょう。   
+    # パスを取得するためにそれを掴みましょう。そうすれば、言語プレフィクスを取り除くことができます。   
+    next_url = urlsplit(next_url).path  
+    # urlを翻訳する前に、urlから言語コードを取り除きます。   
+    for lang_code, lang_name in settings.LANGUAGES:  
+        prefix = '/' + lang_code + '/'  
+        if next_url.startswith(prefix):  
+            next_url = next_url[len(prefix) - 1:]  
+            break  

    response = HttpResponseRedirect(next_url) if next_url else HttpResponse(status=204)  
    if request.method == "POST":  
        lang_code = request.POST.get(LANGUAGE_QUERY_PARAMETER)  
        if lang_code and check_for_language(lang_code):  
            if next_url:  
                next_trans = translate_url(next_url, lang_code)  
                if next_trans != next_url:  
                    response = HttpResponseRedirect(next_trans)  
            response.set_cookie(  
                settings.LANGUAGE_COOKIE_NAME,  
                lang_code,  
                max_age=settings.LANGUAGE_COOKIE_AGE,  
                path=settings.LANGUAGE_COOKIE_PATH,  
                domain=settings.LANGUAGE_COOKIE_DOMAIN,  
                secure=settings.LANGUAGE_COOKIE_SECURE,  
                httponly=settings.LANGUAGE_COOKIE_HTTPONLY,  
                samesite=settings.LANGUAGE_COOKIE_SAMESITE,  
            )    return response

僕が上記で使用したHTML言語スイッチャーのように、リクエスト内にnextパラメータが設定されていない場合では, next_urlは自動的に https://killianarts.online/ja/the/rest/of/the/pathまたはhttps://killianarts.online/the/rest/of/the/pathに設定されます。追加したコードを使用すると、pathが抽出されて /ja/the/rest/of/the/pathまたは /the/rest/of/the/pathになります。それから、 /ja/the/rest/of/the/pathの場合, /jaを取り除いて続きを使用します。 translate_url関数が実際にURLを言語コードに基づいて正しいものに切り替える作業をします。

HTMXでスムーズにスワップ

デフォルトでは、ページ全体が上までスクロールした状態で再読み込みされます。HTMX属性を数個追加するだけで、他の言語へスムーズに移行し、スクロール位置を保持することができます(また、Googleマップのような他のDOM要素のスクリーン上の状態も保持します)。異なる言語への切り替えはシームレスに行います。

はじめに、Idiomorphライブラリが必要です。テンプレートファイル内でHTMXスクリプトインポートの後でこの行を追加します:

1
<script src="https://unpkg.com/idiomorph/dist/idiomorph-ext.min.js"></script>

そして、フォームを変更します:

1
2
3
4
5
6
7
8
<form id="language-switcher"  
      action="{% url 'set_language' %}"  
      method="post"  
+     hx-post="{% url 'set_language' %}"  
+     hx-target="body"  
+     hx-push-url="true"
+     hx-ext="morph"
+     hx-swap="morph:innerHTML transition:true show:none">

hx-post="{% url 'set_language' %}"は、set_language関数にHTMX POSTリクエストを作成します。

hx-target="body"は、hx-post="{% url 'set_language' %}"のレスポンスのターゲットを<body>要素にします。

hx-push-url="true"は、hx-postがレスポンスで返すURLをブラウザのURLに更新します。 set_languageは単にHttpResponseRedirectを返し、別のurlに移動し、そのレスポンスを返します。

また、hx-posthx-targethx-push-urlhx-boost="true"と置き換えることもできます。これは全く同じことを行います。一般的な印象としては、すでに古いコードベースを持っている場合や、フォームがAJAXリクエストを使用するように簡単かつ迅速にアップグレードするようなことをしたい場合、hx-boostの使用を人々が提案することを聞きます。新しいコードベースを作成したり、大幅なリファクタリングを行う場合、他の属性を使用する方が良いでしょう。なぜなら、ほとんどの場合それらが必要になるからです(例えば、おそらく異なるターゲットが必要で、そしてよくURLを押さないためには)。

この記事を書いている現在、上記の set_language のバグのため、僕が上記に追加した修正とともに prefix_default_language=False を設定したい場合、 hx-boost の使用をお勧めします。理由は不明ですが、一部のページで hx-post が400のエラーを返し、リダイレクトが完了しません。

hx-ext="morph"は、僕達がIdiomorphを使用することができるようにします。

hx-swapはリクエストへのレスポンスをどのように交換するかを決定します。これをinnerHTMLに設定すると、<body>のinnerHTMLがレスポンスで置き換えられます。つまり、ページ全体を置き換えることになります。「ねえ、マイカ、ページ全体を置き換えるのであれば、なんでHTMXを使用するのか? なぜ普通のようにページ全体をリロードしないの?」って?

HTMXは<head>を置き換えないので、CSSやJSなどのインポートはリロードされません。これにより、スタイルのないコンテントのフラッシュがない、より高速な切り替えが可能になります。

さらに、Idiomorphなど、他のクールなことも可能にします。 morph:修飾子をinnerHTMLの前に置くと、交換方法がデフォルトの変形方法ではなく、Idiomorphを使用するようになります。Idiomorphを使用すると、言語を切り替えるときに、ビデオやGoogleマップのような動的な要素が状態を失うことはありません。絶対に必要な機能ではないけど、かっこいいです。

次は本質的なポイントです。 transition:trueは、HTMXが交換のためにビュートランジションAPIを使用するようにします。 set_languageにより返される新しいページへの現在のページの遷移方法を変更するためにスタイリッシュなことをCSSで行うことができるけど、デフォルトの設定のままでいいです。

最後に、show:noneは現在のブラウザのスクロール状態を維持します。デフォルトでは、ブーストしたリンクとフォームはshow:topにデフォルト設定されています。show:noneを使うと、スクロールした位置が変更されません。

僕でもできるように簡単でした

まとめると、HTMXの数行で、言語スイッチャーを大幅にアップグレードできます。今では、それは超スムーズで、ユーザーが邪魔されることなく、場所を失うことなく言語を切り替えることができます。それは、どちらの言語も学びたい人々や、僕の日本語がおかしくて分かりづらい日本人にとって役にたつでしょう。

Profile Photo

Author

Added

2023年12月2日

Technologies
HTMX HTMX
Django Django