In this part of the TaskBuster Django Tutorial, we’ll talk about Internationalization, Localization and Time Zones.
We will define two different languages for our website, create urls specific for each of them, and make our website to support these two languages.
Moreover, we’ll talk about the time zone and how we can show the local time on a template.
The outline of this part is:
- Internationalization – Settings
- Internationalization – Urls
- Internationalization – Templates
- Internationalization – Translation
- Localization
- Time Zones
Let’s start! 🙂
Internationalization – Settings
Let’s work on how we can implement internationalization, i.e. that our page can support multiple languages.
First, we will create a folder to save all the translation files. Those files will be created automatically by Django, writing the strings that we want to translate. I’ll show you how you can tell Django which strings you want to translate latter, but the idea is that you edit the files and put the translation for each string manually. This way, Django will choose one language or another depending on the user preferences.
The translation’s folder will be located inside the taskbuster folder:
$ mkdir taskbuster/locale
Next, open your settings/base.py file and make sure you have
USE_I18N = True
and the template context processor django.template.context_processors.i18n is inside the TEMPLATES[‘OPTIONS’][‘context_processors’] setting:
TEMPLATES = [ { ... 'OPTIONS': { 'context_processors': [ ... 'django.template.context_processors.i18n', ], }, }, ]
Note: You can also find the value of a specific setting by using the Django shell. For example:
$ python manage.py shell >>> from django.conf import settings >>> settings.TEMPLATES
and it will output the current value of that variable.
Next, add the Locale middleware in the correct position, to be able to determine the user’s language preferences through the request context:
MIDDLEWARE_CLASSES = ( ... 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', ... )
Next, specify the languages you want to use:
from django.utils.translation import ugettext_lazy as _ LANGUAGES = ( ('en', _('English')), ('ca', _('Catalan')), )
We will use English and Catalan (but feel free to put the languages you want, you can find its codes here). The ugettext_lazy function is used to mark the language names for translation, and it’s usual to use the function’s shortcut _.
Note: there is another function, ugettext, used for translation. The difference between these two functions is that ugettext translates the string immediately whereas ugettext_lazy translates the string when rendering a template.
For our settings.py, we need to use ugettext_lazy because the other function would cause import loops. In general, you should use ugettext_lazy in your model.py and forms.py files as well.
Moreover, the LANGUAGE_CODE setting defines the default language that Django will use if no translation is found. I’ll leave the default:
LANGUAGE_CODE = 'en-us'
And finally, specify the locale folder that we created before:
LOCALE_PATHS = ( os.path.join(BASE_DIR, 'locale'), )
Don’t forget the trailing coma.
Internationalization – Urls
Ok, now that we have configured the settings, we need to think about how we want the app to behave with different languages. Here we will take the following approach: we will include a language prefix on each url that will tell Django which language to use. For the Home page it will be something like:
- mysite.com/en
- mysite.com/ca
- mysite.com/en/myapp
- mysite.com/ca/myapp
This way the user may change from one language to another easily. However, we don’t want that neither the robots.txt nor the humans.txt files follow this structure (search engines will look at mysite.com/robots.txt and mysite.com/humans.txt to find them).
One way to implement this is with the following urls.py file:
# -*- coding: utf-8 -*- from django.conf.urls import include, url from django.contrib import admin from django.conf.urls.i18n import i18n_patterns from .views import home, home_files urlpatterns = [ url(r'^(?P<filename>(robots.txt)|(humans.txt))$', home_files, name='home-files'), ] urlpatterns += i18n_patterns( url(r'^$', home, name='home'), url(r'^admin/', include(admin.site.urls)), )
Note that we left the robots.txt and humans.txt files with the same url, and the ones that we want to be translated use the i18n_patterns function.
Run your local server and visit the home page, it should redirect to /en or /ca. You can learn more about how Django discovers language preference in the official documentation.
But how does the user change its language preferences? Well, Django comes with a view that does this for you 😉
This view expects a POST request with a language parameter. However, we will take care of that view in another time in this tutorial, when customizing the top navigation bar. The idea is to have a drop-down menu with all the possible languages, and just select one to change it.
Before proceeding, let’s run our tests,
$ python mange.py test taskbuster.test
Oh…. one failed! Well actually both unittest in taskbuster/test.py fail, as the template rendered when trying to use reverse(“home”) is not found. This is because we need to set an active language for the reverse to work properly. First, write this at the top of the file:
from django.utils.translation import activate
and next, activate the selected language just after the test declaration. For example:
def test_uses_index_template(self): activate('en') response = self.client.get(reverse("home")) self.assertTemplateUsed(response, "taskbuster/index.html")
And the same for the other test: test_uses_base_template.
Now, tests pass. You should do the same for the functional_tests/test_all_users.py: import the activate method at the beginning of the file and add the activate(‘en’) as the last step on the setUp method.
Internationalization – Templates
Let’s focus now on how we can translate the h1 Hello World title of the Home Page. Open the index.html template and look for <h1>Hello, world!</h1>.
We will use two different template tags:
- trans is used to translate a single line – we will use it for the title
- blocktrans is used for extended content – we will use it for a paragraph
Change the h1 and p contents of the jumbotron container for the following code:
<div class="jumbotron"> <div class="container"> <h1>{% trans "Welcome to TaskBuster!"%}</h1> <p>{% blocktrans %}TaskBuster is a simple Task manager that helps you organize your daylife. </br> You can create todo lists, periodic tasks and more! </br></br> If you want to learn how it was created, or create it yourself!, check www.django-tutorial.com{% endblocktrans %}</p> <p><a class="btn btn-primary btn-lg" role="button">Learn more »</a></p> </div> </div> </div>
Moreover, to have access to the previous template tags, you will have to write {% load i18n %} near the top of your template. In this case, after the extends from base.html tag.
Internationalization – Translation
Finally, we are able to translate our strings!
Go to the terminal, inside the taskbuster_project folder (at the same level as the manage.py file), and run:
$ python manage.py makemessages -l ca
This will create a message file for the language we want to translate. As we will write all our code in english, there is no need to create a message file for that language.
But ohh!! we get an ugly error that says that we don’t have the GNU gettext installed (if you don’t get the error, good for you! skip this installation part then!). Go to the GNU gettext home page and download the last version. Inside the zip file you’ll find the installation instructions on a file named INSTALL.
Basically, you should go inside the package folder (once unzipped) and type:
$ ./configure
to configure the installation for your system. Next, type
$ make
to compile the package. I always wonder why some installations print all that awful code on your terminal!
If you want, use
$ make check
to run package tests before installing them, to see that everything works. Finally, run
$ make install
to install the package.
Okey! Let’s go back to our developing environment, and try to generate our message file!
$ python manage.py makemessages -l ca
Yes! It worked!
Now go to the taskbuster/locale folder to see what’s in there.
$ cd taskbuster/locale $ ls
There is a folder named ca (or the language you chose to translate) with a folder named LC_MESSAGES inside. If you go inside it, you’ll find another file named django.po. Inspect that file with your editor.
There is some metadata at the beginning of the file, but after that you’ll see the strings we marked for translation:
- The language’s names “English” and “Catalan” in the base.py settings file
- The Welcome to TaskBuster! title on the index.html file
- The paragraph after the title on the index.html file
Translating the title is simple:
msgid "Welcome to TaskBuster!" msgstr "Benvingut a TaskBuster!"
And with a paragraph, you have to be careful to start and end each line with “”:
msgid "" "TaskBuster is a simple Task manager that helps you organize your daylife. </" "br> You can create todo lists, periodic tasks and more! </br></br> If you " "want to learn how it was created, or create it yourself!, check www.django-" "tutorial.com" msgstr "" "TaskBuster és un administrador senzill de tasques que t'ajuda a administrar " "el teu dia a dia. </br> Pots crear llistes de coses pendents, tasques " "periòdiques i molt més! </br></br> Si vols apendre com s'ha creat, o" "crear-lo tu mateix!, visita la pàgina <a href='https://www.marinamele.com/taskbuster-django-tutorial' target=_'blank'>Taskbuster Django Tutorial</a>."
Also, note the final space at the end of the line. If you don’t include that space, the words at the end of the line and at the beginning of the next line will concatenate.
Once you have all the translations set, you must compile them with:
$ python manage.py compilemessages -l ca
You can run your local server and see the effect by going to the home page, but I prefer writing a test first! 🙂
In the functional_tests/test_all_users.py add the following tests:
def test_internationalization(self): for lang, h1_text in [('en', 'Welcome to TaskBuster!'), ('ca', 'Benvingut a TaskBuster!')]: activate(lang) self.browser.get(self.get_full_url("home")) h1 = self.browser.find_element_by_tag_name("h1") self.assertEqual(h1.text, h1_text)
Remember to change the Benvingut a TaskBuster! sentence and the activate(‘ca’) if you’re using another language!
I hope all of your tests passed! 🙂
Localization
Django is capable to render times and dates in the template using the format specified for the current locale. This means that two people with different locales may see a different date format on the template.
To enable Localization, edit the base.py settings file and make sure that:
USE_L10N = True
This way, when including a value in a template, Django will try to render it using the locale‘s format. However, we also need a way to deactivate this automatic formatting. For example, when using javascript, we need that the value we use has a uniform format for all locals.
Let’s write a test for it! In the Home Page, we will show today’s date and time using both local and non-local formats.
Open the functional_tests/test_all_users.py and write these imports at the top:
from datetime import date from django.utils import formats
and next, add this test method inside the HomeNewVisitorTest class:
def test_localization(self): today = date.today() for lang in ['en', 'ca']: activate(lang) self.browser.get(self.get_full_url("home")) local_date = self.browser.find_element_by_id("local-date") non_local_date = self.browser.find_element_by_id("non-local-date") self.assertEqual(formats.date_format(today, use_l10n=True), local_date.text) self.assertEqual(today.strftime('%Y-%m-%d'), non_local_date.text)
Next, run the test to see what should we implement first. You can run just this test using the command:
$ python manage.py test functional_tests.test_all_users.HomeNewVisitorTest.test_localization
The test complains because it can’t find the element with a local-date id. Let’s create it!
Open again taskbuster/index.html template and load the localization template tags at the beginning of the file (for example, after loading the internationalization template tags):
{% load l10n %}
Note: that it’s a lowercase L10N, not a 110N!!
Next, look for the container div. We will edit the first two columns to display the date on their h2 headers:
<div class="col-md-4"> <h2 id="local-date">{{today}}</h2> <p>This is the time using your local information </p> <p><a class="btn btn-default" href="#" role="button">View details »</a></p> </div> <div class="col-md-4"> <h2 id="non-local-date">{{today|unlocalize}}</h2> <p>This is the default time format.</p> <p><a class="btn btn-default" href="#" role="button">View details »</a></p> </div>
Note the unlocalize filter on the second header. This disables the localization format while rendering the today variable. Moreover, there is another template tag to disable large blocks:
{% localize off %} code without localization {% endlocalize %}
You can learn more about this in the Django documentation.
Save and run the test again. It fails again, because we didn’t pass the today variable in the home view. Let’s do that!
Open the views.py and edit the home view:
import datetime def home(request): today = datetime.date.today() return render(request, "taskbuster/index.html", {'today': today})
Rerun the test! Yeah! It passed! 🙂
Time Zones
If your project has to handle dates and times from all over the world, it’s better to work in UTC (coordinated universal time). This way all your dates and times have a uniform convention in your database, so you can compare them no matter the time zone of the user.
And don’t worry, Django translates them automatically to the desired time zone in forms and templates.
To enable the time zone support, open your base.py settings file and make sure you have
USE_TZ = True
Django also recommends to install pytz, a Python package that allows for timezones calculations. Moreover, it’s also a package needed to use Celery, a task queue manager that we will use latter in this tutorial. Let’s install it!
Activate your development environment and type:
$ pip install pytz
and add it into the base.txt requirements file (you can see the version installed with pip freeze). Remember to install it too in your testing environment.
There are two different kind of datetime objects, the ones that are aware of the time zone and the ones that are not. You can use the datetime methods is_aware() and is_naive() to determine which one it is.
If we enable time zone support, all the datetime instances will be aware. Therefore, in order to interact with some saved datetime, we should create an aware datetime instance.
For example:
import datetime from django.utils.timezone import utc now_naive = datetime.datetime.now() now_aware = datetime.datetime.utcnow().replace(tzinfo=utc)
Moreover, if you have time zone support enabled, i.e. USE_TZ=True, there is a shortcut to obtain the current aware time:
from django.utils.timezone import now now_aware = now()
However, as explained in the Django documentation, there is no way to determine the user time zone preferences through the HTTP header. What we will do is to ask the preferred time zone to the user and save it in his user profile. But that will come latter in this tutorial, sorry 😉
Meanwhile, we will define a default time zone with the setting variable:
TIME_ZONE = 'Europe/Madrid'
which is the time zone here in Barcelona 🙂 You can pick your time zone from here.
So let’s see how to display the current time here in Barcelona (using the default time zone), the UTC time, and the time in New York.
Open your functional_tests/test_all_users.py and write the following test:
def test_time_zone(self): self.browser.get(self.get_full_url("home")) tz = self.browser.find_element_by_id("time-tz").text utc = self.browser.find_element_by_id("time-utc").text ny = self.browser.find_element_by_id("time-ny").text self.assertNotEqual(tz, utc) self.assertNotIn(ny, [tz, utc])
we will check that the Barcelona, UTC and New York times are different.
Run this test and see it fail because the element with an id of time-tz is missing. Open the index.html template and edit the third column:
<div class="col-md-4"> <h2>Time Zones</h2> <p> Barcelona: <span id="time-tz">{{now|time:"H:i"}}</span></p> <p> UTC: <span id="time-utc">{{now|utc|time:"H:i"}}</span></p> <p> New York: <span id="time-ny"> {{now|timezone:"America/New_York"|time:"H:i"}}</span></p> <p><a class="btn btn-default" href="#" role="button">View details »</a></p> </div>
Where I used the time filter to display only the time. Run the test again, and it will fail because it can’t find the utc filter. In your index.html file add at the top of the file:
{% load tz %}
Run your tests again and this time they will fail because the views didn’t pass any now variable. Open the views.py and add this import at the beginning of the file:
from django.utils.timezone import now
and add the now variable into the home view:
def home(request): today = datetime.date.today() return render(request, "taskbuster/index.html", {'today': today, 'now': now()})
Run your tests again, and they should pass!
Ok, this is the end of this part of the tutorial. Don’t forget to commit your changes:
$ git add . $ git status $ git commit -m "Internationalization and localization" $ git push origin master
The last command only if you want to push it in your cloud repository, like Bitbucket or GitHub.
In the next part of the tutorial, we’ll cover Documentation. Yes, it may sound a boring subject to you, but I’m sure that you really appreciate a package with a understandable and well structured documentation, right?
Don’t miss it!
Do you like this tutorial? Don’t forget to share it with your friends! 🙂
Thanks!