Inner workings¶
Django-modeltrans uses a django.contrib.postgres.JSONField
to store field
translations in the table and adds some augmentation to the queries made by
Django’s QuerySet methods to allow transparent use of the translated values.
The inner workings are illustrated using this model:
class Blog(models.Model):
title = models.CharField(max_length=255)
body = models.TextField(null=True)
i18n = TranslationField(fields=("title", "body"))
When creating an object, translated fields in the constructor are transformed into a value in the i18n field. So the following two calls are equivalent:
Blog.objects.create(title="Falcon", title_nl="Valk", title_de="Falk")
Blog.objects.create(title="Falcon", i18n={"title_nl": "Valk", "title_de": "Falk"})
So adding a translated field does not need any migrations: it just requires
adding a key to the i18n
field.
When selecting objects django-modeltrans replaces any occurrence of a translated field with the appropriate JSONB key get operation:
Blog.objects.filter(title_nl="Valk")
# SELECT ... FROM "app_blog" WHERE (app_blog.i18n->>'title_nl')::varchar(255) = 'Valk'
Blog.objects.filter(title_nl__contains="a")
# SELECT ... FROM "app_blog" WHERE (app_blog.i18n->>'title_nl')::varchar(255) LIKE '%a%'
In addition to that, you can use <fieldname>_i18n
to filter on. That will use
COALESCE
to look in both the currently active language and the default
language:
from django.utils.translation import override
with override("nl"):
Blog.objects.filter(title_i18n="Valk")
# SELECT ... FROM "app_blog"
# WHERE COALESCE((app_blog.i18n->>'title_nl'), "app_blog"."title") = 'Valk'
Model objects containing translated fields get virtual fields for each field/
language combination plus a field which always returns the active language.
In the example, we have configured 3 translation languages: ('nl', 'de', 'fr')
resulting in 4 virtual fields for each original field:
b = Blog.objects.create(title='Falcon', title_nl='Valk', title_de='Falk')
b._meta.get_fields()
(<django.db.models.fields.AutoField: id>,
<django.db.models.fields.CharField: title>,
<django.db.models.fields.TextField: body>,
<django.db.models.fields.related.ForeignKey: category>,
<modeltrans.fields.TranslationField: i18n>,
<modeltrans.fields.TranslatedCharField: title_i18n>,
<modeltrans.fields.TranslatedCharField: title_en>,
<modeltrans.fields.TranslatedCharField: title_nl>,
<modeltrans.fields.TranslatedCharField: title_de>,
<modeltrans.fields.TranslatedCharField: title_fr>,
<modeltrans.fields.TranslatedTextField: body_i18n>,
<modeltrans.fields.TranslatedTextField: body_en>,
<modeltrans.fields.TranslatedTextField: body_nl>,
<modeltrans.fields.TranslatedTextField: body_de>,
<modeltrans.fields.TranslatedTextField: body_fr>)
Each virtual field for an explicit language will only return a value if that language is defined:
print(b.title_nl, b.title_fr)
# 'Valk', None
The virtual field <field>_i18n
returns the translated value for the current
active language and falls back to the language in LANGUAGE_CODE
:
with override("nl"):
print(b.title_i18n)
# 'Valk'
with override("de"):
print(b.title_i18n)
# 'Falk'
with override("fr"):
print(b.title_i18n)
# 'Falcon' (no french translation available, falls back to LANGUAGE_CODE)
Django-modeltrans also allows ordering on translated values. Ordering on
<field>_i18n
probably makes most sense, as it more likely that there is a
value to order by:
with override("de"):
qs = Blog.objects.order_by("title_i18n")
# SELECT ...,
# FROM "app_blog"
# ORDER BY COALESCE((app_blog.i18n->>'title_de'), "app_blog"."title") ASC
Results in the following ordering:
title_i18n title_en title_nl title_de
------------ ------------ ------------ ------------
Crayfish Crayfish
Delfine Dolphin Dolfijn Delfine
Dragonfly Dragonfly Libellen
Duck Duck Eend
Falk Falcon Valk Falk
Frog Frog Kikker
Kabeljau Cod Kabeljau
Toad Toad Pad
As you can see, although the german translations are not complete, ordering on
title_i18n
still results in a useful ordering.
Note
These examples assume the default setting for MODELTRANS_FALLBACK
.
If you customize that setting, it can get slightly more complex, resulting
in more than 2 arguments to the COALESCE
function.