Login

Modeli18n

Author:
pavl
Posted:
June 8, 2010
Language:
Python
Version:
1.2
Score:
2 (after 2 ratings)

This is a Model base class used to support internationalization (i18n) for your models.

This code extends the Django's Model class so you can use all available options from Model safely. Basicly, it uses introspection to create a sub-model to your model to hold translation.

Features:

  1. Simplicity of use. You simply extend your model with this class and add all the fields that needs to be translated are placed under the locale sub-class;
  2. The code uses the django.utils.translation.get_language() to select the current language;
  3. You can use python ./manage.py syncdb command safely;
  4. Force the user to enter a translation for each language even if the fields can be blank. This makes sure that all objects are returned safely.

Ordering by locale fields:

To sort on translated fields, use the form of "model_i18n__transfieldname" (see code for example).

Limitation:

Do not use localized fields in unicode, the admin will throw an exception when you'll add a new item and do "save and continue".

Just drop a comment if you need more information.

(last update: 06/15/2010)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
import inspect

from django.contrib import admin
from django.conf import settings
from django.db import models
from django.forms import ModelForm
from django.utils import translation
from django.utils.translation import ugettext_lazy as _


class TranslationModelForm(ModelForm):
  """
    Form used by the StackedInline object created at run-time.
  """
  def __init__(self, *args, **kwargs):
    super(TranslationModelForm, self).__init__(*args, **kwargs)

    # This little flag makes sure that
    # the user has to fill the form.
    self.empty_permitted = False


class LocaleManager(models.Manager):
  """
    Manager used to
  """
  use_for_related_fields = True

  def __init__(self, name=None, table=None, fieldnames=None):
    super(LocaleManager, self).__init__()
    self.name = name
    self.table = table
    self.fieldnames = fieldnames

  def _locale(self):
    query = super(LocaleManager, self).get_query_set()

    if self.table and self.fieldnames:
      lang = translation.get_language()

      tablei18n = "%s_i18n" % self.table

      # Extra fields to select.
      extrafield = {}
      for name in self.fieldnames:
        fieldname = "%s__%s" % (self.name, name)
        extrafield[name] = '%s.%s' % (tablei18n, name)

      kwargs = {"%s_i18n__lang" % (self.name.lower(), ):lang}
      query = query.filter(**kwargs).extra(select=extrafield,
                                           tables=[tablei18n,])
    return query

  def get_query_set(self):
    return self._locale()


class Modeli18nMeta(models.Model.__metaclass__):
  """
    Metaclass used to create a sub-Model attribute
    to the current class that will hold the
    translated fields.
  """
  def __new__(cls, name, bases, attrs):

    # Create a new Model class reference
    # to hold the parent Model translation.
    #
    # Add this Model class as an attribute
    # of the parent class.
    if name != "Modeli18n":
      if "locale" in attrs:
        # Language utilities.
        lenlang = len(settings.LANGUAGES)
        languages = []
        for lan, value in settings.LANGUAGES:
          languages.append([lan, _(value)])

        ###############################
        # Create a new Model subclass #
        # by copying the attributes   #
        # from the locale subclass.   #
        ###############################

        # Get the module name.
        module = attrs['__module__']

        # Set the sub-Model attributes.
        attributes = {'__module__': module}

        # Get locale class definition object.
        locale = attrs["locale"]

        # Get all defined attributes.
        members = inspect.getmembers(locale)

        # Get attributes and record their names.
        locale_attrs = []
        for obj_name, obj in members:
          if "Field" in str(obj):
            obj.null = True
            obj.blank = True
            attributes[obj_name] = obj
            locale_attrs.append(obj_name)

        # Set the table name.
        table = None
        if "Meta" in attrs:
          table = getattr(attrs['Meta'], 'db_table', None)
        if not table:
          table = "%s_%s" % (module.split('.')[-2], name.lower())

        # Set the sub-Model Meta class.
        newtable = "%s_i18n" % table
        newname = "%s_i18n" % name
        values = {'db_table': newtable}
        values['unique_together'] = ("parent", "lang")
        values['verbose_name'] = _("translation")
        values['verbose_name_plural'] = _("translations")
        meta = type('Meta', (object,), values)
        attributes['Meta'] = meta

        # Parent's foreign key.
        attributes["parent"] = models.ForeignKey("%s" % (name), null=True, blank=False)

        # Language field.
        attributes['lang'] = models.CharField(name="lang", verbose_name=_('language'),
                                              help_text=_("The language is required even if the fields are empty."),
                                              max_length=lenlang,
                                              choices=languages)
        # Friendly verbose.
        attributes['__unicode__'] = lambda x : x.lang

        # Create the sub-Model...
        attrs["locale"] = type(newname, (models.Model,), attributes)
        # and add it to its parent Model as "locale" attribute.
        attrs["objects"] = LocaleManager(name, table, locale_attrs)

        #
        # Add a StackedInline(admin.StackedInline) class
        # for validating and displaying as convenience.
        #
        del attributes
        attributes = {'max_num': lenlang,
                      'extra': lenlang,
                      'model': attrs["locale"],
                      'form': TranslationModelForm,
                      'can_delete': False,}
        inlines = type('StackedInline', (admin.StackedInline, ), attributes)
        attrs["StackedInline"] = inlines

    return super(Modeli18nMeta, cls).__new__(cls, name, bases, attrs)


class Modeli18n(models.Model):
  """
    Abstract class used as base class for all models
    that needs to be translated.

    i.e.: Product(Modeli18n)
  """
  __metaclass__ = Modeli18nMeta

  class Meta:
    abstract = True

  def get_locale(self, lang=None):
    # Return the locale object
    # for the given language.
    obj = None
    if lang:
      try:
        obj = self.locale.objects.get(lang__exact=lang, parent__exact=self.id)[0]
      except self.locale.DoesNotExist:
        try:
          # As a fallback we use the default language.
          obj = self.locale.objects.get(lang__exact=settings.LANGUAGE_CODE, parent__exact=self.id)[0]
        except self.locale.DoesNotExist:
          pass
    return obj

#######################
#                     #
# settings.py         #
#                     #
#######################
# ...

LANGUAGE_CODE = 'en' # Default language used.

ADMIN_LANGUAGE_CODE = LANGUAGE_CODE

LANGUAGES = (
    ('fr', gettext('French')),
    ('en', gettext('English')),
    ('es', gettext('Spanish')),
)

#######################
#                     #
# Model Usage example #
#                     #
# i.e.: models.py     #
#                     #
#######################

import Modeli18n, LocaleManager

class Activity(Modeli18n):
  # A tag nothing special.
  tag = models.CharField(max_length=30, blank=False, null=False, db_index=True)

  # Important! Use this manager to sort correctly by language.
  objects = LocaleManager('Activity')

  class locale:
    # Define all locale fields here!
    # name to be translated
    name = models.CharField(verbose_name=_('name'), max_length=100)

  class Meta:
    # Ordering the class by it's translated name.
    # Here, activity_i18n is the sub-Model 
    # generated by the code.
    ordering = ('activity_i18n__name',)

  def get_name(self):
    try:
      return self.name
    except AttributeError:
      return None

  def __unicode__(self):
    str = self.get_name()
    if not str:
      str = self.tag
    return "%s" % (str)

#######################
#                     #
# Admin Usage example #
#                     #
# i.e.: admin.py      #
#                     #
#######################

from django.contrib import admin
from models import Activity

#
# Activity.StackedInline is available to the
# model as a courtesy and simplicity.
# 
class ActivityAdmin(admin.ModelAdmin):
  inlines = [Activity.StackedInline,]
  list_display = ('get_name',)

admin.site.register(Activity, ActivityAdmin)


#######################
#                     #
# Simple usage        #
#                     #
#######################
from models import Activity

import operator

activity = Activity.objects.all()[0]
print activity.name

# Get a specific locale.
french = activity.get_locale('fr')
if french:
  print french.name

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 11 months, 3 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 4 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 7 months ago
  5. Help text hyperlinks by sa2812 1 year, 8 months ago

Comments

pavl (on June 10, 2010):

Just added the "ordering" support for Model and ModelAdmin classes.

#

pavl (on June 15, 2010):

Removed the limitation (bug) where if no translation didn't exist for a given language, the object Model won't be returned;

Optimized data fetch process from the DB: Added a models.Management to optimize the fetching process. No more useless SELECT triggered for each object.

#

pavl (on June 16, 2010):

Added some methods "get_name" on the "Activity" example model. Check the limitation notes.

#

Please login first before commenting.