Package: django-database-translation
Django Database TranslationDESCRIPTIONNatively, Django only handles hardcoded text translation. This package aims to assist you in translating the entries of your database. It is fairly easy to setup and use, as demonstrated below.The three main actions of the program are:Automatically generates database entries for any field that must be translatedAllows you to access the translations directly from your model's adminEasily translate your instances before pushing them in your template/context/jsonPlease note, this does not TRANSLATE for you. It creates the entries in the database for your translations, which you have to fill. Here's how it works in practice:I have registered 3 languages: French, English, ItalianI have registered the "Project" model and said it must be translatedI have registered 2 fields within the Project model, "name" and "description", as field that will require translationsWhenever I create a new project, 6 new translation entries will be create: "name" in 3 languages, and "description" in 3 languages.If I go in my "Project" admin, I will be able to directly edit those 6 translationsTo use translations in the frontend, simply translate your object using our utils functions before pushing them in your context/template/JSONNote that this package works RETROACTIVELY. Translation entries will be generated for all existing instances when deploying this appWHAT IT CONTAINSHere's a quick recap of what this package contains:4 new database tables:Field(ddt_fields)Item(ddt_items)Language(ddt_languages)Translation(ddt_translations)A few class extenders/templates:TranslatedFieldwhich is a Field that will be used for any field that must be translatedTranslatedModelto extend any model that has at least oneTranslatedFieldTranslatedAdminto extend any admin whose model inherits fromTranslatedModelA few tools for language selection:LanguageSelection, a form that can be used in the frontend to pick a languageupdate_user_language, a function that updates the user language for both django and our appA few tools for getting translationsall_instances_as_translated_dict: Applies 'instance_as_translated_dict' to the iterable of instancesget_current_language: Returns the Language instance used by our user or sets a defaultinstance_as_translated_dict: Returns a model instance into a dict containing all of its fieldsIt contains other elements, but this is what you will be using 99% of the time.SETUP1. InstallationFirst, we must install the package:Install the package withpip install django-database-translationInsettings.py, in yourINSTALLED_APPS, add"django_database_translation"(note that we are usingunderscoresthis time)Then run thepython manage.py makemigrations django_database_translationcommandThen run thepython manage.py migrate django_database_translationcommand to create the 4 new tables2. Updating your modelsThe point here is to "flag" which models have fields that must be translated. As a result, we willextendthe models, andchangethe fields:Extend a model usingTranslatedModelfor any model that contains TranslatedFieldsChange any field that will be translated to aTranslatedField(it is a ForeignKey to our Item model)Here's an example of code:fromdatetimeimportdatetimefromdjango.dbimportmodelsfromdjango_database_translation.fieldsimportTranslatedFieldfromdjango_database_translation.modelsimportTranslatedModelclassProject(TranslatedModel):# <-- Extended classtitle=TranslatedField(# <-- Field changedrelated_name="portfolio_project_title",verbose_name="Title")description=TranslatedField(# <-- Field changedrelated_name="portfolio_project_description",verbose_name="Description")date_posted=models.DateField(db_index=True,default=datetime.now,help_text="Date displayed on the frontend",null=False,verbose_name="Posted on",)Once you're all done, run themakemigrationsandmigratecommands to update your own database with your new changes.3. Updating your adminsNow that our models have been updated, we can update their admins. Note that you must only update the admins of models that have been extended withTranslatedModel.Simply extend your admin using theTranslatedAdminclass:fromdjango.contribimportadminfromdjango_database_translation.adminimportTranslatedAdminfrom.modelsimportProject@admin.register(Project)classProjectAdmin(TranslatedAdmin):# <-- Extended class# Your code hereNow you will be able to edit translations directly from your admins.4. Manually fill Language and Field in the adminNow we need to manually create a few entries in both ourLanguageandFieldmodels. Do not worry aboutItemandTranslation, their content will be generated automatically.--> LanguageIn this table, simply create all the languages available on your website. Make sure that thedjango_language_namematches an entry inLANGUAGESfromsettings.py.# If settings.py is like this:LANGUAGES=(("fr-fr","Français"),("en-us","English"),)# Make sure your "django_language_name" is either "fr-fr" or "en-us"--> FieldHere you must simply register all the fields you've changed toTranslatedField. Make sure their name matches the actual name of the field.5. Translate your entriesNow that everything is setup, you can go in your admin and go in any of your database entry. If it is a model that usesTranslatedModeland its admin isTranslatedAdmin, you'll be able to see the translations directly in its admin. Go ahead and translate anything that must be translated.SHOWING TRANSLATIONS1. Updating the user languageSince you're translating your database, you are probably also translating all of your HTML/Python texts, meaning you are using django built-ini18nandtranslationmodules (gettext, etc.). To make life easier, we created theupdate_user_languagefunction. It will update both django and our module current language for this user. Here's how it works:Have a form where the user can choose its languageMake sure to use ourLanguageentries as available choicesWhen the users POST the form (andform.is_valid()), call this function with the request object and the chosen Language id.The session will be updated using your Languageinstance.django_language_nameExample:# We call this when the form is validdefform_valid(self,form):# (...)language_id=form.cleaned_data['language_id']update_user_language(self.request,language_id=language_id)# (...)To gain some time, you can use theLanguageSelectionform as your form for the language selection. It is a form with:Only one field: the language idThis field is a ChoiceFieldThe available choices are the Language entriesUses either a RADIO for the form renderingHere is a full example of a view that is accessible everywhere on a website:fromdjango.shortcutsimportredirectefromdjango.views.genericimportViewfromdjango_database_translation.formsimportLanguageSelectionfromdjango_database_translation.utilsimportupdate_user_languageclassUpdateLanguageSelection(View):"""View only reachable through POST that allows you to select your languageA form sending a POST request to this view is available on every page:- The form is in our context_processor- The main.html renders the form"""# ----------------------------------------# Settings# ----------------------------------------form_class=LanguageSelection# ----------------------------------------# Core Methods# ----------------------------------------defget(self,request,*args,**kwargs):"""Defines how to handle a GET request.In this case, they are not allowed and will be redirected"""returnself.redirect_to_current_page()defpost(self,request,*args,**kwargs):"""Defines how to handle a POST request.It will update the user session language."""form=self.form_class(request.POST)ifform.is_valid():response=self.form_valid(form)else:response=self.form_invalid(form)returnresponse# ----------------------------------------# Helper Methods# ----------------------------------------defform_invalid(self,form):"""Simply redirects to the current page"""returnself.redirect_to_current_page()defform_valid(self,form):"""Called if the form is valid and data has been cleanedUpdates the user current language then redirects him to the current page"""language_id=form.cleaned_data['language_id']update_user_language(self.request,language_id=language_id)returnself.redirect_to_current_page()defredirect_to_current_page(self):"""Reloads the current page or redirects you to the homepage"""current_page=self.request.META['HTTP_REFERER']ifnotcurrent_page:current_page="home"returnredirect(current_page)2. Displaying the translationsThe last thing left to do is to display the translations. Our instances have keys toItem, which then have keys toTranslation. To get the translations, we need to pair anItemwith aLanguage.First off, get the language using ourget_current_language. This will return the user's currentLanguageinstance.Then, before sending your object(s) in the template/context/JSON, translate them usinginstance_as_translated_dictorall_instances_as_translated_dict. Your instances will become dictionaries, and yourItemkeys will automatically be replaced with your translated text. Those two functions can either be given alanguage, or guess it themselves by using therequestarg. If you're going to make several translations in the same functions, get thelanguagefirst. This will avoid making a new database request each time to guess the language.You'll find below an example with a Project model and a Job model, where we:Translate projects by overriding theget_querysetmethodGet and translate jobs by overriding theget_context_datamethodIn one case, we get the language THEN translateIn the other case, we simply push the request, and the function will guess the language(This is done only as a showcase)fromdjango.utils.translationimportgettextfromdjango.views.genericimportListViewfromdjango_database_translation.utilsimportall_instances_as_translated_dict,get_current_languagefrom.modelsimportJob,ProjectclassProjectList(ListView):# ----------------------------------------# Settings# ----------------------------------------model=Projecttemplate_name="portfolio/pages/portfolio.html"context_object_name="projects"ordering=["-date_posted"]# ----------------------------------------# Overridden Methods# ----------------------------------------defget_context_data(self,**kwargs):"""The method was overridden to do the following:- Add the 'meta_title' to the context- Add the active 'jobs' to the context and translating their data"""# Get the Job instances and translate them using the requestall_jobs=Job.objects.all()jobs=filter(lambdax:x.count_projects(),all_jobs)jobs=all_instances_as_translated_dict(jobs,depth=True,request=self.request)# Update the contextcontext=super(ProjectList,self).get_context_data(**kwargs)context.update({"meta_title":gettext("portoflio_meta_title"),"jobs":jobs,})returncontextdefget_queryset(self):# We get the language first, then use it for our translationlanguage=get_current_language(self.request)query=Project.objects.filter(active=True)query=all_instances_as_translated_dict(query,depth=True,language=language)returnqueryADDTIONAL INFORMATIONTranslate with depthIn the example above, with Project and Job, you'll notice we translated them usingdepth=True. It means that if ourTranslatedModelhas foreign keys to other models, those models will also be translated. In this case, ifProjecthas a FK toJob, we can accesJob.nameby:Usingproject["job"]["name"]in python (fields are now keys, not attributes)Usingproject.job.namein html/templates (same syntax as normal instances)How to translate a form that uses the databaseSometimes, your form will haveChoiceFieldgenerated from your database. If the table used is aTranslatedModel, you'll need to get the right language for your form. Since a form doesn't have access to therequestobject, you need to pass either thelanguageor therequestin your form init method. Here's an example on how to do it:# In your FORM, override the __init__ method like so:def__init__(self,*args,**kwargs):self.language=kwargs.pop("language",None)super(YourFormName,self).__init__(*args,**kwargs)# In your VIEW, call the form like so:deffunction_inside_your_view(self):# (...)request=self.requestlanguage=get_current_language(request)form=self.form_class(request.GET,language=language)# (or .POST, etc.)# (...)Model.objects.bulk_create()If a model inherits fromTranslatedModel, it will not be able to use its.bulk_create()method. We are forced deactivate it as our program usessignalsto work, and.bulk_create()does not trigger signals.More info on the utils functionsHere's a closer look on the utils functions:# --------------------------------------------------------------------------------# > Imports# --------------------------------------------------------------------------------fromdjango.dbimportmodelsfromdjango.db.models.fields.filesimportImageFieldFile,FieldFilefromdjango.utils.translationimportactivate,LANGUAGE_SESSION_KEYfrom.modelsimportItem,Language,Translation# --------------------------------------------------------------------------------# > Functions# --------------------------------------------------------------------------------defall_instances_as_translated_dict(instances,depth=True,language=None,request=None):"""Description:Applies 'instance_as_translated_dict' to the iterable of instancesReturns a list of dicts which contains the fields of all your instancesCheck the 'instance_as_translated_dict' for more infoArgs:instances (iterable): An iterable of your model instancesdepth (bool, optional): Determines if FK will also be transformed into dicts. Defaults to True.language (Language, optional): A Language instance from this app. Defaults to None.request (HttpRequest, option): HttpRequest from Django. Defaults to None.Returns:list: A list of dicts, where each dict contains the fields/values of the initial instances"""# Checking argumentsiflanguageisNoneandrequestisNone:raiseTypeError("You must provide either 'language' or 'request'")# Get the language from the sessioniflanguageisNone:language=get_current_language(request)# Loop over instancesresults=[]forinstanceininstances:result=instance_as_translated_dict(instance,depth=depth,language=language)results.append(result)returnresultsdefget_current_language(request,set_default=True,default_id=1):"""Description:Returns the current active language. Will set a default language if none is found.Args:request (HttpRequest): HttpRequest from Djangoset_default (Boolean): Indicates if a default language must be activated (if none currently is). Default to True.default_id (Integer): The PK for the default Language instance. Default to 1Returns:Language: The currently used language from our app's Language model"""# Base variableslanguage=Nonelanguage_name=request.session.get(LANGUAGE_SESSION_KEY,False)# Get the languageiflanguage_name:try:language=Language.objects.get(django_language_name=language_name)exceptLanguage.DoesNotExist:pass# Set a default language if necessaryiflanguageisNoneandset_default:language=set_default_language(request,default_id)# Always return the active languagereturnlanguagedefget_translation(language,item_id):"""Description:Returns a translated text using an Item id and a Language instanceArgs:language (Language): Language instance from this appitem_id (int): Key contained in the 'translated field'Returns:str: The translated text"""translation=""try:entry=Translation.objects.get(language=language,item_id=item_id)translation=entry.textexceptTranslation.DoesNotExist:passreturntranslationdefinstance_as_translated_dict(instance,depth=True,language=None,request=None):"""Description:Returns a model instance into a dict containing all of its fieldsLanguage can be given as an argument, or guess through the user of "request"With "depth" set to True, ForeignKey will also be transformed into sub-dictFiles and images are replaced by a subdict with 'path', 'url', and 'name' keysMeaning you will be able to manipulate the dict in an HTML template much like an instanceArgs:instance (Model): An instance from any of your modelsdepth (bool, optional): Determines if FK will also be transformed into dicts. Defaults to True.language (Language, optional): A Language instance from this app. Defaults to None.request (HttpRequest, option): HttpRequest from Django. Defaults to None.Returns:dict: A dict with all of the instance's fields and values"""# Checking argumentsiflanguageisNoneandrequestisNone:raiseTypeError("You must provide either 'language' or 'request'")# Get the language from the sessioniflanguageisNone:language=get_current_language(request)# Loop over fieldstranslated_dict={}fields=instance._meta.get_fields()forfieldinfields:value=getattr(instance,field.name,None)ifvalueisnotNone:value_type=type(value)# Case 1: Get the translationifvalue_type==Item:new_value=Translation.objects.get(item=value,language=language).text# Case 2: Go to the linked model and repeat the process (unless depth=False)elifissubclass(value_type,models.Model):ifdepth:new_value=instance_as_translated_dict(value,depth=True,language=language)else:new_value=value# Case 3:elifvalue_typein{ImageFieldFile,FieldFile}:ifvalue:new_value={"name":getattr(value,"name",""),"url":getattr(value,"url",""),"path":getattr(value,"path",""),}else:new_value=""# Case 4: Keep the value as it iselse:new_value=valuetranslated_dict[field.name]=new_valuereturntranslated_dictdefset_default_language(request,pk=1):"""Sets the default language if none is chosen"""language=Language.objects.get(id=pk)update_user_language(request,language=language)returnlanguagedefupdate_user_language(request,language=None,language_id=None):"""Description:Updates the user current language following Django guildelinesThis will allow for both "Django" frontend translations and "our app" database translationThe new language must be passed either through a Language instance or an IDArgs:request (HttpRequest): Request object from Django, used to get to the sessionlanguage (Language, optional): A Language instance from this app. Defaults to None.language_id (id, optional): ID of the language in our database. Defaults to None."""# Checking argumentsiflanguageisNoneandlanguage_idisNone:raiseTypeError("You must provide either 'language' or 'language_id'")# Get the language from the sessioniflanguageisNone:language=Language.objects.get(id=language_id)# Update the user's languageactivate(language.django_language_name)request.session[LANGUAGE_SESSION_KEY]=language.django_language_name