Imagine! You’ve got this awesome app served by a RESTful API through Django/DRF and suddenly a requirement comes in:

  • We want this app and CMS (admin panel) in Chinese


  • We want this app and CMS (admin panel) in German

And if you’ve got this humongous code-base not written with all the ugettext and ugettext_lazy stuff in mind, then “Boy! You’re in danger! You should better start praying to almighty to give you strength and endurance on your path to become a Multilingual Super Hero”

In this post, we’ll see how you can support multiple languages in your Django based projects and be a Multilingual Super Hero!

The journey to be any hero isn’t easy and same is the case with this. So, we’ll also see certain gotchas that would be your shield in supporting internationalization and localization in your projects. Of course, there is the Django documentation, but one might feel lost in all the details that make it quite overwhelming. So, this post would be a primer to get you started with supporting localization in your projects.

NOTE: This is by no means a comprehensive tutorial. Although we’ll have a primer for you to explaining most of the things that are needed for supporting internationalization and translation in your projects, there are various other things that are provided with Django for more complex cases. For more information, please refer to the Django Translation Docs.

There are two kinds of data in the app that we’ll need to handle: static data (translations for all the fields, error messages etc. that the app already has) and dynamic data (custom data input by the user in the app). We’ll see how to best handle both of these.

Preparing your project for Internationalization and Localization

The first and foremost thing to enable translation is to tell Django what is the list of languages it should support, where does it find the translation for static data and what is the default language it should fall back on (in case there is no translation available for the requested language)


from django.utils.translation import ugettext_lazy as _

# Setup LocaleMiddleware to enable translations using ugettext_lazy and ugettext
# Make sure that LocaleMiddleware comes after SessionMiddleware and before CommonMiddleware

# Provide a lists of languages which your site supports.
    ('en', _('English')),
    ('zn-ch', _('Simplified Chinese')),
    ('zn-tw', _('Traditional Chinese')),

# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery. Make sure it is set to
# True if you want to support localization
USE_I18N = True

# If you set this to False, Django will not format dates, numbers and
# calendars according to the current locale.
USE_L10N = True

# Set the default language for your site.

# Contains the path list where Django should look into for django.po files for all supported languages
    os.path.join(BASE_DIR, 'locale'),

Gotcha #1: The LocaleMiddleware should always come before CommonMiddleware and after SessionMiddleware

With the settings in place, we’ve told Django the information that it needs to enable translations.

Retrieving Language preference from client’s request

Now, we need to decide how to track the language the client wants to communicate in. There are different ways to get the language preference in an HTTP request.

In order to retrieve the language preference from client’s request, LocaleMiddleware tries to determine the user’s language preference by following algorithm:

  • First, it looks for the language prefix in the requested URL. This is only performed when you are using the i18n_patterns function in your root URLconf.

  • Failing that, it looks for the LANGUAGE_SESSION_KEY key in the current user’s session.

  • Failing that, it looks for a cookie. The name of the cookie used is set by the LANGUAGE_COOKIE_NAME setting. (The default name is django_language.)

  • Failing that, it looks at the Accept-Language HTTP header. This header is sent by your browser and tells the server which language(s) you prefer, in order by priority. Django tries each language in the header until it finds one with available translations.

  • Failing that, it uses the global LANGUAGE_CODE setting.

For the REST APIs, I found Accept-Language header a much cleaner way to accomplish the task. For enabling multiple languages in admin panel, we’ll prefer i18n_patterns function and modify our root as shown below:

urlpatterns += i18n_patterns(
 # Django Admin

The i18n_patterns will automatically prepend the current active language code to all URL patterns defined within i18n_patterns(). So, all your admin URLs, with the current configuration having zh-cn and en activated, will have URLs as:


NOTE: The * above is a regular expression indicating anything after admin. For whatever language you want the admin panel to be accessible, the use of corresponding language code in the URL can help you in accessing that.

With prefix_default_language=False set, the default language code will not be prepended (en in this case), and you can access the admin route at just /admin. Although you can do this to all the URLs, but for the API endpoints, we prefer to supply this bit of information in the Accept-Language header, which seems more cleaner approach for API endpoints.

What does the LocaleMiddleware help with?

LocaleMiddleware is the secret sauce in the Translation Machinery. It does the following:

For the request part, it parses it and decide what translation object to install in the current thread context.

For the response part, it formats the URL with appropriate activated language if i18n_patterns are used and set the Content-Language header in response for the client to know what language is used in the response for parsing as shown.

LocaleMiddleware: The secret sauce in Django Translation machinery

LocaleMiddleware: The secret sauce in Django Translation machinery

Now we just need to tell, what to translate.

What to translate?

Majorly, we need to translate two kinds of data:

  • Static data that includes model names, field names of models, error messages, etc. that are static in the application.

  • Dynamic data that majorly includes the field value in models that is input by the user.

In the next article we’ll see how we can support translation in both of these cases and various gotcha moments! Don’t forget to visit part 3 of the series where I’ll discuss about how you can include locale context in async tasks and much more.

Related articles: