################ Django-i18ntools ################ :Author: Benoit Bryon <firstname.lastname@example.org> :Date: 2008-07-07 ************ Introduction ************ Django-i18ntools provides utilities to create translatable models and corresponding translations. Why not use Django-multilingual? ================================ There is at least one project with similar goal: http://code.google.com/p/django-multilingual/ The main difference is about code design. In Django-multilingual, you use inheritance only on the translation model. Then, after model initialization, Django-multilingual performs its magic, modifying the model on-the-fly. Since the model itself is updated, it becomes quite difficult to override translation-related model methods. In this project, inheritance is used at both Translatable and Translation levels. You can easily override methods or reuse parts of code of the Translatable and Translation base classes. ************ Installation ************ Requirements ============ This application requires: * django-1.0: it uses Django's model inheritance * MySQL or SQLite database backend, to use the TranslatableQuerySet.translated() method. This method uses custom SQL that have not been tested in other backends. Please install and configure these applications *before* this one. Get the code ============ The code is not published yet. Ask the author. Settings ======== * add 'i18ntools' to your INSTALLED_APPS * you may customize the LANGUAGES value as following:: ugettext = lambda s: s LANGUAGES = ( ('en', ugettext('English')), ('fr', ugettext('French')), # ... other languages ) * optionally, you can enable automatic fallback translations by setting I18NTOOLS_ENABLE_AUTOMATIC_FALLBACK_TRANSLATIONS to True. Defaults is False. ***** Usage ***** Base untranslatable example model ================================= A good way to introduce i18ntools is to describe a Django model that does not use such functionalities. Consider the following model declaration:: from django.db import models from django.utils.translation import ugettext_lazy as _ class Article(models.Model): publication_date = models.DateField(_('publication date')) title = models.CharField(_('title'), max_length=200) class Meta: verbose_name = _('article') verbose_name_plural = _('articles') def __unicode__(self): return self.title This model does not suit the needs of a multilingual application: title cannot be translated. The following parts of this README will teach you how to create a translatable equivalent. Creating a Translatable and the corresponding Translation model =============================================================== First declare the Translatable class, with only untranslatable fields:: from django.db import models from django.utils.translation import ugettext_lazy as _ from i18ntools.models import Translatable class Article(Translatable): publication_date = models.DateField(_('publication date')) class Meta: verbose_name = _('article') verbose_name_plural = _('articles') def __unicode__(self): try: return self.title except ArticleTranslation.DoesNotExist: return u'%s' % _('Translation not available in active language') Note: in this example, the __unicode__() method does not crash in the case the Article instance have no translation in the active language. Thus, using this pattern is recommended. Then create the Translation class, with translated fields:: from i18ntools.models import Translation class ArticleTranslation(Translation): translated = models.ForeignKey(Article, related_name='translations') title = models.CharField(_('title'), max_length=200) class Meta(Translation.Meta): verbose_name = _('article translation') verbose_name_plural = _('articles translations') def __unicode__(self): return u'%s' % self.title Note: in the "translated" field declaration, do not change the field name nor the related name (keep "translated" and "translations"). Note: the Meta class inherits from Translation.Meta. This propagates an unique key on ('translated', 'language_code'). See explanations about unique keys below. Adding unique keys on translations ================================== Consider the previous Article and ArticleTranslation models. Let's add a slug field to ArticleTranslation:: class ArticleTranslation(Translation): translated = models.ForeignKey(Article, related_name='translations') title = models.CharField(_('title'), max_length=200) slug = models.SlugField(_('slug')) What if the slug have to be unique for the language? Add a unique_together declaration in the ArticleTranslation.Meta class:: class ArticleTranslation(Translation): # ... your fields class Meta(Translation.Meta): # ... other meta information unique_together = Translation.Meta.unique_together + (('slug', 'language_code'), ) This is required because Translation.Meta defines a unique key on ('translated', 'language_code'). "Magic" in Translatable models ============================== The active_translation property and get_active_translation method ----------------------------------------------------------------- The active_translation property is an automatic property defined in Translatable base class. It returns the translation corresponding to the current request language. This property is mainly used internally. In general use cases, you should not use this property or method. Following this guideline will improve the compatibility of monolingual and multilingual applications. See automatic properties below for alternatives. The _install_translated_properties() method ------------------------------------------- The Translatable class overrides the default __init__() method (should it override the __new__() method instead?) and introduces a call to the _install_translated_properties() method. This method adds shortcuts and convenience methods to the instance (it alters the class instead of the instance). Automatic properties -------------------- At __init__(), the _install_translated_properties() have a look at the model's "translations" field (a reverse foreign key) and for each of its fields: * adds a get_translated_%s() getter to the class * adds a set_translated_%s() setter to the class * adds a del_translated_%s() deleter to the class * adds a %s property to the class, which use the previously described method names. As an example, using the previously described Article model:: >>>> article_instance = Article.objects.get(translations__title='this is english') >>>> article.active_translation.title # we recommend not to use this form u'this is english' >>>> article.title # recommended form u'this is english' >>>> article.get_translated_title() # alternative form u'this is english' For each of these operations, if the instance already have the attribute, then nothing is done. As an example, using the previously described Article model:: >>>> article_instance = Article.objects.get(id=123) >>>> article.id 123L >>>> article.active_translation.id 456L >>>> article.get_translated_id() 456L This design makes it possible for you to override the automatic properties and methods in Translatable models. The TranslationNotAvailable exception ===================================== This exception is deprecated. If Translation objects does not exist, then some ObjectDoesNotExist exception is raised. More precisely the actual DoesNotExist exception of the model class is raised (as an example: Article.DoesNotExist rather than generic ObjectDoesNotExist). Warning: by default, only 'active language' translations are considered. So you may catch some ObjectDoesNotExist exceptions in public views or use translated() method of TranslatableManager. Forms ===== This application provides the following form tools in i18ntools.forms: * TranslationInlineFormset: a custom formset, based on BaseInlineFormSet, that verifies that at least one translation is provided. Using django.contrib.admin ========================== This application provides the following admin tools in i18ntools.admin: * TranslationInline: based on django.contrib.admin.StackedInline, uses i18ntools.forms.TranslationInlineFormset. Initiates extra and max_num to the number of available languages. Consider the previously described Article and ArticleTranslation models. The following code can be used in admin.py: from django import forms from django.contrib import admin from i18ntools.admin import TranslationInline from models import Article, ArticleTranslation class ArticleTranslationInline(TranslationInline): model = ArticleTranslation class ArticleAdmin(admin.ModelAdmin): inlines = [ ArticleTranslationInline, ] admin.site.register(Article, ArticleAdmin) Using managers and querysets ============================ This application provides TranslatableManager in i18ntools.managers. The TranslatableManager manager ------------------------------- This manager provides methods and filters to simplify use of Translatable instances and their translations. Because this manager can perform optimizations. It is recommended that you use it (or a subclass) as the default manager of your Translatable models (i.e. the 'objects' property). See explanations below for details. This manager has the following characteristics: * the get_query_set() method returns a TranslatableQuerySet (which resides in i18ntools.query). The select_translation() optimization is enabled by default (see below). * the translated() method is a filter that limit the queryset to instances that do have translations. This method accepts an optional 'is_translated' argument that, set to False, returns only instances which have no translation. This argument defaults to True. * the select_translation() method enables (or disables) optimizations. When enabled (optional parameter 'enable_select_translation' defaults to True), an additional query is performed to fill the cache of translations (currently active translation's cache). It avoids making an additional query to get the active translation of each the instances in the queryset. The TranslatableQuerySet class ------------------------------ The TranslatableQuerySet class is used and returned by TranslatableManager. It implements the following additional methods: * translated() * select_translation(enable_select_translation=True) Because the queryset defines them, these methods can be chained:: Article.objects.all().translated().select_translation(False) Automatic fallback translations =============================== By default, when some content has no translation in the active language, then it does not appear in querysets. When you try to access translated properties of such an instance, you get an ObjectDoesNotExist exception. In some cases you want i18ntools to try returning a translation in an alternative language if no translation exists in the active language. The I18NTOOLS_ENABLE_AUTOMATIC_FALLBACK_TRANSLATIONS setting can be set to True to activate this behaviour. By default, I18NTOOLS_ENABLE_AUTOMATIC_FALLBACK_TRANSLATIONS is set to False. You can set I18NTOOLS_ENABLE_AUTOMATIC_FALLBACK_TRANSLATIONS in your project's settings. You can access I18NTOOLS_ENABLE_AUTOMATIC_FALLBACK_TRANSLATIONS through i18ntools.settings. Keep in mind that, in most cases, automatic fallback translations are not fair for the user. When you cannot understand some language, you do not want to view a mix of translated/untranslated content on a web page. So, we recommend displaying a message like "The content you requested does not exists in [the requested language]. You can view alternative translations in the following languages: (list of languages with links)". ****************** Sample application ****************** A sample application is distributed with this package: i18ntools_sample. Its code illustrates the use of i18ntools within the real world. The i18ntools test unit use this sample application to make sure that the concepts and tools developed here are fully compatible with real life applications. ****************** Note to developers ****************** If you want to contribute to the code of i18ntools, you should add the i18ntools_sample application to your development project, so that you can test the behavior of i18ntools in a real-life application. If you feel that pertinent use cases are not covered by i18ntools_sample, do not hesitate to add models, views, templates or whatever... When performing changes, do not forget to test both i18ntools and i18ntools_sample. **** TODO **** * Add support for settings about language priority and language fallback behaviour (should we return an alternative?). * Make it possible to manage translations as separate objects, within a translation workflow as example. This requires a non abstract Translation model. * Enhance the Translatable and Translation APIs (automatic properties), at least by providing methods to get a translation instance or property by specifying the language (e.g. article.get_translated_title('en')) * Optimize the TranslatableQuerySet.select_translation() method. Both Python and SQL optimizations may be done. * Enhance base Admin classes. * Enhance base Form classes. * Use the django.db.models.signals.class_prepared signal rather than Translatable.__init__() to install the automatic properties. The class_prepared signal can be connected to the Translation subclass model by using a custom field ("TranslatableForeignKey" for example) that have a contribute_to_class() method. The signal connection happens in the contribute_to_class() method. It cannot occur before the model definition because in class_prepared, the 'sender' parameter must be a class, not a class name. It cannot occur after model definition, because the class_prepared signal is sent by the Model's metaclass (ModelBase). The Translation subclass must be used rather than the Translatable one. This is because in Translation you can access the Translatable through the TranslatableForeignKey found in _meta.fields, while in Translatable you cannot access the 'translations' reverse property through the model (it must be accessed through instance). * Make the "post-class_prepared" magic overridable or configurable. This could be achieved by using a class with methods rather than several global functions and some settings. * Add enable_select_translation and active_language as internal properties in TranslatableQueryset. This would make it possible to configure and optimize a queryset for a given language, which may not be the global active one. * Remove deprecated TranslatablePublicManager and TranslationPublicManager classes. * Add 'language' input parameter to the TranslatableQueryset.translated() method. This would make it possible to locally override the active language. * 'language' parameter in Translatable.translated() could also be an iterable to limit queryset to several languages (not only one). * 'language' parameter in Translatable.translated() could also be a callable.