Package to handle database translation in your Django apps
Project description
Django Database Translation
DESCRIPTION
Natively, 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 translated
- Allows you to access the translations directly from your model's admin
- Easily translate your instances before pushing them in your template/context/json
Please 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, Italian
- I have registered the "Project" model and said it must be translated
- I have registered 2 fields within the Project model, "name" and "description", as field that will require translations
- Whenever 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 translations
- To use translations in the frontend, simply translate your object using our utils functions before pushing them in your context/template/JSON
Note that this package works RETROACTIVELY. Translation entries will be generated for all existing instances when deploying this app
WHAT IT CONTAINS
Here'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:
TranslatedField
which is a Field that will be used for any field that must be translatedTranslatedModel
to extend any model that has at least oneTranslatedField
TranslatedAdmin
to extend any admin whose model inherits fromTranslatedModel
- A 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 app
- A few tools for getting translations
all_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 fields
It contains other elements, but this is what you will be using 99% of the time.
SETUP
1. Installation
First, we must install the package:
- Install the package with
pip install django-database-translation
- In
settings.py
, in yourINSTALLED_APPS
, add"django_database_translation"
(note that we are using underscores this time) - Then run the
python manage.py makemigrations django_database_translation
command - Then run the
python manage.py migrate django_database_translation
command to create the 4 new tables
2. Updating your models
The point here is to "flag" which models have fields that must be translated. As a result, we will extend the models, and change the fields:
- Extend a model using
TranslatedModel
for any model that contains TranslatedFields - Change any field that will be translated to a
TranslatedField
(it is a ForeignKey to our Item model)
Here's an example of code:
from datetime import datetime
from django.db import models
from django_database_translation.fields import TranslatedField
from django_database_translation.models import TranslatedModel
class Project(TranslatedModel): # <-- Extended class
title = TranslatedField( # <-- Field changed
related_name="portfolio_project_title",
verbose_name="Title"
)
description = TranslatedField( # <-- Field changed
related_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 the makemigrations
and migrate
commands to update your own database with your new changes.
3. Updating your admins
Now 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 with TranslatedModel
.
Simply extend your admin using the TranslatedAdmin
class:
from django.contrib import admin
from django_database_translation.admin import TranslatedAdmin
from .models import Project
@admin.register(Project)
class ProjectAdmin(TranslatedAdmin): # <-- Extended class
# Your code here
Now you will be able to edit translations directly from your admins.
4. Manually fill Language and Field in the admin
Now we need to manually create a few entries in both our Language
and Field
models. Do not worry about Item
and Translation
, their content will be generated automatically.
--> Language
In this table, simply create all the languages available on your website. Make sure that the django_language_name
matches an entry in LANGUAGES
from settings.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"
--> Field
Here you must simply register all the fields you've changed to TranslatedField
. Make sure their name matches the actual name of the field.
5. Translate your entries
Now that everything is setup, you can go in your admin and go in any of your database entry. If it is a model that uses TranslatedModel
and its admin is TranslatedAdmin
, you'll be able to see the translations directly in its admin. Go ahead and translate anything that must be translated.
SHOWING TRANSLATIONS
1. Updating the user language
Since you're translating your database, you are probably also translating all of your HTML/Python texts, meaning you are using django built-in i18n
and translation
modules (gettext, etc.). To make life easier, we created the update_user_language
function. 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 language
- Make sure to use our
Language
entries as available choices - When the users POST the form (and
form.is_valid()
), call this function with the request object and the chosen Language id. - The session will be updated using your Language
instance.django_language_name
Example:
# We call this when the form is valid
def form_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 the LanguageSelection
form as your form for the language selection. It is a form with:
- Only one field: the language id
- This field is a ChoiceField
- The available choices are the Language entries
- Uses either a RADIO for the form rendering
Here is a full example of a view that is accessible everywhere on a website:
from django.shortcuts import redirecte
from django.views.generic import View
from django_database_translation.forms import LanguageSelection
from django_database_translation.utils import update_user_language
class UpdateLanguageSelection(View):
"""
View only reachable through POST that allows you to select your language
A 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
# ----------------------------------------
def get(self, request, *args, **kwargs):
"""
Defines how to handle a GET request.
In this case, they are not allowed and will be redirected
"""
return self.redirect_to_current_page()
def post(self, request, *args, **kwargs):
"""
Defines how to handle a POST request.
It will update the user session language.
"""
form = self.form_class(request.POST)
if form.is_valid():
response = self.form_valid(form)
else:
response = self.form_invalid(form)
return response
# ----------------------------------------
# Helper Methods
# ----------------------------------------
def form_invalid(self, form):
"""Simply redirects to the current page"""
return self.redirect_to_current_page()
def form_valid(self, form):
"""
Called if the form is valid and data has been cleaned
Updates 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)
return self.redirect_to_current_page()
def redirect_to_current_page(self):
"""Reloads the current page or redirects you to the homepage"""
current_page = self.request.META['HTTP_REFERER']
if not current_page:
current_page = "home"
return redirect(current_page)
2. Displaying the translations
The last thing left to do is to display the translations. Our instances have keys to Item
, which then have keys to Translation
. To get the translations, we need to pair an Item
with a Language
.
First off, get the language using our get_current_language
. This will return the user's current Language
instance.
Then, before sending your object(s) in the template/context/JSON, translate them using instance_as_translated_dict
or all_instances_as_translated_dict
. Your instances will become dictionaries, and your Item
keys will automatically be replaced with your translated text. Those two functions can either be given a language
, or guess it themselves by using the request
arg. If you're going to make several translations in the same functions, get the language
first. 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 the
get_queryset
method - Get and translate jobs by overriding the
get_context_data
method - In one case, we get the language THEN translate
- In the other case, we simply push the request, and the function will guess the language
- (This is done only as a showcase)
from django.utils.translation import gettext
from django.views.generic import ListView
from django_database_translation.utils import all_instances_as_translated_dict, get_current_language
from .models import Job, Project
class ProjectList(ListView):
# ----------------------------------------
# Settings
# ----------------------------------------
model = Project
template_name = "portfolio/pages/portfolio.html"
context_object_name = "projects"
ordering = ["-date_posted"]
# ----------------------------------------
# Overridden Methods
# ----------------------------------------
def get_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 request
all_jobs = Job.objects.all()
jobs = filter(lambda x: x.count_projects(), all_jobs)
jobs = all_instances_as_translated_dict(jobs, depth=True, request=self.request)
# Update the context
context = super(ProjectList, self).get_context_data(**kwargs)
context.update({
"meta_title": gettext("portoflio_meta_title"),
"jobs": jobs,
})
return context
def get_queryset(self):
# We get the language first, then use it for our translation
language = get_current_language(self.request)
query = Project.objects.filter(active=True)
query = all_instances_as_translated_dict(query, depth=True, language=language)
return query
ADDTIONAL INFORMATION
Translate with depth
In the example above, with Project and Job, you'll notice we translated them using depth=True
. It means that if our TranslatedModel
has foreign keys to other models, those models will also be translated. In this case, if Project
has a FK to Job
, we can acces Job.name
by:
- Using
project["job"]["name"]
in python (fields are now keys, not attributes) - Using
project.job.name
in html/templates (same syntax as normal instances)
How to translate a form that uses the database
Sometimes, your form will have ChoiceField
generated from your database. If the table used is a TranslatedModel
, you'll need to get the right language for your form. Since a form doesn't have access to the request
object, you need to pass either the language
or the request
in 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:
def function_inside_your_view(self):
# (...)
request = self.request
language = get_current_language(request)
form = self.form_class(request.GET, language=language) # (or .POST, etc.)
# (...)
Model.objects.bulk_create()
If a model inherits from TranslatedModel
, it will not be able to use its .bulk_create()
method. We are forced deactivate it as our program uses signals to work, and .bulk_create()
does not trigger signals.
More info on the utils functions
Here's a closer look on the utils functions:
# --------------------------------------------------------------------------------
# > Imports
# --------------------------------------------------------------------------------
from django.db import models
from django.db.models.fields.files import ImageFieldFile, FieldFile
from django.utils.translation import activate, LANGUAGE_SESSION_KEY
from .models import Item, Language, Translation
# --------------------------------------------------------------------------------
# > Functions
# --------------------------------------------------------------------------------
def all_instances_as_translated_dict(instances, depth=True, language=None, request=None):
"""
Description:
Applies 'instance_as_translated_dict' to the iterable of instances
Returns a list of dicts which contains the fields of all your instances
Check the 'instance_as_translated_dict' for more info
Args:
instances (iterable): An iterable of your model instances
depth (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 arguments
if language is None and request is None:
raise TypeError("You must provide either 'language' or 'request'")
# Get the language from the session
if language is None:
language = get_current_language(request)
# Loop over instances
results = []
for instance in instances:
result = instance_as_translated_dict(instance, depth=depth, language=language)
results.append(result)
return results
def get_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 Django
set_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 1
Returns:
Language: The currently used language from our app's Language model
"""
# Base variables
language = None
language_name = request.session.get(LANGUAGE_SESSION_KEY, False)
# Get the language
if language_name:
try:
language = Language.objects.get(django_language_name=language_name)
except Language.DoesNotExist:
pass
# Set a default language if necessary
if language is None and set_default:
language = set_default_language(request, default_id)
# Always return the active language
return language
def get_translation(language, item_id):
"""
Description:
Returns a translated text using an Item id and a Language instance
Args:
language (Language): Language instance from this app
item_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.text
except Translation.DoesNotExist:
pass
return translation
def instance_as_translated_dict(instance, depth=True, language=None, request=None):
"""
Description:
Returns a model instance into a dict containing all of its fields
Language 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-dict
Files and images are replaced by a subdict with 'path', 'url', and 'name' keys
Meaning you will be able to manipulate the dict in an HTML template much like an instance
Args:
instance (Model): An instance from any of your models
depth (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 arguments
if language is None and request is None:
raise TypeError("You must provide either 'language' or 'request'")
# Get the language from the session
if language is None:
language = get_current_language(request)
# Loop over fields
translated_dict = {}
fields = instance._meta.get_fields()
for field in fields:
value = getattr(instance, field.name, None)
if value is not None:
value_type = type(value)
# Case 1: Get the translation
if value_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)
elif issubclass(value_type, models.Model):
if depth:
new_value = instance_as_translated_dict(value, depth=True, language=language)
else:
new_value = value
# Case 3:
elif value_type in {ImageFieldFile, FieldFile}:
if value:
new_value = {
"name": getattr(value, "name", ""),
"url": getattr(value, "url", ""),
"path": getattr(value, "path", ""),
}
else:
new_value = ""
# Case 4: Keep the value as it is
else:
new_value = value
translated_dict[field.name] = new_value
return translated_dict
def set_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)
return language
def update_user_language(request, language=None, language_id=None):
"""
Description:
Updates the user current language following Django guildelines
This will allow for both "Django" frontend translations and "our app" database translation
The new language must be passed either through a Language instance or an ID
Args:
request (HttpRequest): Request object from Django, used to get to the session
language (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 arguments
if language is None and language_id is None:
raise TypeError("You must provide either 'language' or 'language_id'")
# Get the language from the session
if language is None:
language = Language.objects.get(id=language_id)
# Update the user's language
activate(language.django_language_name)
request.session[LANGUAGE_SESSION_KEY] = language.django_language_name
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file django_database_translation-1.1.5.tar.gz
.
File metadata
- Download URL: django_database_translation-1.1.5.tar.gz
- Upload date:
- Size: 25.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.4.1 importlib_metadata/4.0.1 pkginfo/1.7.0 requests/2.25.1 requests-toolbelt/0.9.1 tqdm/4.61.0 CPython/3.9.5
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | b32965f1c0e9f02e5b3934626868983ba66decaf5e57e02d4603d51d742fda80 |
|
MD5 | 703af598a0df11be4100c6b3b98b99ef |
|
BLAKE2b-256 | 56f18abc4982646c75d045c28d02a98a19c0a12a7996639e3ece1733d2f3a823 |
File details
Details for the file django_database_translation-1.1.5-py3-none-any.whl
.
File metadata
- Download URL: django_database_translation-1.1.5-py3-none-any.whl
- Upload date:
- Size: 22.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.4.1 importlib_metadata/4.0.1 pkginfo/1.7.0 requests/2.25.1 requests-toolbelt/0.9.1 tqdm/4.61.0 CPython/3.9.5
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | ed92470a23f1046527acb9e6a6bcbe5f0cc3d62174e67fb64f5aa2df1777fa34 |
|
MD5 | 282c86466527fdfe250ebf96f4a804cd |
|
BLAKE2b-256 | c4e4dfaf6c9973d02d65956023b432c670b6ed6d5822d31436ec5c21e3f0550b |