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:
- 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; - The code uses the
django.utils.translation.get_language()
to select the current language; - You can use
python ./manage.py syncdb
command safely; - 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
- Template tag - list punctuation for a list of items by shapiromatron 11 months, 3 weeks ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 4 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 7 months ago
- Help text hyperlinks by sa2812 1 year, 8 months ago
Comments
Just added the "ordering" support for Model and ModelAdmin classes.
#
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.
#
Added some methods "get_name" on the "Activity" example model. Check the limitation notes.
#
Please login first before commenting.