Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@diox
Created September 16, 2013 14:47
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save diox/e99d74a8e445032b49fa to your computer and use it in GitHub Desktop.
Save diox/e99d74a8e445032b49fa to your computer and use it in GitHub Desktop.
Translations in Zamboni

Translations in Zamboni

High-level overview

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.

Behind the scenes

When fetching

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.

When setting

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.

When deleting

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 !

Additional tricks

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment