How to Make a Super Smooth Language Switcher with Django and HTMX
Need a super smooth language switcher that just works everywhere?
Django has a built in helper function called set_language
that will translate any given url and redirect the user to that translated url. Very nice. With just a little bit of work, you can make it work even better with HTMX.
What you will learn
In this guide, you'll learn:
- How Django's built in
set_language
function works and how to use it - How to modify
set_language
to work withprefix_default_language=False
set ini18n_patterns
- How to make it super smooth with HTMX
Getting started
Before getting started, first you need to setup your website for localization. Check out Testdrive.io's guide for setting up Django's localization framework. The rest of this guide assumes you've got a localized project, a basic understanding of how to use it, and just need a language switcher.
For HTMX stuff, I assume you have HTMX set up and ready to use (if not, refer to my guide on setting up HTMX and the rest of the GRUG stack ).
How to use set_language
The documentation on set_language
states:
As a convenience, Django comes with a view,
django.views.i18n.set_language()
, that sets a user’s language preference and redirects to a given URL or, by default, back to the previous page.
This means that you should be able to use set_language
anywhere for any arbitrary page. Nice. The docs also include an example HTML template for using set_language
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
There are some important things to understand:
set_language
needs a POST request
1 2 |
|
The next
parameter is optional
1 2 3 |
|
The language
parameter needs to be set
1 2 3 4 5 6 7 8 9 10 11 |
|
For my use, I only have one other language to switch to, so I use the following in my template:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
This will generate a button for each language in LANGUAGES in your settings.py
. The button submits the language code as the value of the language
parameter. set_language
will use that value to set the user's language preferences for your site.
I also styled it to make the button fixed to the bottom right of the screen.
Now, all we need is to make sure that the set_language
view is in our project-level urls.py
file:
1 2 |
|
You should now have a language switcher that works on every current and future page of your site.
One problem
When I first used set_language
, I had a problem that you might run into. I could switch from my default language, but I couldn't switch back to it. The problem was related to the fact that I had prefix_default_language=False
set in i18n_patterns
. prefix_default_language=False
means that when the user is on the default language for my site, urls won't have en/
added to the path
(switch to Japanese now and notice that ja/
is added to the beginning of a path after the domain). This is a known bug.
There are two ways to fix the problem:
- Set
prefix_default_language
to True (its default value) and accept that the language code for the default language will be added to all urls. - Slightly modify
set_language
.
To make set_language
work with prefix_default_language=False
, we copy the set_language
function and add a fix.
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 |
|
Using my HTML language switcher above--where no next
parameter is set in the request--the next_url
will be automatically set to https://killianarts.online/ja/the/rest/of/the/path
or https://killianarts.online/the/rest/of/the/path
. With the code we added, the path
will be extracted, leaving /ja/the/rest/of/the/path
or /the/rest/of/the/path
. Then, for /ja/the/rest/of/the/path
, we strip out the /ja
and use the rest. The translate_url
function will do the work of switching the url to the correct one based on the language code.
Smooth swapping with HTMX
By default, the whole page will be reloaded with the page scrolled back to the top. By adding just a few HTMX attributes, we can make it smoothly transition to the other language via the new View Transitions API and maintain the scroll position (and even the state of other DOM elements on the screen, like the Google map on my Contact Us page) using Idiomorph. The switch to a different language will be seamless.
To get started, we need the Idiomorph library. Add this line after your HTMX script import in your template file:
1 |
|
Then, modifying the form:
1 2 3 4 5 6 7 8 |
|
hx-post="{% url 'set_language' %}"
will make an HTMX POST request to the set_language
function.
hx-target="body"
will make the <body>
element the target of whatever the response to hx-post="{% url 'set_language' %}"
is.
hx-push-url="true"
will update the url in the browser to the url that hx-post
returns in its response. set_language
simply returns an HttpResponseRedirect, which goes to a different url and returns its response.
We could also replace hx-post
, hx-target
, and hx-push-url
with hx-boost="true"
, since it would do exactly the same thing. Generally, my impression is that if you already have an older code base that you want to give a quick and easy upgrade so that forms use AJAX requests, things like that, I've heard people suggest using hx-boost
. If you're building a new code base or making a significant refactor, you'd be better off using the other attributes since you'll need them in most cases (for example, you probably will need a different target, and you'll often not want to push the url).
As of this writing, because of the bug with
set_language
above, if you want to setprefix_default_language=False
with the fix I added above, I recommend usinghx-boost
. I don't know why, but for some pages,hx-post
returns a 400 error and doesn't complete the redirect.
hx-ext="morph"
will allow us to use Idiomorph.
hx-swap
is how we decide the way to swap in the response to the request. Setting it to innerHTML
will replace the innerHTML of the <body>
with the response. That means we're replacing the whole page. You might be wondering, "Hey Micah, if you're going to replace the whole page anyway, why use HTMX at all? Why not simply reload the whole page as usual?"
HTMX doesn't replace the <head>
, and thus CSS, JS, etc. imports won't be reloaded. That means a speedier switch with no Flash of Unstyled Content.
Additionally, it allows us to do other cool stuff like use Idiomorph. Putting the morph:
modifier before innerHTML
will make the swap method use Idiomorph, rather than the default morphing method. Using Idiomorph, when we switch languages, dynamic elements like videos or Google maps will not lose their state. Honestly, not a critical feature, but a cool one
The real meat and potatoes comes next. transition:true
will make HTMX use the View Transition API for the swap. We could do fancy stuff using CSS to modify how the current page transitions to the new page returned by set_language
, but the defaults are actually just fine.
Finally, show:none
will maintain the current browser scroll state. By default, boosted links and forms default to show:top
. Using show:none
, we'll keep our scroll position on the page.
So easy, even I could do it
Altogether, with just a few lines of HTMX, we can significantly upgrade the language switcher. Now, it's super smooth and lets users switch between languages without distraction or losing their place. Useful for people who want to learn either language or for Japanese people who can't understand what they hell I'm trying to say in Japanese.
Author
Added
Dec. 2, 2023