Django language selection workflow
The first step in the process to support multiple languages and countries in a Django project, is to understand how a Django application determines whether to present itself in English, Spanish, German or some other language variation.
The entry point: LANGUAGE_CODE value
Let's start with a Django project with the settings.py
values described in listing 13-1 -- which are the default and should be there if you didn't tweak settings.py
. Next, ensure you have the Django admin set up and change the LANGUAGE_CODE
value in settings.py
to one of the following: fr
(for French), de
(for German) or es
(for Spanish).
Next, go to the Django admin main page -- by default at http://localhost:8000/admin/
-- and what do you see ? The login page which previously showed the field "Username" – with the LANGUAGE_CODE
set to en-us
– will now show either "Nom d'utilisateur", "Benutzername" or "Nombre de usuario", depending on the LANGUAGE_CODE
value you configured.
With the LANGUAGE_CODE
variable set to a different value, Django uses the locale corresponding to this new LANGUAGE_CODE
value, which in turns makes the Django admin use the locale message bundles for the given language. For example, with LANGUAGE_CODE='fr'
, Django sets it locale to fr
, which in turn loads the French localized message files (a.k.a. locale message bundles) included in the Django distribution (e.g. django/contrib/auth/locale/fr/LC_MESSAGES/django.po
). Future sections describe how to use and create your own locale message bundles.
The good news is you just localized the Django admin -- as well as a series of other internal Django internal messages -- to use another language by simply modifying the LANGUAGE_CODE
values. The bad news is you've now forced all users accessing the Django admin into what's probably not their native language.
A better approach is to let users have a say into which language pipeline they want and leave the LANGUAGE_CODE
to your primary audience's language, whatever that may be.
User language preferences: HTTP headers and Django middleware
When users send requests to a Django application their browsers send an HTTP header called Accept-Language
. The value for this header consists of a list of languages (i.e. a two character value) or locales (i.e. a two character language value, a dash, two character country code) determined by a user's browser preferences.
For example, browsers in Australia are likely to send out: Accept-Language:en-AU, en
, to indicate a preference for English with an Australian dialect and then plain English, where as browsers in multi-language countries like Switzerland are likely to send out a variation of Accept-Language:de-CH, fr-CH, it-CH
with the language order varying depending on a user's browser preferences. The important take away is that all users around the world send a hint in the Accept-Language
value regarding which language they prefer.
Django is equipped with built-in middleware -- which inspects all incoming requests, as described in Chapter 2 -- to use the values in Accept-Language
and direct users to the Django application language pipeline they prefer. The built-in middleware class for Django language detection is django.middleware.locale.LocaleMiddleware
, which must be added to the MIDDLEWARE
variable in settings.py
as shown in listing 13-2.
Listing 13-2.- Django Locale middleware enabled in settings.py
MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', ... ]
The MIDDLEWARE
class order is important because of the potential interdependent logic executed by each class. In listing 13-2 you can see the LocalMiddleware
class is declared after the SessionMiddleware
class because the locale determination uses session data. In addition, the LocalMiddleware
is declared before CommonMiddleware
so a user's locale is set before executing common logic in CommonMiddleware
(e.g. url resolution that can depend on language).
Once you update your project's MIDDLEWARE
value as illustrated in listing 13-2, go back to the Django admin and you'll see it translated to a language based on your browser's language preferences! If you don't see any changes in the Django admin, then it just means your browser's main language preferenece is the same as the Django project's LANGUAGE_CODE
value in settings.py
. In case you weren't able to see a different language, let's briefly explore how to change your browser's language preferences.
If you're using the Google Chrome browser, go to the 'Settings' menu, click on the 'Advanced' option on the left side followed by the 'Languages' option below it. This updates the center of the screen, click on the 'Language' menu and you'll see the 'Order languages based on your preference', as illustrated in figure 13-1.
Figure 13-1. Google Chrome language preferences in Settings menu
If you click on the far left button on each language row -- the vertical ellipsis named 'More actions' -- you can modify a language's position with the options 'Move to the top', 'Move up', 'Move down' and 'Remove'. Sorting the order of this language list, effectively updates the values sent in the HTTP header Accept-Language
indicating which languages you prefer.
You can add new languages to your browser preferences by clicking on the 'Add languages' link at the bottom of the 'Order languages based on your preference' list. Clicking on the 'Add languages' link brings up pop-up window like the one if figure 13-2, where you can add new languages to your browser preferences.
Figure 13-2. Google Chrome add new languages to preferences in Settings menu
The HTTP Accept-Language
header is generated with all languages in the 'Order languages based on your preference' list, so the final processing party -- in this case, the Django application -- determines which language to use.
When the Django LocaleMiddleware
class detects the HTTP Accept-Language
value(s) it performs the following logic:
- It inspects the first language/locale in the
Accept-Language
list, if the Django application supports the language -- that is, it's defined in theLANGUAGES
value insettings.py
-- it selects this language pipeline for the user and in turn uses the locale message bundles for a user’s visit. - If the first language/locale in the
Accept-Language
list is not supported, Django attempts to use the second language/locale inAccept-Language
as the language pipeline; this process goes on until one of languages in the HTTPAccept-Language
header matches a supported language in a Django project'sLANGUAGES
values. - If no language/locale in the
Accept-Language
list is supported by a Django project, Django defaults to theLANGUAGE_CODE
value insettings.py
, which in turns makes the Django use this language pipeline and locale message bundles for the user.
Note TheLANGUAGE_CODE
value is reset byLocalMiddleware
on a user-by-user basis to reflect a user's preferred language based on HTTPAccept-Language
header values.
As you can see, the Django LocaleMiddleware
class offers an effective solution to present a Django project to users in their preferred language, and in case users request an unsupported language, Django uses the default LANGUAGE_CODE
value in settings.py
.
For the moment, be patient using the Django admin as the reference point to visualize how multiple languages function a Django project. I'll shortly illustrate how to add multiple language content to your entire project, we just have one more step in the language selection workflow to go.
Although the LocaleMiddleware
class functionality allows users to determine their Django language pipeline via browser preferences and is a step forward over forcing all users into the default LANGUAGE_CODE
value, it has one glaring usability and SEO (Search Engine Optimization) problem: users and search engines can't view the same Django application in different languages, unless they adjust their languages preferences.
Multi-language urls: i18n_patterns and LANGUAGES
To avoid locking users and search engines into viewing a single language of a Django application, Django also supports multi-language urls. Multi-language urls allow access to different languages of the same Django project by means of a language key in a url.
For example, without multi-language urls the same Django admin url /admin/
presents content in German, French, Spanish or English based on language preferences. With multi-language urls, the Django admin for German can be accessible at the url /de/admin/
, the Django admin for French can be accessible at the url /fr/admin/
, etc. This not only benefits users, but also search engines, since a site's multiple languages can be accesed through different urls.
As you can imagine, Django multi-language urls offer an even greater level of functionality and usability, since with a few links -- irrespective of language preferences -- users and search engines can navigate a Django project's entire set of languages. To enable multi-language urls, you must wrap a project's standard url definitions in urls.py
-- as described in chapter 2 – with a special urls function called i18n_patterns
. Listing 13-3 illustrates a urls.py
file that uses i18n_patterns
.
Listing 13-3.- Django urls wrapped in i18n_patterns
from django.conf.urls.i18n import i18n_patterns from django.contrib import admin from django.views.generic import TemplateView from django.urls import path urlpatterns = [ path('',TemplateView.as_view(template_name='homepage.html'),name="homepage"), ] urlpatterns += i18n_patterns( path('admin/', admin.site.urls), )
Notice in listing 13-3 how a i18n_patterns
function is added to the standard urlpatterns
reference in the main urls.py
file. In this case, the i18n_patterns
function contains a single url -- the Django admin -- and tells Django to produce multi-language urls for /admin/
.
An important behavior of the i18n_patterns
function is you can chose which urls to be multi-language. Notice in listing 13-3 the standard urlpatterns
reference declares the url for the home page -- path('',...)
-- excluding the home page from multi-language urls. Although the home page in listing 13-3 can use different languages due to the LocalMiddleware
class, there would only be one home page url with no ability for the home page to be seen in other languages, unless a requesting party specifies a language via the HTTP Accept-Language
header.
Once you change your project's main urls.py
file to reflect listing 13-3, when you visit the Django admin at the /admin/
url, depending on the requesting language preferences, a redirect is made to the language-specific url (e.g. /de/admin/
, /fr/admin/
, /es/admin/
). More importantly than language detection redirection though, is multi-language urls allow applications to have different & live urls for all supported languages by a Django project.
Tip You can add theprefix_default_language=False
as the last argument toi18n_patterns
to force theLANGUAGE_CODE
value insettings.py
to not require qualifiying its url (e.g. withprefix_default_language=False
, ifLANGUAGE_CODE='en'
then/en/admin/
become accessible at the base/admin/
url).
By default, Django supports close to 100 languages[8]. With multi-language urls enabled for the Django admin, this is easy to corroborate. Go to some of the following Django admin urls: /tt/admin/
for Tatar, /pa/admin/
for Punjabi, /es-ni/admin/
for Nicaraguan Spanish, /ro/admin/
for Romanian or /zh-hans/admin
for Simplified Chinese. While this is a fun a exercise, it raises an interesting question, do you really want or need all these languages enabled in your application ? In most cases you don't and you can disable them.
You can declare the LANGUAGES
variable in settings.py
to override the default languages supported by all Django projects. Listing 13-4 illustrates a sample LANGUAGES
value that restricts a project's languages to four.
Listing 13-4.- Django LANGUAGES variable in settings.py with message id values
from django.utils.translation import gettext_lazy as _ LANGUAGES = [ ('es', _('Spanish')), ('en', _('English')), ('fr', _('French')), ('de', _('German')), ]
If you declare the LANGUAGES value in listing 13-4 in your project's settings.py
, you'll notice the only valid multi-language urls for the Django admin are now those language keys in listing 13-4 (i.e. 'es'
,'en'
, 'fr'
, 'de'
), the remaining language keys that once worked are now ignored since they no longer form part of a project’s languages.
It's worth mentioning how the language detection middleware (i.e. LocalMiddleware) functions with a limited amount of LANGUAGES
values. If no language in the HTTP Accept-Language
header matches one of the LANGUAGES
values, Django assigns the user to the default LANGUAGE_CODE
pipeline, so no matter how esoteric a language request, all users are guaranteed to get a valid response in the project's default language.
Now, look back at listing 13-4 and notice the second part of each language tuple. The explicit value is preceded by _
and based on the import statement is the gettext_lazy
method. So what is the reason behind this awkward syntax ? Why aren't the tuple elements declared as simple strings ? (e.g. ('es', 'Spanish')
, ('en', 'English')
).
The _
is a syntax convention for translation message ids. Even though encountering statements like ('en', gettext_lazy('English'))
would be more telling to what's going on, using _
to represent translation message ids has become standard. The presence of _
or technically the gettext_lazy()
method, tells Django to get the value for the translation message id in whatever language pipeline it's currently on. For example, if a user is in the es
language pipeline, the list of tuples in listing 13-4 gets interpreted to the following:
LANGUAGES = [ ('es','Español'), ('en','Inglés'), ('fr','Francés'), ('de','Alemán'), ]
Similarly, if a user is in the en
, fr
, or de
language language pipelines, the list of tuples in listing 13-4 gets interpreted differently based on the values for the translation message id (e.g. in the fr
language pipeline, the ('en', _('English'))
statement gets interpreted as ('en', 'Anglais')
. All values for message ids in every language are defined in locale message bundles, which is the topic of the next section.