django / mark_safe / translatables

django / mark_safe / translatables

  • Written by
    Walter Doekes
  • Published on

Look at this snippet of Django code in models.py, and in particular the help_text bit:

from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.safestring import mark_safe

class MyModel(models.Model):
    my_field = models.CharField(max_length=123,
                                help_text=mark_safe(_('Some <b>help</b> text.')))

For those unfamiliar with Django. A quick run-down:

  • The definition of MyModel creates a mapping between the MyModel class and a underlying app_mymodel table in a database.
  • That table will consist of two columns: id, an automatic integer as primary key (created by default), and my_field, a varchar/text field of at most 123 characters.
  • With minimal effort a HTML form can be generated from this. That form will show my_field as a text input box and near it the text we defined in help_text.
  • The _()-function has the gettext functions run over it so the text can be served in different languages with minimal effort.
  • The mark_safe()-function tells the template rendererer that this output is already safe to use in HTML. Without it, the user would see: Some &lt;b&gt;help&lt;/b&gt; text.

Unfortunately this doesn’t do what you would expect.

Let’s examine why.

There is a reason why we use the ugettext_lazy wrapper in models.py. This code is executed once at startup / first run, and the language that was selected at that time would be substituted if we used the non-lazy ugettext. The lazy variant makes sure the substitution takes place at the last possible time.

mark_safe forces the translation to happen immediately.

In the best case that means someone else can get the help text served in the wrong language. In the worst case, you get a recursive import when the translation routines attempt to import all INSTALLED_APPS while looking for locale files. Your MyModel might be referenced from one of those apps. The result: recursion and a resulting ImportError.

...
File "someapp/models.py", line 5, in <module>
    class MyModel(models.Model):
File "someapp/models.py", line 6, in <module>
    my_field = models.CharField(max_length=123,
File "django/utils/safestring.py", line 101, in mark_safe
    return SafeUnicode(s)
...
File "django/utils/translation/trans_real.py", line 180, in _fetch
    app = import_module(appname)
File "django/utils/importlib.py", line 35, in import_module
    __import__(name)
...
ImportError: cannot import name MyModel

Lessons learnt: if you’re using translations then don’t call mark_safe on anything until it’s view time.

In this case, we would fix it by adding the mark_safe call to the Form constructor. We know that that is run for every form instantiation, so that’s late enough.

class MyModelForm(forms.ModelForm):
    class Meta:
        model = MyModel

    def __init__(self, *args, **kwargs):
        super(MyModelForm, self).__init__(*args, **kwargs)
        self.fields['my_field'].help_text = mark_safe(self.fields['my_field'].help_text)

But suggestions for prettier solutions are welcome.

Update 2013-03-21

The Django 1.4 docs provide the better solution:

from django.utils import six  # Python 3 compatibility
from django.utils.functional import lazy
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _

mark_safe_lazy = lazy(mark_safe, six.text_type)

Back to overview Newer post: ubuntu / sip video / softphone Older post: ipython classic mode / precise pangolin