Skip to content

Marina Mele's site

Reflections on family, values, and personal growth

Menu
  • Home
  • About
Menu
Taskbuster Internationalization

Part V – Internationalization and Localization. Languages and Time zones

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.

taskbuster_internationalization
Make Django spread around the world by talking different 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
And for the rest of urls, like mysite.com/myapp, it will be:
  • 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 &raquo;</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
Each of these sentences appear in a line beginning with msgid. You have to put your translation in the next line, the one that starts with msgstr.

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='http://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 &raquo;</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 &raquo;</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 &raquo;</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!

Leave a Reply Cancel reply

You must be logged in to post a comment.

Categories

  • Personal Growth and Development
  • Mindful Parenting and Family Life
  • Productivity and Time Management
  • Mindfulness and Wellness
  • Values and Life Lessons
  • Posts en català
  • Other things to learn
Follow @marina_mele
  • Cookie Policy
  • Privacy Policy
©2023 Marina Mele's site | Built using WordPress and Responsive Blogily theme by Superb
This website uses cookies to improve your experience. If you keep navigating through this website, we'll assume you're ok with this, but you can opt-out if you wish.Accept Read More
Privacy & Cookies Policy

Privacy Overview

This website uses cookies to improve your experience while you navigate through the website. Out of these, the cookies that are categorized as necessary are stored on your browser as they are essential for the working of basic functionalities of the website. We also use third-party cookies that help us analyze and understand how you use this website. These cookies will be stored in your browser only with your consent. You also have the option to opt-out of these cookies. But opting out of some of these cookies may affect your browsing experience.
Necessary
Always Enabled
Necessary cookies are absolutely essential for the website to function properly. This category only includes cookies that ensures basic functionalities and security features of the website. These cookies do not store any personal information.
Non-necessary
Any cookies that may not be particularly necessary for the website to function and is used specifically to collect user personal data via analytics, ads, other embedded contents are termed as non-necessary cookies. It is mandatory to procure user consent prior to running these cookies on your website.
SAVE & ACCEPT