strona główna

Archive for January, 2007

Django multilingual extension

Tuesday, January 23rd, 2007

Cool stuff: Google provides free project hosting for open source software. I created a new project, django-multilingual and uploaded the prototype code there.

From now on you can use svn to get the source code and track changes. The initial version in svn is the code I posted in my previous blog entry with renamed directories, but without any other changes.

Multilingual content in Django

Tuesday, January 23rd, 2007

I am currently developing a website with lots of multilingual content, the most common use case being a model that contains a number of fields independent of user language and a set of translatable fields. I wanted something easy to use, fitting well with the rest of Django and hopefully elegant :) I ended up with half-baked multilingualization library for Django. Let me know if any of this looks useful to you.

The good news is that the library is already working; you even get to edit the translations in the admin interface. The bad news is it is not ready for production use yet. I would be happy to hear any suggestions to improve it.

Let's start with an example:

File: test/models.py

[python]
from django.db import models
from django.contrib.auth.models import User
from elksoft.multilingual.models import *

class Category(models.Model):
"""
Test model for multilingual content.
"""

# First, some fields that do not need translations
creator = models.ForeignKey(User)
created = models.DateTimeField(auto_now_add=True)
parent = models.ForeignKey('self', blank=True, null=True)

# And now the translatable fields
class Translation:
"""
The definition of translation model.

The multilingual machinery will automatically add these to the
Category class:

* get_name(language_id=None)
* set_name(value, language_id=None)
* get_description(language_id=None)
* set_description(value, language_id=None)
* name and description properties using the methods above
"""

name = models.CharField(blank=True, null=False, maxlength=250)
description = models.TextField(blank=True, null=False)

def __str__(self):
# note that you can use name and description fields as usual
return self.name

class Admin:
# again, field names just work
list_display = ('name', 'description')

class Meta:
verbose_name_plural = 'categories'
[/python]

The Translation class defines a set of translatable fields. The library extends the Category model with a number of methods and properties that give you access to and control of the translated fields; in particular you get 'name' and 'description' properties that assume you want to get or set values in the default language.

Internally the library creates a CategoryTranslation model that contains all the translatable fields. All the get_name/set_name methods work on the data in that model.

And now, the plumbing:

File: elksoft/multilingual/models.py

[python]
"""
Multilingual model support.
"""

from django.db import models
from django.db.models.base import ModelBase
from django.dispatch.dispatcher import connect
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import signals

# This is ugly, ideally languages should be taken from the DB or
# settings file. Oh well, it is a prototype anyway.

# It is important that the language identifiers are consecutive
# numbers starting with 1.
LANGUAGES = [['en', 'English'], # id=1
['pl', 'Polish']] # id=2

LANGUAGE_CNT = len(LANGUAGES)

from django.contrib.admin.templatetags.admin_modify import StackedBoundRelatedObject

class TransBoundRelatedObject(StackedBoundRelatedObject):
"""
This class changes the template for translation objects.
"""
def template_name(self):
return "admin/edit_inline_translations.html"

def get_default_language():
# you might take the ID from elsewhere, ie
# cookies or threadlocals
return 1

def translation_contribute_to_class(cls, main_cls, name):
"""
Handle the inner 'Translation' class.
"""

def getter_generator(trans_name, field_name):
"""
Generate get_'field name' method for model trans_name,
field field_name.
"""
def get_translation_field(self, language_id=None):
if language_id == None:
language_id = get_default_language()
try:
return getattr(self.get_translation(language_id), field_name)
except ObjectDoesNotExist:
return "None"
get_translation_field.short_description = "get " + field_name
return get_translation_field

def setter_generator(trans_name, field_name):
"""
Generate set_'field name' method for model trans_name,
field field_name.
"""
def set_translation_field(self, value, language_id=None):
if language_id == None:
language_id = get_default_language()
try:
setattr(self.get_translation(language_id), field_name, value)
except ObjectDoesNotExist:
return "None"
set_translation_field.short_description = "set " + field_name
return set_translation_field

def finish_multilingual_class(*args, **kwargs):
"""
Create a model with translations of a multilingual class.
"""
class TransMeta:
ordering = ('language',)

trans_name = main_cls.__name__ + name

# create get_'field name'(language) and set_'field
# name'(language) methods for all the translation fields.
# Add the 'field name' properties while you're at it, too.
for fname, field in cls.__dict__.items():
if isinstance(field, models.fields.Field):
getter = getter_generator(trans_name, fname)
setattr(main_cls, 'get_' + fname, getter)

setter = setter_generator(trans_name, fname)
setattr(main_cls, 'set_' + fname, setter)

setattr(main_cls, fname, property(getter, setter, doc=fname))

trans_attrs = cls.__dict__.copy()
trans_attrs['Meta'] = TransMeta
trans_attrs['language'] = models.IntegerField(blank=False, null=False, core=True)
trans_attrs['master'] = models.ForeignKey(main_cls, blank=False, null=False,
edit_inline=TransBoundRelatedObject,
num_in_admin=LANGUAGE_CNT,
min_num_in_admin=LANGUAGE_CNT,
num_extra_on_change=0)

trans_model = ModelBase(trans_name, (models.Model,), trans_attrs)

def get_translation(self, language_id):
return getattr(self, trans_name.lower() + '_set').get(language=language_id)

main_cls.get_translation = get_translation

# delay the creation of the *Translation until the master model is
# fully created
connect(finish_multilingual_class, signal=signals.class_prepared,
sender=main_cls, weak=False)

# modify ModelBase.__new__ so that it understands how to handle the
# 'Translation' inner class
_old_new = ModelBase.__new__
def multilingual_modelbase_new(cls, name, bases, attrs):
if 'Translation' in attrs:
attrs['Translation'].contribute_to_class = classmethod(translation_contribute_to_class)
return _old_new(cls, name, bases, attrs)
setattr(ModelBase, '__new__', staticmethod(multilingual_modelbase_new))
[/python]

There are at least two ugly parts: the LANGUAGES array and overriding ModelBase.__new__. The former lets me use lots of standard admin machinery while the latter makes the library as simple to use as possible. The multilingual app needs to appear in INSTALLED_APPS before any app with translatable models.

On the other hand, it really shows the nice thing about Django: you can do a lot without changing its source code.

File: elksoft/templates/admin/edit_inline_translations.html

[html]
{% load admin_modify %}
{% load multilingual_tags %}


{% for fcw in bound_related_object.form_field_collection_wrappers %}

Language: {{ forloop.counter|language_name }}

{% if bound_related_object.show_url %}{% if fcw.obj.original %}

View on site

{% endif %}{% endif %}
{% for bound_field in fcw.bound_fields %}
{% if bound_field.hidden %}
{% field_widget bound_field %}
{% else %}
{% ifequal bound_field.field.name "language" %}

{% else %}
{% admin_field_line bound_field %}
{% endifequal %}
{% endif %}
{% endfor %}
{% endfor %}

[/html]admin_multilingual_category.png

This template tweaks the admin interface for inline models to hide language identifiers. As a result, you get something like the image to the right instead of standard inline editor.

File: elksoft/multilingual/templatetags/multilingual_tags.py

[python]
from django import template
from django import forms
from django.template import Node, NodeList, Template, Context, resolve_variable
from django.template.loader import get_template, render_to_string
from django.conf import settings
import math
import StringIO
import tokenize

register = template.Library()

from elksoft.multilingual.models import LANGUAGES

def language_code(language_id):
"""
Return the code of the language with id=language_id
"""
return LANGUAGES[language_id - 1][0]

register.filter(language_code)

def language_name(language_id):
"""
Return the name of the language with id=language_id
"""
return LANGUAGES[language_id - 1][1]

register.filter(language_name)
[/python]

And that's it, the whole library for now.

As for the bad news I mentioned earlier, this is what I believe needs to be done before the library gets really useful:

  • caching: right now every access to get_name or set_name accesses the database; ideally the library should retrieve all translations for a single Category with a single SELECT and save them only with Category.save,
  • sorting: name and description for the default language should be appended as extras to every queryset with Category.objects,
  • specifying the default language in querysets and model instances: Category.objects.all().for_language('pl')[0].name should return the value for Polish language,
  • another "backend", one that uses name_en, name_pl, name_de fields in the Category model instead of creating a separate model; this would speed everyting up at the expense of more complicated database updates when you change the list of available languages. The API would not change.

You can get the complete source code here. Let me know what you think.

Update: the newest version of this code is now available at Google Code as django-multilingual. There were multiple improvements and some API changes, so if you want to try it make sure you get the code from there.

RuPy 2007

Saturday, January 20th, 2007

Już wiem, gdzie będę 14 i 15 kwietnia tego roku: na Ruby & Python conference w Poznaniu. Pełen program pojawi się dopiero w marcu, ale już teraz można przejrzeć listę gości.

Zapowiada się dobrze, chociaż na razie wszystkie widoczne prelekcje dotyczą Pythona.