django / mark_safe / translatables
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 underlyingapp_mymodel
table in a database. - That table will consist of two columns:
id
, an automatic integer as primary key (created by default), andmy_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 inhelp_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 <b>help</b> 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)