Create a contact form with Django Crispy Forms and Bootstrap 4 (II): internationalization and localization with gettext


   

This time I will show you how to add internationalization and localization to a Django app.

Internationalization can be described as the process of preparing a website to be translated to different languages. Localization refers to writing the translations and features such as displaying local date formats.

As an example I will use the contact form we built on the previous post, which I will translate to Spanish (my native language). By the end you will know how to load different translations on the fly.



There is no gettext logo, so here is the GNU logo.

Introducing gettext


Django uses a translation and localization system called gettext, which was created with the goal of separating programming from translating. Gettext was created by Sun Microsystems in the early 90s and the GNU project released a free implementation a few years later.

Gettext is divided in two parts: i18n for internalization and L10n for localization.


Setting up translation and localization


 Translation and localization are enabled by default in your settings file with

USE_I18N = True  
USE_L10N = True


The reason I mention this is that if you don't use internationalization, you can save a little overhead by setting USE_I18N to False. For this project both of them will be left as True.

We also need to configure a folder to store the translations like this:

project/settings.py

LOCALE_PATHS = (
BASE_DIR + '/locale',
)


By default, Django doesn't include the middleware needed for internationalization. This middleware should be placed between SessionMiddleware and CommonMiddleware.

project/settings.py

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]


Other than that, we need to install gettext:

$ sudo apt install gettext


Using gettext in Python code


contact_form/views.py

from django.utils.translation import gettext as _


To mark a string for translation, we call gettext() passing it the string.

messages.success(self.request, _('Message received πŸ‘½'))


To translate the form fields' labels we will create them with a marked string.

contact_form/forms.py

from django import forms
from django.utils.translation import gettext as _

from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit, Field


class ContactForm(forms.Form):
def __init__(self, *args, **kwargs):
super(ContactForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()

self.helper.form_method = 'post'
self.helper.form_action = '/contact'

self.helper.add_input(Submit('submit', _('Submit')))

email = forms.EmailField(label=_('Email'), max_length=254)
message = forms.CharField(label=_('Message'), max_length=254, widget=forms.Textarea)


Using gettext in Django templates


Django provides some custom template tags to mark strings for translation in templates. All we have to do is wrap the string to be translated with {% trans %}.

contact_form/templates/contact_form/contact.html

{% load crispy_forms_tags %}
{% load i18n %}

<body>
<h1 class="text-center"> {% trans "Contact form" %}</h1>


Generating the message files


Translation is done by generating .po files that contain the original strings with an empty string below each of them. We translate the words by writing these strings.

Django has a management command for this:

$ ./manage.py makemessages --locale es_ES


A folder structure will be created on your project root:
locale/
└── es_ES
└── LC_MESSAGES
└── django.po


Go ahead and open the django.po file with your favourite text editor. You should see something similar to this (your favourite editor should be Emacs):




As I explained above, translations can be added by writing the msgstr strings:

#: contact_form/forms.py:16
msgid "Submit"
msgstr "Enviar"

#: contact_form/forms.py:18
msgid "Email"
msgstr "Correo electrΓ³nico"

#: contact_form/forms.py:19
msgid "Message"
msgstr "Mensaje"

#: contact_form/templates/contact_form/contact.html:11
msgid "Contact form"
msgstr "Formulario de contacto"

#: contact_form/views.py:21
msgid "Message received πŸ‘½"
msgstr "Mensaje recibido πŸ‘½"


Compiling message files


We're almost done, but Gettext is not able to work with our translations yet. Translation files (.po) need to be compiled into binaries (.mo) every time we make a change to a message file.

$ ./manage.py compilemessages


Doing some manual testing


If you change your language preferences to Spanish, gettext will do its magic.






Can you notice something strange? The Email and Message labels are not showing their translated text.


The difference between gettext and gettext_lazy


When we run a Django server some parts of our app are only executed once.

ContactFormView is being executed constantly, but ContactForm isn't, and that causes some of our translated strings not to show. This Stack Overflow reply gives an excellent explanation.

This has an easy solution, which is to use gettext_lazy instead of gettext in those cases. This way, every time the email or button fields are accessed their translation will be applied.


project/contact_form/forms.py

from django.utils.translation import gettext_lazy as _
class ContactForm(forms.Form):
def __init__(self, *args, **kwargs):
super(ContactForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()

self.helper.form_method = 'post'
self.helper.form_action = '/contact'

self.helper.add_input(Submit('submit', _('Submit')))

email = forms.EmailField(label=_('Email'), max_length=254)
message = forms.CharField(label=_('Message'), max_length=254, widget=forms.Textarea)


This time our text is translated perfectly:




Adding buttons to switch languages on the fly


From a developer's point of view internationalization can be an annoying process, due to the way language preferences are set and how many clunky browsers the market offers.

On the other hand, most users don't even know how to change their language preferences. Giving them the option to switch languages in a quick and friendly way makes their experience much better.


contact_form/templates/contact_form/contact.html

<body>
<nav>
<form method="GET">
<button class="btn-info" name="lang-button" value="en" type="submit">EN</button>
<button class="btn-info" name="lang-button" value="es" type="submit">ES</button>
</form>
</nav>




Activating the requested language and making the browser remember it


To get the data we need we will override get_context_data().

When a user requests our form, we will check if he has sent us any language information. If he does so, there will be a lang-button:language entry in the request.GET dictionary, and we will load the translation with translation.activate()

For the sake of readability, I have added a new set_language method. There are two possible GET requests on this view: the initial request and the one when you press a language button.

The very first time users visit our app they will see it in English. If they press a language button, their sessions are updated with a new default language. This way, users don't have to constantly remind the app of their preferred language.

I have overridden the POST method as well, so we can see the translation of the 'Message received' string after the form is successfully submitted.


contact_form/views.py

from django.contrib import messages
from django.views.generic.edit import FormView
from django.utils.translation import gettext as _
from django.conf import settings
from django.utils import translation

from .forms import ContactForm
from .models import Message


class ContactFormView(FormView):
template_name = 'contact_form/contact.html'
form_class = ContactForm
success_url = '/contact'

def get_context_data(self, *args, **kwargs):
self.set_language(self.request)

return super(ContactFormView, self).get_context_data(**kwargs)

def post(self, *args, **kwargs):
translation.activate(self.request.session.get(translation.LANGUAGE_SESSION_KEY))

return super(ContactFormView, self).post(self.request, *args, **kwargs)

def form_valid(self, form):
email = form.cleaned_data['email']
message = form.cleaned_data['message']

message = Message(email=email, message=message)
message.save()

messages.success(self.request, _('Message received πŸ‘½'))

return super().form_valid(form)

def set_language(self, request):
default_language = request.session.get(translation.LANGUAGE_SESSION_KEY) or 'en'
user_language = request.GET.get('lang-button') or default_language

request.session[translation.LANGUAGE_SESSION_KEY] = user_language

translation.activate(user_language)