Kostas Papadimitriou avatar Kostas Papadimitriou committed c360495 Merge

merged from svn

Comments (0)

Files changed (11)

+  ADDED: Support for related fields - ForeignKey, ManyToManyField and
+         OneToOneField.
+         (resolves issue 15)
+
+  FIXED: TranslationField pre_save does not get the default language
+         correctly.
+         (thanks to jaap, resolves issue 31)
+
+v0.2
+====
+Date: 2010-06-15
+Packaged from revision 57.
+
+  ADDED: Support for admin prepopulated_fields.
+         (resolves issue 21)
+  ADDED: Support for admin list_editable.
+         (thanks carl.j.meyer, resolves issue 20)
+  ADDED: Preserve the formfield widget of the translated field.
+         (thanks piquadrat)
+  ADDED: Initial support for django-south.
+         (thanks andrewgodwin, resolves issue 11)
+  ADDED: Support for admin inlines, common and generic.
+         (resolves issue 12 and issue 18)
+
+  FIXED: Admin form validation errors with empty translated values and
+         unique=True.
+         (thanks to adamsc, resolves issue 26)
+  FIXED: Mangling of untranslated prepopulated fields.
+         (thanks to carl.j.meyer, resolves issue 25)
+  FIXED: Verbose names of translated fields are not translated.
+         (thanks to carl.j.meyer, resolves issue 24)
   FIXED: Race condition between model import and translation registration in
          production by ensuring that models are registered for translation
          before TranslationAdmin runs.
   FIXED: Removed unused dependency to content type which can break syncdb.
          (thanks carl.j.meyer, resolves issue 1)
 
-  ADDED: Support for admin prepopulated_fields.
-         (resolves issue 21)
-  ADDED: Support for admin list_editable.
-         (thanks carl.j.meyer, resolves issue 20)
-  ADDED: Preserve the formfield widget of the translated field.
-         (thanks piquadrat)
-  ADDED: Initial support for django-south.
-         (thanks andrewgodwin, resolves issue 11)
-  ADDED: Support for admin inlines, common and generic.
-         (resolves issue 12 and issue 18)
-
-
 v0.1
 ====
-
 Date: 2009-02-22
-
 Initial release packaged from revision 19.
-Copyright (c) 2009, Peter Eschler
+Copyright (c) 2010, 2009, Peter Eschler, Dirk Eschler
 All rights reserved.
 
-Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
 
-    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
-    * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
-    * Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of the author nor the names of its contributors may be
+      used to endorse or promote products derived from this software without
+      specific prior written permission.
 
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Metadata-Version: 1.0
+Name: django-modeltranslation
+Version: 0.2
+Summary: Translates Django models using a registration approach.
+Home-page: http://code.google.com/p/django-modeltranslation/
+Author: Peter Eschler,
+        Dirk Eschler
+Author-email: "Peter Eschler" <peschler@googlemail.com>,
+              "Dirk Eschler" <eschler@gmail.com>
+License: New BSD
+Description: The modeltranslation application can be used to translate dynamic
+             content of existing models to an arbitrary number of languages
+             without having to change the original model classes. It uses a
+             registration approach (comparable to Django's admin app) to be
+             able to add translations to existing or new projects and is fully
+             integrated into the Django admin backend.
+
+             The advantage of a registration approach is the ability to add
+             translations to models on a per-project basis. You can use the
+             same app in different projects, may they use translations or not,
+             and you never have to touch the original model class.
+Platform: UNKNOWN
+Keywords: django translation i18n

docs/modeltranslation/modeltranslation-0.2.txt

+.. _ref-topics-modeltranslation:
+
+===================
+ Model translation
+===================
+
+.. admonition:: About this document
+
+    This document provides an introduction to the modeltranslation application.
+
+.. currentmodule:: modeltranslation.models
+.. moduleauthor:: Peter Eschler <peschler@googlemail.com>,
+                  Dirk Eschler <eschler@gmail.com>
+
+The modeltranslation application can be used to translate dynamic content of
+existing models to an arbitrary number of languages without having to change
+the original model classes. It uses a registration approach (comparable to
+Django's admin app) to be able to add translations to existing or new projects
+and is fully integrated into the Django admin backend.
+
+The advantage of a registration approach is the ability to add translations to
+models on a per-project basis. You can use the same app in different projects,
+may they use translations or not, and you never have to touch the original
+model class.
+
+.. contents::
+
+
+Features
+========
+
+- Unlimited number of target languages
+- Add translations without changing existing models
+- Django admin support
+- Supports inherited models
+
+
+Installation
+============
+
+To install the application please follow these steps. Each step is described
+in detail in the following sections:
+
+1. Add the ``modeltranslation`` app to the ``INSTALLED_APPS`` variable of your
+   project's ``settings.py``.
+
+2. Configure your languages in the ``settings.py``.
+
+3. Create a ``translation.py`` in your project directory and register
+   ``TranslationOptions`` for every model you want to translate.
+
+4. Configure the ``TRANSLATION_REGISTRY`` variable in your ``settings.py``.
+
+5. Sync the database using ``manage.py syncdb`` (note that this only applies
+   if the models registered in the ``translations.py`` did not have been
+   synced to the database before. If they did - read further down what to do
+   in that case.
+
+Configure the project's ``settings.py``
+---------------------------------------
+The following variables have to be added to or edited in the project's
+``settings.py``:
+
+**settings.INSTALLED_APPS**
+
+Make sure that the ``modeltranslation`` app is listed in your
+``INSTALLED_APPS`` variable::
+
+    INSTALLED_APPS = (
+        ...
+        'modeltranslation',
+        ....
+    )
+
+Also make sure that the app can be found on a path contained in your
+``PYTHONPATH`` environment variable.
+
+**settings.LANGUAGES**
+
+The LANGUAGES variable must contain all languages used for translation. The
+first language is treated as the *default language*.
+
+The modeltranslation application uses the list of languages to add localized
+fields to the models registered for translation. To use the languages ``de``
+and ``en`` in your project, set the settings.LANGUAGES variable like this
+(where ``de`` is the default language)::
+
+    gettext = lambda s: s
+    LANGUAGES = (
+        ('de', gettext('German')),
+        ('en', gettext('English')),
+    )
+
+Note that the ``gettext`` lambda function is not a feature of the
+modeltranslation app, but rather required for Django to be able to
+(statically) translate the verbose names of the languages using the standard
+``i18n`` solution.
+
+**settings.TRANSLATION_REGISTRY**
+
+In order to be able to import the project's ``translation.py`` registration
+file the ``TRANSLATION_REGISTRY`` must be set to a value in the form
+``<PROJECT_MODULE>.translation``. E.g. if your project is located in a folder
+named ``myproject`` the ``TRANSLATION_REGISTRY`` must be set like this::
+
+    TRANSLATION_REGISTRY = "myproject.translation"
+
+
+Registering models and their fields for translation
+---------------------------------------------------
+The ``modeltranslation`` app can translate ``CharField`` and ``TextField``
+based fields of any model class. For each model to translate a translation
+option class containg the fields to translate is registered with the
+``modeltranslation`` app.
+
+Registering models and their fields for translation requires the following
+steps:
+
+1. Create a ``translation.py`` in your project directory.
+2. Create a translation option class for every model to translate.
+3. Register the model and the translation option class at the
+   ``modeltranslation.translator.translator``
+
+The ``modeltranslation`` application reads the ``translation.py`` file in your
+project directory thereby triggering the registration of the translation
+options found in the file.
+
+A translation option is a class that declares which fields of a model to
+translate. The class must derive from ``modeltranslation.ModelTranslation``
+and it must provide a ``fields`` attribute storing the list of fieldnames. The
+option class must be registered with the
+``modeltranslation.translator.translator`` instance.
+
+.. note:: In contrast to the Django admin application which looks for
+          ``admin.py`` files in the project **and** application directories,
+          the modeltranslation app looks only for one ``translation.py`` file in
+          the project directory.
+
+To illustrate this let's have a look at a simple example using a ``News``
+model. The news in this example only contains a ``title`` and a ``text`` field.
+Instead of a news, this could be any Django model class::
+
+    class News(models.Model):
+        title = models.CharField(max_length=255)
+        text = models.TextField()
+
+In order to tell the ``modeltranslation`` app to translate the ``title`` and
+``text`` field, create a ``translation.py`` file in your project directory and
+add the following::
+
+    from modeltranslation.translator import translator, TranslationOptions
+    from some.news.models import News
+
+    class NewsTranslationOptions(TranslationOptions):
+        fields = ('title', 'text',)
+
+    translator.register(News, NewsTranslationOptions)
+
+Note that this does not require to change the ``News`` model in any way, it's
+only imported. The ``NewsTranslationOptions`` derives from
+``TranslationOptions`` and provides the ``fields`` attribute. Finally the model
+and it's translation options are registered at the ``translator`` object.
+
+At this point you are mostly done and the model classes registered for
+translation will have been added some auto-magical fields. The next section
+explains how things are working under the hood.
+
+Changes automatically applied to the model class
+------------------------------------------------
+After registering the ``News`` model for transaltion an SQL dump of the
+News app will look like this::
+
+    $ ./manage.py sqlall news
+    BEGIN;
+    CREATE TABLE `news_news` (
+        `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
+        `title` varchar(255) NOT NULL,
+        `title_de` varchar(255) NULL,
+        `title_en` varchar(255) NULL,
+        `text` longtext NULL,
+        `text_de` longtext NULL,
+        `text_en` longtext NULL,
+    )
+    ;
+    ALTER TABLE `news_news` ADD CONSTRAINT page_id_refs_id_3edd1f0d FOREIGN KEY (`page_id`) REFERENCES `page_page` (`id`);
+    CREATE INDEX `news_news_page_id` ON `news_news` (`page_id`);
+    COMMIT;
+
+Note the ``title_de``, ``title_en``, ``text_de`` and ``text_en`` fields which
+are not declared in the original News model class but rather have been added by
+the modeltranslation app. These are called *translation fields*. There will be
+one for every language in your project's ``settings.py``.
+
+The name of these additional fields is build using the original name of the
+translated field and appending one of the language identifiers found in the
+``settings.LANGUAGES``.
+
+As these fields are added to the registered model class as fully valid Django
+model fields, they will appear in the db schema for the model although it has
+not been specified on the model explicitly.
+
+.. _set_language: http://docs.djangoproject.com/en/dev/topics/i18n/#the-set-language-redirect-view
+
+If you are starting a fresh project and have considered your translation needs
+in the beginning then simply sync your database and you are ready to use
+the translated models.
+
+In case you are translating an existing project and your models have already
+been synced to the database you will need to alter the tables in your database
+and add these additional translation fields. Note that all added fields are
+declared ``null=True`` not matter if the original field is required. In other
+words - all translations are optional. To populate the default translation
+fields added by the ``modeltranslation`` application you can use the
+``update_translation_fields`` command below. See the `The
+update_translation_fields command` section for more infos on this.
+
+
+Accessing translated and translation fields
+===========================================
+The ``modeltranslation`` app changes the behaviour of the translated fields. To
+explain this consider the News example again. The original ``News`` model
+looked like this::
+
+    class News(models.Model):
+        title = models.CharField(max_length=255)
+        text = models.TextField()
+
+Now that it is registered with the ``modeltranslation`` app the model looks
+like this - note the additional fields automatically added by the app::
+
+    class News(models.Model):
+        title = models.CharField(max_length=255)  # original/translated field
+        title_de = models.CharField(null=True, blank=True, max_length=255)  # default translation field
+        title_en = models.CharField(null=True, blank=True, max_length=255)  # translation field
+        text = models.TextField() # original/translated field
+        text_de = models.TextField(null=True, blank=True) # default translation field
+        text_en = models.TextField(null=True, blank=True) # translation field
+
+The example above assumes that the default language is ``de``, therefore the
+``title_de`` and ``text_de`` fields are marked as the *default translation
+fields*. If the default language is ``en``, the ``title_en`` and ``text_en``
+fields would be the *default translation fields*.
+
+Rules for translated field access
+---------------------------------
+So now when it comes to setting and getting the value of the original and the
+translation fields the following rules apply:
+
+**Rule 1**
+
+Reading the value from the original field returns the value translated to the
+*current language*.
+
+**Rule 2**
+
+Assigning a value to the original field also updates the value in the
+associated default translation field.
+
+**Rule 3**
+
+Assigning a value to the default translation field also updates the original
+field - note that the value of the original field will not be updated until the
+model instance is saved.
+
+**Rule 4**
+
+If both fields - the original and the default translation field - are updated
+at the same time, the default translation field wins.
+
+
+Examples for translated field access
+------------------------------------
+Because the whole point of using the ``modeltranslation`` app is translating
+dynamic content, the fields marked for translation are somehow special when it
+comes to accessing them. The value returned by a translated field is depending
+on the current language setting. "Language setting" is referring to the Django
+`set_language`_ view and the corresponding ``get_lang`` function.
+
+Assuming the current language is ``de`` in the News example from above, the
+translated ``title`` field will return the value from the ``title_de`` field::
+
+    # Assuming the current language is "de"
+    n = News.objects.all()[0]
+    t = n.title # returns german translation
+
+    # Assuming the current language is "en"
+    t = n.title # returns english translation
+
+This feature is implemented using Python descriptors making it happen without
+the need to touch the original model classes in any way. The descriptor uses
+the ``django.utils.i18n.get_language`` function to determine the current
+language.
+
+
+Django admin backend integration
+================================
+In order to be able to edit the translations via the admin backend you need to
+register a special admin class for the translated models. The admin class must
+derive from ``modeltranslation.admin.TranslationAdmin`` which does some funky
+patching on all your models registered for translation::
+
+    from django.contrib import admin
+    from modeltranslation.admin import TranslationAdmin
+
+    class NewsAdmin(TranslationAdmin):
+        list_display = ('title',)
+
+    admin.site.register(News, NewsAdmin)
+
+Tweaks applied to the admin
+---------------------------
+
+The ``TranslationAdmin`` class does only implement one special method which is
+``def formfield_for_dbfield(self, db_field, **kwargs)``. This method does the
+following:
+
+1. Removes the original field from every admin form by setting it
+   ``editable=False``.
+2. Copies the widget of the original field to each of it's translation fields.
+3. Checks if the - now removed - original field was required and if so makes the
+   default translation field required instead.
+
+TranslationAdmin in combination with other admin classes
+--------------------------------------------------------
+If there already exists a custom admin class for a translated model and you
+don't want or can't edit that class directly there is another solution.
+
+Taken the News example let's say there is a ``NewsAdmin`` class defined by the
+News app itself. This app is not yours or you don't want to touch it at all,
+but it has this nice admin class::
+
+    class NewsAdmin(model.Admin):
+        def formfield_for_dbfield(self, db_field, **kwargs):
+            # does some funky stuff with the formfield here
+
+So a first attempt might be to create your own admin class which subclasses
+``NewsAdmin`` and ``TranslationAdmin`` to combine stuff like so::
+
+    class MyTranslatedNewsAdmin(NewsAdmin, TranslationAdmin):
+        pass
+
+Unfortunately this won't work because Python can only execute one of the
+``formfield_for_dbfield`` methods. Since both admin class implement this method
+Python must make a decision and it chooses the first class ``NewsAdmin``. The
+functionality from ``TranslationAdmin`` will not be executed and translation in
+the admin will not work for this class.
+
+But don't panic, here's a solution::
+
+    class MyTranslatedNewsAdmin(NewsAdmin, TranslationAdmin):
+        def formfield_for_dbfield(self, db_field, **kwargs):
+            field = super(MyTranslatedNewsAdmin, self).formfield_for_dbfield(db_field, **kwargs)
+            self.patch_translation_field(db_field, field, **kwargs)
+            return field
+
+This implements the ``formfield_for_dbfield`` such that both functionalities
+will be executed. The first line calls the superclass method which in this case
+will be the one of ``NewsAdmin`` because it is the first class inherited from.
+The ``TranslationAdmin`` capsulates all it's functionality in the
+``patch_translation_field(db_field, field, **kwargs)`` method and the
+``formfield_for_dbfield`` implementation of the ``TranslationAdmin`` class
+simply calls it. You can copy this behaviour by calling it from a
+custom admin class and that's done in the example above. After that the
+``field`` is fully patched for translation and finally returned.
+
+
+Inlines
+-------
+*New in development version*
+Support for tabular and stacked inlines, common and generic ones.
+
+A translated inline must derive from one of the following classes:
+
+ * `modeltranslation.admin.TranslationTabularInline`
+ * `modeltranslation.admin.TranslationStackedInline`
+ * `modeltranslation.admin.TranslationGenericTabularInline`
+ * `modeltranslation.admin.TranslationGenericStackedInline`
+
+Just like `TranslationAdmin` these classes implement a special method
+`def formfield_for_dbfield(self, db_field, **kwargs)` which does all the
+patching.
+
+For our example we assume that there is new model called Image. It's
+definition is left out for simplicity. Our News model inlines the new model:
+
+{{{
+from django.contrib import admin
+from modeltranslation.admin import TranslationTabularInline
+
+class ImageInline(TranslationTabularInline):
+    model = Image
+
+class NewsAdmin(admin.ModelAdmin):
+    list_display = ('title',)
+    inlines = [ImageInline,]
+
+admin.site.register(News, NewsAdmin)
+}}}
+
+*Note:* In this example only the Image model is registered in translation.py.
+It's not a requirement that `NewsAdmin` derives from `TranslationAdmin` in
+order to inline a model which is registered for translation.
+
+In this more complex example we assume that the News and Image models are
+registered in translation.py. The News model has an own custom admin class and
+the Image model an own generic stacked inline class. It uses the technique
+described in [InstallationAndUsage#TranslationAdmin_in_combination_with_other_admin_classes TranslationAdmin in combination with other admin classes].:
+
+{{{
+from django.contrib import admin
+from modeltranslation.admin import TranslationAdmin, TranslationGenericStackedInline
+
+class TranslatedImageInline(ImageInline, TranslationGenericStackedInline):
+    model = Image
+
+class TranslatedNewsAdmin(NewsAdmin, TranslationAdmin):
+    def formfield_for_dbfield(self, db_field, **kwargs):
+        field = super(TranslatedNewsAdmin, self).formfield_for_dbfield(db_field, **kwargs)
+        self.patch_translation_field(db_field, field, **kwargs)
+        return field
+
+    inlines = [TranslatedImageInline,]
+
+admin.site.register(News, NewsAdmin)
+}}}
+
+
+The ``update_translation_fields`` command
+=========================================
+In case the modeltranslation app was installed on an existing project and you
+have specified to translate fields of models which are already synced to the
+database, you have to update your database schema manually.
+
+Unfortunately the newly added translation fields on the model will be empty
+then, and your templates will show the translated value of the fields (see
+Rule 1 below) which will be empty in this case. To correctly initialize the
+default translation field you can use the ``update_translation_fields``
+command::
+
+    manage.py update_translation_fields
+
+Taken the News example from above this command will copy the value from the
+news object's ``title`` field to the default translation field ``title_de``.
+It only does so if the default translation field is empty otherwise nothing
+is copied.
+
+.. note:: The command will examine your ``settings.LANGUAGES`` variable and the
+          first language declared there will be used as the default language.
+
+All translated models (as specified in the project's ``translation.py`` will be
+populated with initial data.
+
+
+Caveats
+=======
+Consider the following example (assuming the default lanuage is ``de``)::
+
+    >>> n = News.objects.create(title="foo")
+    >>> n.title
+    'foo'
+    >>> n.title_de
+    >>>
+
+Because the original field ``title`` was specified in the constructor it is
+directly passed into the instance's ``__dict__`` and the descriptor which
+normally updates the associated default translation field (``title_de``) is not
+called. Therefor the call to ``n.title_de`` returns an empty value.
+
+Now assign the title, which triggers the descriptor and the default translation
+field is updated::
+
+    >>> n.title = 'foo'
+    >>> n.title_de
+    'foo'
+    >>>
+
+Accessing translated fields outside views
+-----------------------------------------
+Since the ``modeltranslation`` mechanism relies on the current language as it
+is returned by the ``get_language`` function care must be taken when accessing
+translated fields outside a view function.
+
+Within a view function the language is set by Django based on a flexible model
+described at `How Django discovers language preference`_ which is normally used
+only by Django's static translation system.
+
+.. _How Django discovers language preference: http://docs.djangoproject.com/en/dev/topics/i18n/#id2
+
+When a translated field is accessed in a view function or in a template, it
+uses the ``django.utils.translation.get_language`` function to determine the
+current language and return the appropriate value.
+
+Outside a view (or a template), i.e. in normal Python code, a call to the
+``get_language`` function still returns a value, but it might not what you
+expect. Since no request is involved, Django's machinery for discovering the
+user's preferred language is not activated. *todo: explain more*
+
+The unittests in ``tests.py`` use the ``django.utils.translation.trans_real``
+functions to activate and deactive a specific language outside a view function.
+
+
+Related projects
+================
+
+`django-multilingual`_
+----------------------
+
+    A library providing support for multilingual content in Django models.
+
+It is not possible to reuse existing models without modifying them.
+
+
+`django-multilingual-model`_
+----------------------------
+A much simpler version of the above `django-multilingual`.
+It works very similiar to the `django-multilingual` approach.
+
+`transdb`_
+----------
+
+    Django's field that stores labels in more than one language in database.
+
+This approach uses a specialized ``Field`` class, which means one has to change
+existing models.
+
+`i18ndynamic`_
+--------------
+This approach is not developed any more.
+
+
+`django-pluggable-model-i18n`_
+------------------------------
+
+    This app utilizes a new approach to multilingual models based on the same
+    concept the new admin interface uses. A translation for an existing model
+    can be added by registering a translation class for that model.
+
+This is more or less what ``modeltranslation`` does, unfortunately it is far
+from being finished.
+
+.. _django-multilingual: http://code.google.com/p/django-multilingual/
+.. _django-multilingual-model: http://code.google.com/p/django-multilingual-model/
+.. _django-transdb: http://code.google.com/p/transdb/
+.. _i18ndynamic: http://code.google.com/p/i18ndynamic/
+.. _django-pluggable-model-i18n: http://code.google.com/p/django-pluggable-model-i18n/

modeltranslation/admin.py

 # -*- coding: utf-8 -*-
-from copy import deepcopy
+from copy import copy
 
 from django import forms, template
 from django.conf import settings
 from django.contrib.contenttypes import generic
 
 from modeltranslation.translator import translator
-from modeltranslation.utils import get_translation_fields
+from modeltranslation.utils import get_translation_fields, get_default_language
 # Ensure that models are registered for translation before TranslationAdmin
 # runs. The import is supposed to resolve a race condition between model import
 # and translation registration in production (see issue 19).
 
             # In case the original form field was required, make the default
             # translation field required instead.
-            if db_field.language == settings.LANGUAGES[0][0] and \
+            if db_field.language == get_default_language() and \
                orig_formfield.required:
                 orig_formfield.required = False
                 orig_formfield.blank = True
                 field.required = True
                 field.blank = False
 
-            field.widget = deepcopy(orig_formfield.widget)
+            field.widget = copy(orig_formfield.widget)
 
 
 class TranslationAdmin(admin.ModelAdmin, TranslationAdminBase):
         if self.prepopulated_fields:
             prepopulated_fields_new = dict(self.prepopulated_fields)
             for (k, v) in self.prepopulated_fields.items():
-                translation_fields = get_translation_fields(v[0])
-                prepopulated_fields_new[k] = tuple([translation_fields[0]])
+                if v[0] in trans_opts.fields:
+                    translation_fields = get_translation_fields(v[0])
+                    prepopulated_fields_new[k] = tuple([translation_fields[0]])
             self.prepopulated_fields = prepopulated_fields_new
 
     def formfield_for_dbfield(self, db_field, **kwargs):

modeltranslation/fields.py

 # -*- coding: utf-8 -*-
 from django.conf import settings
 from django.db.models.fields import Field, CharField
+from django.db.models.fields.related import (ForeignKey, OneToOneField,
+                                             ManyToManyField)
 
-from modeltranslation.utils import get_language, build_localized_fieldname
+from modeltranslation.utils import (get_default_language,
+                                    build_localized_fieldname,
+                                    build_localized_verbose_name)
 
 
 class TranslationField(Field):
     that needs to be specified when the field is created.
     """
     def __init__(self, translated_field, language, *args, **kwargs):
+        # Update the dict of this field with the content of the original one
+        # This might be a bit radical?! Seems to work though...
+        self.__dict__.update(translated_field.__dict__)
+
+        # Common init
+        self._post_init(translated_field, language)
+
+    def _post_init(self, translated_field, language):
+        """Common init for subclasses of TranslationField."""
         # Store the originally wrapped field for later
         self.translated_field = translated_field
         self.language = language
 
-        # Update the dict of this field with the content of the original one
-        # This might be a bit radical?! Seems to work though...
-        self.__dict__.update(translated_field.__dict__)
-
         # Translation are always optional (for now - maybe add some parameters
         # to the translation options for configuring this)
         self.null = True
         self.blank = True
 
         # Adjust the name of this field to reflect the language
-        self.attname = build_localized_fieldname(translated_field.name,
-                                                 language)
+        self.attname = build_localized_fieldname(self.translated_field.name,
+                                                 self.language)
         self.name = self.attname
 
-        # Copy the verbose name and append a language suffix (will e.g. in the
-        # admin). This might be a proxy function so we have to check that here.
-        if hasattr(translated_field.verbose_name, '_proxy____unicode_cast'):
-            verbose_name = \
-                translated_field.verbose_name._proxy____unicode_cast()
-        else:
-            verbose_name = translated_field.verbose_name
-        self.verbose_name = '%s [%s]' % (verbose_name, language)
+        # Copy the verbose name and append a language suffix
+        # (will show up e.g. in the admin).
+        self.verbose_name =\
+        build_localized_verbose_name(translated_field.verbose_name,
+                                     language)
 
     def pre_save(self, model_instance, add):
         val = super(TranslationField, self).pre_save(model_instance, add)
-        if get_language() == self.language and not add:
+        if get_default_language() == self.language and not add:
             # Rule is: 3. Assigning a value to a translation field of the
             # default language also updates the original field
             model_instance.__dict__[self.translated_field.name] = val
         return val
 
+    def get_db_prep_value(self, value, connection, prepared=False):
+        if value == "":
+            return None
+        else:
+            return value
+
     def get_internal_type(self):
         return self.translated_field.get_internal_type()
 
-    #def contribute_to_class(self, cls, name):
-        #super(TranslationField, self).contribute_to_class(cls, name)
-        ##setattr(cls, 'get_%s_display' % self.name,
-                ##curry(cls._get_FIELD_display, field=self))
-
     def south_field_triple(self):
         """Returns a suitable description of this field for South."""
         # We'll just introspect the _actual_ field.
         from south.modelsinspector import introspector
         field_class = '%s.%s' % (self.translated_field.__class__.__module__,
                                  self.translated_field.__class__.__name__)
-        args, kwargs = introspector(self.translated_field)
+        args, kwargs = introspector(self)
         # That's our definition!
         return (field_class, args, kwargs)
 
         return super(TranslationField, self).formfield(*args, **defaults)
 
 
-#class CurrentLanguageField(CharField):
-    #def __init__(self, **kwargs):
-        #super(CurrentLanguageField, self).__init__(null=True, max_length=5,
-              #**kwargs)
+class RelatedTranslationField(object):
+    """
+    Mixin class which handles shared init of a translated relation field.
+    """
+    def _related_pre_init(self, translated_field, language, *args, **kwargs):
+        self.translated_field = translated_field
+        self.language = language
 
-    #def contribute_to_class(self, cls, name):
-        #super(CurrentLanguageField, self).contribute_to_class(cls, name)
-        #registry = CurrentLanguageFieldRegistry()
-        #registry.add_field(cls, self)
+        self.field_name = self.translated_field.name
+        self.translated_field_name = \
+            build_localized_fieldname(self.translated_field.name,
+                                      self.language)
 
+        # Dynamically add a related_name to the original field
+        translated_field.rel.related_name = \
+            '%s%s' % (self.translated_field.model._meta.module_name,
+                      self.field_name)
 
-#class CurrentLanguageFieldRegistry(object):
-    #_registry = {}
+        TranslationField.__init__(self, self.translated_field, self.language,
+                                  *args, **kwargs)
 
-    #def add_field(self, model, field):
-        #reg = self.__class__._registry.setdefault(model, [])
-        #reg.append(field)
+    def _related_post_init(self):
+        # Dynamically add a related_name to the translation fields
+        self.rel.related_name = \
+            '%s%s' % (self.translated_field.model._meta.module_name,
+                      self.translated_field_name)
 
-    #def get_fields(self, model):
-        #return self.__class__._registry.get(model, [])
+        # ForeignKey's init overrides some essential values from
+        # TranslationField, they have to be reassigned.
+        TranslationField._post_init(self, self.translated_field, self.language)
 
-    #def __contains__(self, model):
-        #return model in self.__class__._registry
+
+class ForeignKeyTranslationField(ForeignKey, TranslationField,
+                                 RelatedTranslationField):
+    def __init__(self, translated_field, language, to, to_field=None, *args,
+                 **kwargs):
+        self._related_pre_init(translated_field, language, *args, **kwargs)
+        ForeignKey.__init__(self, to, to_field, **kwargs)
+        self._related_post_init()
+
+
+class OneToOneTranslationField(OneToOneField, TranslationField,
+                               RelatedTranslationField):
+    def __init__(self, translated_field, language, to, to_field=None, *args,
+                 **kwargs):
+        self._related_pre_init(translated_field, language, *args, **kwargs)
+        OneToOneField.__init__(self, to, to_field, **kwargs)
+        self._related_post_init()
+
+
+class ManyToManyTranslationField(ManyToManyField, TranslationField,
+                                 RelatedTranslationField):
+    def __init__(self, translated_field, language, to, *args, **kwargs):
+        self._related_pre_init(translated_field, language, *args, **kwargs)
+        ManyToManyField.__init__(self, to, **kwargs)
+        self._related_post_init()

modeltranslation/management/commands/update_translation_fields.py

                                          NoArgsCommand)
 
 from modeltranslation.translator import translator
-from modeltranslation.utils import build_localized_fieldname
+from modeltranslation.utils import (build_localized_fieldname,
+                                    get_default_language)
 
 
 class Command(NoArgsCommand):
            'translated application using the value of the original field.'
 
     def handle(self, **options):
-        default_lang = settings.LANGUAGES[0][0]
+        default_lang = get_default_language()
         print "Using default language:", default_lang
 
         for model, trans_opts in translator._registry.items():

modeltranslation/tests.py

 from django.utils.thread_support import currentThread
 from django.utils.translation import get_language
 from django.utils.translation import trans_real
-
-# TODO: tests for TranslationAdmin
+from django.utils.translation import ugettext_lazy
 
 from modeltranslation import translator
 
+# TODO: Tests for TranslationAdmin, RelatedTranslationField and subclasses
+
 settings.LANGUAGES = (('de', 'Deutsch'),
                       ('en', 'English'))
 
 
 class TestModel(models.Model):
-    title = models.CharField(max_length=255)
+    title = models.CharField(ugettext_lazy('title'), max_length=255)
     text = models.TextField(null=True)
 
 
 
         inst.delete()
 
+    def test_verbose_name(self):
+        inst = TestModel.objects.create(title="Testtitle", text="Testtext")
+
+        self.assertEquals(\
+        unicode(inst._meta.get_field('title_de').verbose_name), u'Titel [de]')
+
+        inst.delete()
+
     def test_set_translation(self):
         self.failUnlessEqual(get_language(), "de")
         # First create an instance of the test model to play with

modeltranslation/translator.py

 from django.db.models.base import ModelBase
 from django.utils.functional import curry
 
-from modeltranslation.fields import TranslationField
+from modeltranslation.fields import (TranslationField,
+                                     ForeignKeyTranslationField,
+                                     OneToOneTranslationField,
+                                     ManyToManyTranslationField)
 from modeltranslation.utils import (TranslationFieldDescriptor,
                                     build_localized_fieldname)
 
 
+RELATED_CLASSES = ('ManyToOneRel', 'OneToOneRel', 'ManyToManyRel',)
+
+
 class AlreadyRegistered(Exception):
     pass
 
 
             # This approach implements the translation fields as full valid
             # django model fields and therefore adds them via add_to_class
-            localized_field = model.add_to_class( \
-                localized_field_name,
-                TranslationField(model._meta.get_field(field_name), l[0]))
+            field = model._meta.get_field(field_name)
+            field_class_name = field.rel.__class__.__name__
+            if field_class_name in RELATED_CLASSES:
+                to = field.rel.to._meta.object_name
+                if field_class_name == 'ManyToOneRel':
+                    translation_field = ForeignKeyTranslationField(\
+                                        translated_field=field,
+                                        language=l[0], to=to)
+                elif field_class_name == 'OneToOneRel':
+                    translation_field = OneToOneTranslationField(\
+                                        translated_field=field,
+                                        language=l[0], to=to)
+                elif field_class_name == 'ManyToManyRel':
+                    translation_field = ManyToManyTranslationField(\
+                                        translated_field=field,
+                                        language=l[0], to=to)
+            else:
+                translation_field = TranslationField(field, l[0])
+            localized_field = model.add_to_class(localized_field_name,
+                                                 translation_field)
             localized_fields[field_name].append(localized_field_name)
     return localized_fields
 
             # Create a reverse dict mapping the localized_fieldnames to the
             # original fieldname
             rev_dict = dict()
-            for orig_name, loc_names in \
+            for orig_name, loc_names in\
                 translation_opts.localized_fieldnames.items():
                 for ln in loc_names:
                     rev_dict[ln] = orig_name
 
             translation_opts.localized_fieldnames_rev = rev_dict
 
-        # print "Applying descriptor field for model %s" % model
+        #print "Applying descriptor field for model %s" % model
         for field_name in translation_opts.fields:
             setattr(model, field_name, TranslationFieldDescriptor(field_name))
 

modeltranslation/utils.py

 from django.core.exceptions import ValidationError
 from django.contrib.contenttypes.models import ContentType
 from django.utils.translation import get_language as _get_language
+from django.utils.functional import lazy
 
 
 def get_language():
     """
     Return an active language code that is guaranteed to be in
     settings.LANGUAGES (Django does not seem to guarantee this for us.)
-
     """
     lang = _get_language()
     available_languages = [l[0] for l in settings.LANGUAGES]
     return available_languages[0]
 
 
+def get_default_language():
+    return settings.LANGUAGES[0][0]
+
+
 def get_translation_fields(field):
     """Returns a list of localized fieldnames for a given field."""
     return [build_localized_fieldname(field, l[0]) for l in settings.LANGUAGES]
     return '%s_%s' % (field_name, lang.replace('-', '_'))
 
 
+def _build_localized_verbose_name(verbose_name, lang):
+    return u'%s [%s]' % (verbose_name, lang)
+build_localized_verbose_name = lazy(_build_localized_verbose_name, unicode)
+
+
 class TranslationFieldDescriptor(object):
     """A descriptor used for the original translated field."""
     def __init__(self, name, initial_val=""):
         if hasattr(instance, loc_field_name):
             return getattr(instance, loc_field_name) or \
                    instance.__dict__[self.name]
-        return instance.__dict__[self.name]
-
-
-#def create_model(name, fields=None, app_label='', module='', options=None,
-                 #admin_opts=None):
-    #"""
-    #Create specified model.
-    #This is taken from http://code.djangoproject.com/wiki/DynamicModels
-    #"""
-    #class Meta:
-        ## Using type('Meta', ...) gives a dictproxy error during model
-        ## creation
-        #pass
-
-    #if app_label:
-        ## app_label must be set using the Meta inner class
-        #setattr(Meta, 'app_label', app_label)
-
-    ## Update Meta with any options that were provided
-    #if options is not None:
-        #for key, value in options.iteritems():
-            #setattr(Meta, key, value)
-
-    ## Set up a dictionary to simulate declarations within a class
-    #attrs = {'__module__': module, 'Meta': Meta}
-
-    ## Add in any fields that were provided
-    #if fields:
-        #attrs.update(fields)
-
-    ## Create the class, which automatically triggers ModelBase processing
-    #model = type(name, (models.Model,), attrs)
-
-    ## Create an Admin class if admin options were provided
-    #if admin_opts is not None:
-        #class Admin(admin.ModelAdmin):
-            #pass
-        #for key, value in admin_opts:
-            #setattr(Admin, key, value)
-        #admin.site.register(model, Admin)
-
-    #return model
-
-
-def copy_field(field):
-    """
-    Instantiate a new field, with all of the values from the old one, except
-    the to and to_field in the case of related fields.
-
-    This taken from http://www.djangosnippets.org/snippets/442/
-    """
-    base_kw = dict([(n, getattr(field, n, '_null')) for n in \
-              models.fields.Field.__init__.im_func.func_code.co_varnames])
-    if isinstance(field, models.fields.related.RelatedField):
-        rel = base_kw.get('rel')
-        rel_kw = dict([(n, getattr(rel, n, '_null')) for n in \
-                 rel.__init__.im_func.func_code.co_varnames])
-        if isinstance(field, models.fields.related.ForeignKey):
-            base_kw['to_field'] = rel_kw.pop('field_name')
-        base_kw.update(rel_kw)
-    base_kw.pop('self')
-    return field.__class__(**base_kw)
+        #return instance.__dict__[self.name]
+        # FIXME: KeyError raised for ForeignKeyTanslationField
+        #        in admin list view
+        try:
+            return instance.__dict__[self.name]
+        except KeyError:
+            return None
 from distutils.core import setup
 
 setup(name='django-modeltranslation',
-      version='0.1',
+      version='0.2',
       description='Translates Django models using a registration approach.',
-      author='Peter Eschler',
-      author_email='peschler@googlemail.com',
+      author='Peter Eschler, Dirk Eschler',
+      author_email='peschler@googlemail.com, eschler@gmail.com',
       url='http://code.google.com/p/django-modeltranslation/',
-      packages=['modeltranslation', 'modeltranslation.management', 'modeltranslation.management.commands'],
-      liicense='New BSD' 
-     )
+      packages=['modeltranslation', 'modeltranslation.management',
+                'modeltranslation.management.commands'],
+      license='New BSD')
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.