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:

Note The LANGUAGE_CODE value is reset by LocalMiddleware on a user-by-user basis to reflect a user's preferred language based on HTTP Accept-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 the prefix_default_language=False as the last argument to i18n_patterns to force the LANGUAGE_CODE value in settings.py to not require qualifiying its url (e.g. with prefix_default_language=False, if LANGUAGE_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.

  1. https://github.com/django/django/blob/master/django/conf/global_settings.py#L51