Overview

################
Django-i18ntools
################

:Author: Benoit Bryon <benoit@marmelune.net>
: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.