strona główna

Introduction to Django-multilingual

The Django-multilingual library aims to simplify creating models with
translatable fields.

This document, just like the library, is a work in progress. Stay
tuned!

A translatable field is a field that might contain different values
for different languages. For example, consider a hierarchy of
categories: every category has a name and possibly a number of
subcategories. A drastically simplified model might look like this:

[python]
class Category(models.Model):
parent = models.ForeignKey('self', blank=True, null=True)
name = models.CharField(blank=True, null=False, maxlength=250)
[/python]

It contains information about parent Category and a name. This is
enough for a site that only serves content in one language, but begins
to be a problem when you need to present category names in several
languages.

Django-multilingual lets you do this:

[python]
class Category(models.Model):
parent = models.ForeignKey('self', blank=True, null=True)
class Translation:
name = models.CharField(blank=True, null=False, maxlength=250)
[/python]

This snippet will actually create two models: Category containing the
non-translatable fields (in this case: id and parent) and
CategoryTranslation containing all the translatable fields, plus
language id and a reference to the main Category model.

Django-multilingual does much more than that, though: it tries hard to
creates an illusion that the Category model looks like this (assuming
that the service was configured for two languages, 'en' and 'pl'):

[python]
class Category(models.Model):
parent = models.ForeignKey('self', blank=True, null=True)
name_en = models.CharField(blank=True, null=False, maxlength=250)
name_pl = models.CharField(blank=True, null=False, maxlength=250)
name = models.CharField(blank=True, null=False, maxlength=250)
[/python]

The library flattens the data kept in CategoryTranslation and extends
Category objects with some properties that behave just like they were
actual fields, with a very interesting exception for the "name" field.

But more on that later. First, some examples:

[python]
>>> c = Category()
>>> c.name_en = 'category 1'
>>> c.name_pl = 'kategoria 1'
>>> c.save()

>>> c = Category()
>>> c.name_en = 'category 2'
>>> c.name_pl = 'kategoria 2'
>>> c.save()

>>> c = Category.objects.all().order_by('id')[0]
>>> (c.name, c.name_en, c.name_pl)
('category 1', 'category 1', 'kategoria 1')

>>> c = Category.objects.all().order_by('id')[1]
>>> (c.name, c.name_en, c.name_pl)
('category 2', 'category 2', 'kategoria 2')

>>> [c.name for c in Category.objects.all().order_by('name_en')]
['category 1', 'category 2']

>>> [c.name for c in Category.objects.all().filter(name_en__contains='2')]
['category 2']
[/python]

You can retrieve data from those "fields", assign new data to them,
use them for QuerySet ordering and filtering. You can also use them
in Admin internal class:

[python]
class Category(models.Model):
parent = models.ForeignKey('self', blank=True, null=True)

class Translation:
name = models.CharField(blank=True, null=False, maxlength=250)

class Admin:
list_display = ('id', 'name')
search_fields = ('name', 'description')
[/python]

And it will work just like it should. With a little patch to Django
you even get to order the columns with translatable data in admin list
view; see http://code.djangoproject.com/ticket/3397.

Now, for the "name" field: it has a bit more magic than name_en and
name_pl in that it is wired to either of them, depending on what you
want it to be. See the following example:

[python]
>>> c = Category.objects.all().order_by('id')[0]
>>> set_default_language('en')
>>> (c.name, c.name_en, c.name_pl)
('category 1', 'category 1', 'kategoria 1')
>>> set_default_language('pl')
>>> (c.name, c.name_en, c.name_pl)
('kategoria 1', 'category 1', 'kategoria 1')
[/python]

It works similarly for assignments:

[python]
>>> c = Category.objects.all().order_by('id')[0]

>>> set_default_language('pl')
>>> (c.name, c.get_name(1), c.get_name(2))
('kategoria 1', 'category 1', 'kategoria 1')

# note that the following assignment will change name_pl
>>> c.name = 'kat 1'
>>> (c.name, c.get_name(1), c.get_name(2))
('kat 1', 'category 1', 'kat 1')

>>> set_default_language('en')
# and this assignment will set name_en
>>> c.name = 'cat 1'
>>> (c.name, c.get_name(1), c.get_name(2))
('cat 1', 'cat 1', 'kat 1')
[/python]

And will work just as well in any other place you could use name_en or
name_pl, like filtering and ordering.

The set_default_language function lets you globally set what language
you need. You can also choose a different language just for a single
QuerySet or a particular object:

[python]
>>> c_en = Category.objects.all().for_language('en')
>>> c_pl = Category.objects.all().for_language('pl')
>>> c_en.get(name__contains='1').name
'category 1'
>>> c_pl.get(name__contains='1').name
'kategoria 1'
[/python]

Well, that's it for now, let me
know
if this was helpful or interesting for you. The library
provides even more functionality than I described, including (soon to
be nice) support for inline editing of translations in the automatic
change page for Category objects. There is still a lot to do, so I
could use some help as well :)