Thursday, July 10, 2008

User Editable String Translations with Django.

I was recently tasked with creating a way for a django admin user to edit arbitrary strings of text on their site - Something similar to django's built in internationalization support, but with a proper model and the ability to manage the translations from the admin section. I'm relatively new to django, so this is probably not the best way to do any of this, it's simply the best I could come up with, suggestions are greatly appreciated.

My first thought was to store the translations as normal string with python string-formatters, something like "Hello %(username)s welcome to the site", but I quickly found some problems with that, there was no easy way to render them from the template system, they syntax is ugly, and it doesn't offer much in the way of formatting the variables. I also wanted a way to group the translations logically, so that I could query for a set of translations relevant to a specific view or app and cache them all at once, and to allow filtering in the admin section. I decided the django template engine was exactly what I was looking for, and what I came up with in the end is this:


from django.db import models
from django.core.cache import cache

class TranslationGroup(models.Model):
name = models.CharField(max_length=255)

def __unicode__(self):
return self.name

class Meta:
verbose_name = 'Translation Group'

class Admin:
pass


class Translation(models.Model):
groups = models.ManyToManyField(TranslationGroup, filter_interface=models.HORIZONTAL)
key = models.CharField(max_length=255,db_index=True)
template = models.TextField()
description = models.TextField(blank=True)

def __unicode__(self):
return self.key

def save(self):
"""Ensures the translation cache is updated
after editing a translation

"""
super(Translation, self).save()
for c in self.groups.all():
cache.delete('trans_group_%s' % (c.name,))

class Meta:
verbose_name = 'Translation String'

class Admin:
fields = (
(None, {
'fields': ('key', 'groups', ('template', 'description'))
}),
)

list_display = ('key','template')
list_filter = ('groups',)
search_fields = ('key', 'template')


Here we have a Translation model, and a TranslationGroup model. the latter of which simply acts like a tag or label.

I also wrote a small utility method which gets all translations in a group or list of groups, compiles the templates and returns a dictionary mapping keys to compiled django templates which can be merged into a Context.


from models import *
from django.core.cache import cache
from django.template import Template

def get_strings(categories=None):
"""Returns a dictionary of translation keys
to compiled template objects for every Translation
object belonging to the categories specified.

categories -- an string, or iterable of strings for category names

"""
strings = {}
if categories:
if not isinstance(categories,(list,tuple)):
categories = (categories,)

cats = TranslationGroup.objects.select_related('translation').filter(name__in=categories)
if cats:
for c in cats:
_strings = cache.get('trans_group_%s'%(c.name,))
if not _strings:
_strings={}
for s in c.translation_set.all():
if s.key not in _strings:
_strings[s.key] = Template(s.template)
cache.set('trans_group_%s'%(c.name,), _strings,60*60*24*7) #Cache for a really long time.
strings.update(_strings)
return strings

Finally I needed a way to render these templates from within the template system, again I'm not sure if this was the best way to do this, but I created a custom template tag called trans which takes a compiled django template as its only argument and renders it in the current context. This required creating a new subclass of Node and a method which returns this new node type.


from django import template

register = template.Library()

@register.tag('trans')
def do_translation(parser,token):
try:
tag_name, tpl = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError, "%s requires exactly 1 argument" % (token.contents.split()[0],)
return TranslatedNode(tpl)


class TranslatedNode(template.Node):
def __init__(self,tpl):
self.tpl = template.Variable(tpl)

def render(self,context):
try:
t = self.tpl.resolve(context)
except template.VariableDoesNotExist:
return ""
if not isinstance(t, template.Template):
return ""
return t.render(context)

Now to use it in a view you would do something like this.


from trans.models import *
from django.shortcuts import render_to_response
from trans.utils import get_strings

def my_view(request):
s = get_strings('example_group')
s.update({'username':'awes0med00d_1337'})
return render_to_response('index.html', Context(s))


and the corresponding index.html

{% load translations %}
<html>
<head>
<title>Homepage</title>
</head>
<body>
<p>{% trans greeting %}</p>
</body>
</html>

In the above example greeting is the Translation key. it was loaded because it belongs to the TranslationGroup called 'example' the load translations loads the custom tags which I've stored in templatetags/translations.py

And that's about it. Any feedback would be greatly appreciated.

Thursday, May 8, 2008

First Post!

I haven't got time to write anything right now... but I really want to start keeping track of my various projects, since more often than not they end up on the back-burner for long periods of time. My latest endeavour has been Ubuntu on the PS3, and I expect to be blogging quite a bit about that topic as it seems to be a dead(ish) scene right now. I also expect to write a lot about my python experiments, half-assed and half-finished games, explorations of different programming languages and techniques. It should prove to be great fun. or maybe this will be my last post... I guess we'll see.