Each translated field is actually a foreign-key to one... or many Translation
objects. When you interact with your instance containing translations normally,
only one translation is returned, so you see our instance translated in one
language.
Our base manager has a _with_translations()
method that is automatically called
when you instanciate a queryset. It does 2 things:
- Stick an extra lang=lang in the query to prevent query caching from returning objects in the wrong language
- Call
translations.transformers.get_trans()
which does the black magic.
get_trans()
is called, and calls in turn translations.transformer.build_query()
and builds a custom SQL query. This query is the heart of the magic, and it's
build like this:
For each field, join on the translations table, trying to find a translation
in the current language (settings.LANGUAGE_CODE
, which is changed every time
the language is switched in the django process) and then in the language
returned by get_fallback()
(for apps, that's default_locale
)
Only those 2 languages are considered, and a double join + if/else is done every time, for each field.
This query is then ran on the slave (get_trans()
gets a cursor using
connections[multidb.get_slave()]
) to fetch the translations, and some
Translation objects are instanciated from the results and set on the instance(s)
of the original query.
To complete the mechanism, TranslationDescriptor.__get__
returns the
Translation
, and Translations.__unicode__
returns the translated string as you'd
except, making the whole thing transparent.
Everytime you set a translated field to a string value, TranslationDescriptor
__set__
method is called. It determines what method to call (because you can
also assign a dict with multiple translations in multiple languages at the same
time). In this case, it calls translation_from_string()
method, still on the
"hidden" TranslationDescriptor
instance. The current language is passed
at this point, using translation_utils.get_language()
.
From there, translation_from_string()
figures out whether it's a new Translation
of a field we had no translation for, or a new translation of a field we already
had but in a new language, or an update to an existing translation.
It instanciates a new Translation
object with the correct values if necessary,
or just updates the correct one. It then places that object in a queue of
Translation instances to be saved later
When you eventually call obj.save()
, on post_save signal that queue is unqueued
and all Translation objects in it are save()d.
Currently, you can't delete a translation. At all. If you try to do this using
myinstance.mytranslatedfield.delete()
, because of the TranslationDescriptor
magic, myinstance is deleted (yes, really).
If you try to do this by fetching the id and then deleting the Translation
object, using Translation.objects.filter(id=myinstance.mytranslatedfield.id)
, it
fails because... there a FK pointing to that id in myinstance.mytranslatedfield
.
Even if you only delete one translation in one locale and keep one with the same id
in another locale, it will fail. The only way to delete a Translation is to temporary
disable constraints.
Note: this is a huge issue, and it's because we are using MySQL's "feature" of having a FK pointing to something that's not unique.
And even if you manage to delete a Translation, remember how fetching works ? If you deleted a translation that is part of the fallback, then when you fetch that object, depending on your locale you'll get an empty string for that field, even if there are Translation objects in other languages available !
A lot of monkeypatching is used to make the whole thing work, including
something in apps/translations/__init__.py
to bypass errors thrown by django.