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