This page looks best with JavaScript enabled

Django Translations

 ·  🎃 kr0m

Django provides mechanisms for translating applications, including texts in views, templates, code displayed by JavaScript, and model metadata. However, this does not include content in the database. To translate such content, we will use the django-modeltranslation module. In this article, we will show a very simple but illustrative example of how to use all the mentioned resources.

Before starting, it is recommended to read the previous articles on Django as they are the previous steps to this article:


As always, activate the project’s venv:

cd rxWod
source bin/activate
cd rxWodProject/

To support multiple languages in Django, we must enable the django.middleware.locale.LocaleMiddleware middleware:

vi rxWodProject/settings.py

if PRODUCTION_ENABLED:
    MIDDLEWARE = [
        'django.middleware.common.BrokenLinkEmailsMiddleware',
        'django.middleware.security.SecurityMiddleware',
        'django.contrib.sessions.middleware.SessionMiddleware',
        'django.middleware.locale.LocaleMiddleware',
        'django.middleware.common.CommonMiddleware',
        'django.middleware.csrf.CsrfViewMiddleware',
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        'debug_toolbar.middleware.DebugToolbarMiddleware',
        'django.contrib.messages.middleware.MessageMiddleware',
        'django.middleware.clickjacking.XFrameOptionsMiddleware',
    ]
    MANAGERS = [('kr0m', 'kr0m@alfaexploit.com')]

    import re
    IGNORABLE_404_URLS = [
        re.compile(r'\.(php|cgi)$'),
        re.compile(r'^/phpmyadmin/'),
    ]
else:
    MIDDLEWARE = [
        'django.middleware.security.SecurityMiddleware',
        'django.contrib.sessions.middleware.SessionMiddleware',
        'django.middleware.locale.LocaleMiddleware',
        'django.middleware.common.CommonMiddleware',
        'django.middleware.csrf.CsrfViewMiddleware',
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        'debug_toolbar.middleware.DebugToolbarMiddleware',
        'django.contrib.messages.middleware.MessageMiddleware',
        'django.middleware.clickjacking.XFrameOptionsMiddleware',
    ]

We change the default language to English so that when translations are not found for a language, the texts will be displayed in English:

vi rxWodProject/settings.py

LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'

Django determines the language to use through the following checks:

  • Prefix in the URL, for this to work we must enable the i18n_patterns in the Django URL configuration
  • Existence of the Cookie: django_language
  • HTTP Accept-Language header
  • If all of the above fails, it uses the default language: LANGUAGE_CODE

To perform tests more comfortably, it is recommended to install a language change addon in the browser.


As a very simple example, we are going to translate a message and a text from the index view, for this we need to import gettext and indicate what we want to translate with _

from django.utils.translation import gettext as _
vi rxWod/views.py
from django.shortcuts import get_object_or_404, get_list_or_404, render, redirect
from django.http import HttpResponse
from .models import Routine
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.utils.translation import gettext as _

@login_required()
def index(request):
    #print('username: %s' % request.user.username)
    routines = Routine.objects.filter(user=request.user)
    messages.success(request, _('Message sent by index view'))
    text = _('Text from view')

    context = {
        'routines': routines,
        'text': text,
    }
    return render(request, 'rxWod/index.html', context)

In the index template, we are going to translate another test text, for this we load the internationalization:

{% load i18n %}

And we use the translate tag:

{% translate "Text translated in template" %}

It would look like this:

vi rxWod/templates/rxWod/index.html

{% extends 'rxWod/base.html' %}
{% load render_bundle from webpack_loader %}
{% load webpack_static from webpack_loader %}
{% load i18n %}

{% block base_head %}
    {% render_bundle 'index' 'css'%}
{% endblock %}

{% block base_body %}
        {% translate "Text translated in template" %}
        
        {% if messages %}
            {% for message in messages %}
                <p>{{ message }}</p>
            {% endfor%}
        {% endif %}

        {% if text %}
            <p">{{ text }}</p>
        {% endif %}

        {% if routines %}
            {% for routine in routines %}
                <div class="col-xs-12 col-sm-12 col-md-6 col-lg-3 p-2 mb-2">
                    <div class="card text-center text-white bg-dark border border-primary rounded">
                        <div class="card text-center text-white bg-dark border border-primary rounded">
                            <div class="card-header">
                            <img class="text-center" src="{% webpack_static 'django-logo-positive.png' %}" width="150" height="90" alt="rxWod">
                        </div>
                        <div class="card-body">
                            <h5 class="card-title">{{ routine.date }}</h5>
                        </div>
                    </div>
                </div>
            {% endfor %}
        {% else %}
            <p>No routines available.</p>
        {% endif %}

    {% render_bundle 'index' 'js'%}
{% endblock %}

The metadata of the database can also be translated, we just have to import gettext and indicate the translation with _

from django.utils.translation import gettext_lazy as _
vi rxWod/models.py
from django.db import models
from django.contrib.auth.models import User
from django.core.validators import MaxValueValidator, MinValueValidator, validate_comma_separated_integer_list, MinLengthValidator, MaxLengthValidator
from django.utils.translation import gettext_lazy as _

# Exercise
class Exercise(models.Model):
    exercise_id = models.IntegerField(default=-1, unique=True, null=False, blank=False)
    name = models.CharField(max_length=200, unique=False, null=False, blank=False)
    description = models.CharField(max_length=200, unique=False, null=False, blank=False)
    default_value = models.IntegerField(default=1, validators=[MinValueValidator(1)])
    
    EXERCISE_CATEGORY = [
        (0, _('Shoulders')),
        (1, _('Back')),
        (2, _('Biceps')),
        (3, _('Triceps')),
        (4, _('Chest')),
        (5, _('Core')),
        (6, _('Gluteus')),
        (7, _('Quadriceps')),
        (8, _('Hamstring')),
        (9, _('Cardio')),
        (10, _('Lumbar')),
        (11, _('Grip')),
    ]
    category_id = models.PositiveSmallIntegerField(default=1, choices=EXERCISE_CATEGORY, null=False, blank=False)

    EXERCISE_LEVEL = [
        (0, 'N1'),
        (1, 'N2'),
        (2, 'RX'),
        (3, 'RX+'),
    ]
    level = models.PositiveSmallIntegerField(default=0, choices=EXERCISE_LEVEL, null=False, blank=False)
    url = models.URLField()
    is_metabolic = models.BooleanField(default=False, null=False, blank=False)

    def __str__(self):
        return_value = str(self.name) + ' [' + str(self.get_category_id_display()) + '] [' + str(self.get_level_display() + ']')
        return return_value


# Routine
class Routine(models.Model):
    date = models.DateTimeField(auto_now_add=True)
    # Each routine has 10 exercises separated by ,: 1,2,3,4,5,6,7,8,9,10
    exercise_ids = models.CharField(validators=[validate_comma_separated_integer_list], max_length=512, blank=True, null=False)
    exercise_repetitions = models.CharField(validators=[validate_comma_separated_integer_list], max_length=512, blank=True, null=False)
    rounds = models.IntegerField(default=1, validators=[MinValueValidator(1), MaxValueValidator(4)])
    user = models.ForeignKey(User, on_delete=models.CASCADE, null=False, blank=False)
    # There are 10 exercise categories, percentages are separated by ,: 1,2,3,4,5,6,7,8,9,10
    percentages = models.CharField(validators=[validate_comma_separated_integer_list], max_length=512, blank=False, null=False)
    level = models.IntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(100)])

    def __str__(self):
        return str(self.date)

We generate the directory where the translation files will be stored:

mkdir usersAuth/locale
mkdir rxWod/locale

We install gettext if we don’t already have it installed:

pkg install gettext

We generate the translation files:

cd rxWod
django-admin makemessages -l es

processing locale es

We leave the translations prepared in the django.po file.

vi locale/es/LC_MESSAGES/django.po

# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-03-20 13:19+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#: models.py:14
msgid "Shoulders"
msgstr "Hombros"

#: models.py:15
msgid "Back"
msgstr "Espalda"

#: models.py:16
msgid "Biceps"
msgstr "Biceps"

#: models.py:17
msgid "Triceps"
msgstr "Triceps"

#: models.py:18
msgid "Chest"
msgstr "Pecho"

#: models.py:19
msgid "Core"
msgstr "Core"

#: models.py:20
msgid "Gluteus"
msgstr "Glúteos"

#: models.py:21
msgid "Quadriceps"
msgstr "Cuadriceps"

#: models.py:22
msgid "Hamstring"
msgstr "Isquiotibial"

#: models.py:23
msgid "Cardio"
msgstr "Cardio"

#: models.py:24
msgid "Lumbar"
msgstr "Lumbar"

#: models.py:25
msgid "Grip"
msgstr "Agarre"

#: templates/rxWod/index.html:13
msgid "Text translated in template"
msgstr "Texto traducido en el template"

#: views.py:12
msgid "Message sent by index view"
msgstr "Mensaje enviado desde la vista index"

#: views.py:13
msgid "Text from view"
msgstr "Texto desde la vista"

We compile the translations:

django-admin compilemessages

processing file django.po in /usr/home/kr0m/rxWod/rxWodProject/rxWod/locale/es/LC_MESSAGES

Translating texts generated by JavaScript code is a bit more complicated since it runs on the client’s browser and does not have access to gettext. Therefore, translations must be requested from the server.

Django solves the problem by using a JS library and a special view that mimics the behavior of the gettext interface. This way, the client can obtain the translated texts through that library.

This can be a performance problem, so we must take several aspects into account:

We generate the locale directory in the root directory of our project. It is necessary to create it in this location because it is where our JS assets reside. When the makemessages or compilemessages command is executed, the files will be generated at this directory level and not within each app.

cd /home/kr0m/rxWod/rxWodProject
mkdir locale

To generate the special view, we must define a URL for this purpose:

 # JS translations
 path('jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
vi rxWodProject/urls.py
import debug_toolbar

from django.contrib import admin
from django.conf import settings
from django.urls import include, path
from django.views.i18n import JavaScriptCatalog

urlpatterns = [
    # JS translations
    path('jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
    # django.contrib.auth URLs
    path('accounts/', include('django.contrib.auth.urls')),
    path('', include('rxWod.urls')),
    path('', include('usersAuth.urls')),
    path('admin/', admin.site.urls),
    path('__debug__/', include(debug_toolbar.urls)),
]

We import the JS library in our template through a classic import.

<script src="{% url 'javascript-catalog' %}"></script>
vi rxWod/templates/rxWod/index.html
{% extends 'rxWod/base.html' %}
{% load render_bundle from webpack_loader %}
{% load webpack_static from webpack_loader %}
{% load i18n %}

{% block base_head %}
    {% render_bundle 'index' 'css'%}
{% endblock %}

{% block base_body %}
    <script src="{% url 'javascript-catalog' %}"></script>

    {% translate "Text translated in template" %}
        
    {% if messages %}
        {% for message in messages %}
            <p>{{ message }}</p>
        {% endfor%}
    {% endif %}

    {% if text %}
        <p">{{ text }}</p>
    {% endif %}

    {% if routines %}
        {% for routine in routines %}
            <div class="col-xs-12 col-sm-12 col-md-6 col-lg-3 p-2 mb-2">
                <div class="card text-center text-white bg-dark border border-primary rounded">
                    <div class="card text-center text-white bg-dark border border-primary rounded">
                        <div class="card-header">
                        <img class="text-center" src="{% webpack_static 'django-logo-positive.png' %}" width="150" height="90" alt="rxWod">
                    </div>
                    <div class="card-body">
                        <h5 class="card-title">{{ routine.date }}</h5>
                    </div>
                </div>
            </div>
        {% endfor %}
    {% else %}
        <p>No routines available.</p>
    {% endif %}

    {% render_bundle 'index' 'js'%}
{% endblock %}

Now we can use it through the gettext function:

gettext('text to translate')
vi assets/js/index.js
import img from '../images/django-logo-positive.png';

// ------ OnLoad checks ------
window.onload = function() {
    // Basic checks
    console.log('----- window.onload -----');
    if (typeof $.fn.popover == 'function') { 
        console.log('BootStrap working correctly');
    } else {
        console.log('BootStrap NOT working correctly');
    }

    if (typeof jQuery != 'undefined') {  
        // jQuery is loaded => print the version
        console.log('JQuery version: ' + jQuery.fn.jquery);
    }
};

alert(gettext('text to translate'));

console.log('Inside index. Edit me in assets/js/index.js');

To generate the JS catalogs, we must generate the files with the make messages command, but we must tell it to ignore the JS installed using yarn:

django-admin makemessages -l es -d djangojs -i node_modules

processing locale es

We leave the JS translations prepared in the djangojs.po file.

vi locale/es/LC_MESSAGES/djangojs.po

# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-03-20 09:34+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#: assets/js/index.js:20
msgid "text to translate"
msgstr "texto a traducir"

We compile the translations:

django-admin compilemessages

processing file djangojs.po in /usr/home/kr0m/rxWod/rxWodProject/locale/es/LC_MESSAGES  
processing file django.po in /usr/home/kr0m/rxWod/rxWodProject/rxWod/locale/es/LC_MESSAGES

The translations of the JS files will have been generated in the locale/es/LC_MESSAGES/ directory, but each app needs them in its own directory, so we copy them to each one of them (in this example only rxWod):

cp locale/es/LC_MESSAGES/djangojs.mo rxWod/locale/es/LC_MESSAGES/djangojs.mo
#cp locale/es/LC_MESSAGES/djangojs.mo usersAuth/locale/es/LC_MESSAGES/djangojs.mo

When running the makemessages command, a translation similar to an existing one may be detected. If this happens, in the generated .po file we will see a comment #, fuzzy. If we leave it as fuzzy, the text will NOT be translated. If the text should be translated, we must remove the comment.

#: models.py:19
#, fuzzy
msgid "Quadriceps"
msgstr "Cuadriceps"

In summary, the commands to execute to generate all the translations of the project are the following:

django-admin makemessages -l es -d djangojs -i node_modules
cd rxWod
django-admin makemessages -l es
cd ..
#cd usersAuth
#django-admin makemessages -l es
#cd ..
django-admin compilemessages
cp locale/es/LC_MESSAGES/djangojs.mo rxWod/locale/es/LC_MESSAGES/djangojs.mo
#cp locale/es/LC_MESSAGES/djangojs.mo usersAuth/locale/es/LC_MESSAGES/djangojs.mo

In the previous article , we programmed a small script to deploy changes in production more easily and simply. We added the translation compilation part:

vi .scripts/deploy.sh

#!/usr/local/bin/bash
cd /home/kr0m/rxWod/rxWodProject
git pull
cd ..
source bin/activate
pip install -r rxWodProject/requirements.txt
cd rxWodProject
python manage.py makemigrations
python manage.py migrate
django-admin makemessages -l es -d djangojs -i node_modules
cd rxWod
django-admin makemessages -l es
cd ..
#cd usersAuth
#django-admin makemessages -l es
#cd ..
django-admin compilemessages
cp locale/es/LC_MESSAGES/djangojs.mo rxWod/locale/es/LC_MESSAGES/djangojs.mo
#cp locale/es/LC_MESSAGES/djangojs.mo usersAuth/locale/es/LC_MESSAGES/djangojs.mo
yarn install
yarn prod-build
python manage.py collectstatic --noinput
sudo /usr/sbin/service daphne restart

In the Django documentation, we can find a troubleshooting section about translations , which is very useful in case of unexpected problems.

We compile the WebPack bundles and start the integrated server:

yarn dev-build

We check that the translations work by accessing:
http://localhost:8000/

First, we see in the JS that the text appears in English because the browser is configured in English.

The rest of the website also appears in English:

On the other hand, if we change the language, we will see the translated texts:

The rest of the website too:

NOTE: We can see that Django has automatically changed the date format without us having indicated anything about it.


Django is capable of automatically translating plurals. To do this, we only need to indicate the possible options using ngettext:

    count_text = ngettext(
        'there is %(count)d object',
        'there are %(count)d objects',
        count,
    ) % {
        'count': count,
    }

We modify the index view to make use of ngetttext:

vi rxWod/views.py

from django.shortcuts import get_object_or_404, get_list_or_404, render, redirect
from django.http import HttpResponse
from .models import Routine
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.utils.translation import gettext as _
from django.utils.translation import ngettext

@login_required()
def index(request):
    #print('username: %s' % request.user.username)
    routines = Routine.objects.filter(user=request.user)
    messages.success(request, _('Message sent by index view'))
    text = _('Text from view')

    count = int(request.GET['count'])
    count_text = ngettext(
        'there is %(count)d object',
        'there are %(count)d objects',
        count,
    ) % {
        'count': count,
    }

    context = {
        'routines': routines,
        'text': text,
        'count_text': count_text, 
    }
    return render(request, 'rxWod/index.html', context)

We modify the template associated with the view to display the text passed via context:

vi rxWod/templates/rxWod/index.html

{% extends 'rxWod/base.html' %}
{% load render_bundle from webpack_loader %}
{% load webpack_static from webpack_loader %}
{% load i18n %}

{% block base_head %}
    {% render_bundle 'index' 'css'%}
{% endblock %}

{% block base_body %}
    <script src="{% url 'javascript-catalog' %}"></script>

        {% if count_text %}
            <p">{{ count_text }}</p>
        {% endif %}

        {% translate "Text translated in template" %}

        {% if messages %}
            {% for message in messages %}
                <p>{{ message }}</p>
            {% endfor%}
        {% endif %}

        {% if text %}
            <p">{{ text }}</p>
        {% endif %}

        {% if routines %}
            {% for routine in routines %}
                <div class="col-xs-12 col-sm-12 col-md-6 col-lg-3 p-2 mb-2">
                    <div class="card text-center text-white bg-dark border border-primary rounded">
                        <div class="card text-center text-white bg-dark border border-primary rounded">
                            <div class="card-header">
                            <img class="text-center" src="{% webpack_static 'django-logo-positive.png' %}" width="150" height="90" alt="rxWod">
                        </div>
                        <div class="card-body">
                            <h5 class="card-title">{{ routine.date }}</h5>
                        </div>
                    </div>
                </div>
            {% endfor %}
        {% else %}
            <p>No routines available.</p>
        {% endif %}

    {% render_bundle 'index' 'js'%}
{% endblock %}

We generate the translations:

​cd /home/kr0m/rxWod/rxWodProject/rxWod
django-admin makemessages -l es

We translate the new translations:

vi rxWod/locale/es/LC_MESSAGES/django.po

# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-03-20 18:24+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#: models.py:14
msgid "Shoulders"
msgstr "Hombros"

#: models.py:15
msgid "Back"
msgstr "Espalda"

#: models.py:16
msgid "Biceps"
msgstr "Biceps"

#: models.py:17
msgid "Triceps"
msgstr "Triceps"

#: models.py:18
msgid "Chest"
msgstr "Pecho"

#: models.py:19
msgid "Core"
msgstr "Core"

#: models.py:20
msgid "Gluteus"
msgstr "Glúteos"

#: models.py:21
msgid "Quadriceps"
msgstr "Cuadriceps"

#: models.py:22
msgid "Hamstring"
msgstr "Isquiotibial"

#: models.py:23
msgid "Cardio"
msgstr "Cardio"

#: models.py:24
msgid "Lumbar"
msgstr "Lumbar"

#: models.py:25
msgid "Grip"
msgstr "Agarre"

#: templates/rxWod/index.html:17
msgid "Text translated in template"
msgstr "Texto traducido en el template"

#: views.py:12
msgid "Message sent by index view"
msgstr "Mensaje enviado desde la vista index"

#: views.py:13
msgid "Text from view"
msgstr "Texto desde la vista"

#: views.py:17
#, python-format
msgid "there is %(count)d object"
msgid_plural "there are %(count)d objects"
msgstr[0] "hay %(count)d objeto"
msgstr[1] "hay %(count)d objetos"

We compile the translations:

cd ..
django-admin compilemessages

We compile the WebPack bundles and start the test server:

yarn dev-build

Now, if we access the index passing the count parameter via GET, it will appear in singular if it is singular, and in plural if it is plural:

However, if count is greater than one:

Of course, it behaves the same way in English:



Regarding the database, we are translating metadata, but we still have to translate the content of the database itself. To do this, we will have to install one of the available modules .

I personally have opted for django-modeltranslation .

We install the module and freeze the dependencies:

cd /home/kr0m/rxWod/rxWodProject
pip install django-modeltranslation
pip freeze > requirements.txt

We add the modeltranslation app:

vi rxWodProject/settings.py

INSTALLED_APPS = [
    'modeltranslation',
    ...
]

And we indicate the languages we are going to support, the first one will be the default language:

vi rxWodProject/settings.py

gettext = lambda s: s
LANGUAGES = (
    ('en', gettext('English')),
    ('es', gettext('Spanish')),
)

We indicate the fields of the model to be translated, in my case name and description:

vi rxWod/translation.py

from modeltranslation.translator import translator, TranslationOptions
from .models import Exercise

class ExerciseTranslationOptions(TranslationOptions):
    fields = ('name', 'description')

translator.register(Exercise, ExerciseTranslationOptions)

We synchronize the database to generate the additional fields:

python manage.py sync_translation_fields
Missing languages in "name" field from "rxWod.exercise" model: en, es

SQL to synchronize "rxWod.exercise" schema:
   ALTER TABLE "rxWod_exercise" ADD COLUMN "name_en" varchar(200);
   ALTER TABLE "rxWod_exercise" ADD COLUMN "name_es" varchar(200);

Are you sure that you want to execute the previous SQL: (y/n) [n]: y
Executing SQL...
Done
Missing languages in "description" field from "rxWod.exercise" model: en, es

SQL to synchronize "rxWod.exercise" schema:
   ALTER TABLE "rxWod_exercise" ADD COLUMN "description_en" varchar(200);
   ALTER TABLE "rxWod_exercise" ADD COLUMN "description_es" varchar(200);

Are you sure that you want to execute the previous SQL: (y/n) [n]: y
Executing SQL...
Done

NOTE: We must run this command every time a new language is added to the settings.LANGUAGES parameter and every time a new field is added to translate in rxWod/translation.py.

We check that the new fields exist in the database:

su
su - postgres
psql
\c rxwod
\d "rxWod_exercise"  
                                         Table "public.rxWod_exercise"  
     Column     |          Type          | Collation | Nullable |                   Default                      
----------------+------------------------+-----------+----------+----------------------------------------------  
 id             | integer                |           | not null | nextval('"rxWod_exercise_id_seq"'::regclass)  
 exercise_id    | integer                |           | not null |   
 name           | character varying(200) |           | not null |   
 description    | character varying(200) |           | not null |   
 default_value  | integer                |           | not null |   
 category_id    | smallint               |           | not null |   
 level          | smallint               |           | not null |   
 url            | character varying(200) |           | not null |   
 is_metabolic   | boolean                |           | not null |   
 name_en        | character varying(200) |           |          |   
 name_es        | character varying(200) |           |          |   
 description_en | character varying(200) |           |          |   
 description_es | character varying(200) |           |          |   
Indexes:  
    "rxWod_exercise_pkey" PRIMARY KEY, btree (id)  
    "rxWod_exercise_exercise_id_key" UNIQUE CONSTRAINT, btree (exercise_id)  
Check constraints:  
    "rxWod_exercise_category_id_check" CHECK (category_id >= 0)  
    "rxWod_exercise_level_check" CHECK (level >= 0)

We can see that the additional fields are empty:

SELECT * FROM "rxWod_exercise" LIMIT 1;  
  
 id | exercise_id |   name    | description | default_value | category_id | level |          url           | is_metabolic | name_en | name_es | description_en | description_es 
----+-------------+-----------+-------------+---------------+-------------+-------+------------------------+--------------+---------+---------+----------------+----------------
  1 |           0 | test ex   |test ex desc |             1 |           4 |     0 | http://alfaexploit.com | f            |         |         |                | 
(1 row)

To populate the additional fields, we can do it manually or copy the original fields to the translations .

The following command will copy the data to the default language, in this case EN:

python manage.py update_translation_fields

Using default language: en  
Working on models: rxWod.Exercise  
Updating data of model '<class 'rxWod.models.Exercise'>'

We check that the data has been copied:

SELECT * FROM "rxWod_exercise" LIMIT 1;  
  
 id | exercise_id |   name    | description | default_value | category_id | level |          url           | is_metabolic |  name_en  | name_es | description_en | description_es 
----+-------------+-----------+-------------+---------------+-------------+-------+------------------------+--------------+-----------+---------+----------------+----------------
  1 |           0 | test ex   |test ex desc |             1 |           4 |     0 | http://alfaexploit.com | f            | test ex   |         | test ex desc   | 
(1 row)

If we also want to copy it to another language, we can indicate it:

python manage.py update_translation_fields –language es

Using default language: en  
Working on models: rxWod.Exercise  
Updating data of model '<class 'rxWod.models.Exercise'>'

We check that the data has been copied:

SELECT * FROM "rxWod_exercise" LIMIT 1;
 id | exercise_id |   name    | description | default_value | category_id | level |          url           | is_metabolic |  name_en  |  name_es  | description_en | description_es 
----+-------------+-----------+-------------+---------------+-------------+-------+------------------------+--------------+-----------+-----------+----------------+----------------
  1 |           0 | test ex   |test ex desc |             1 |           4 |     0 | http://alfaexploit.com | f            | test ex   | test ex   | test ex desc   | test ex desc 
(1 row)

Every time we introduce a new exercise, we must copy the data to the default language:

python manage.py update_translation_fields

And fill in the corresponding exercise’s Spanish translation from the admin interface:

The fields that come from the model and are displayed in the admin interface, such as the category, also appear translated according to the browser’s language preferences:


We modify the index view to query the exercises:

vi rxWod/views.py
from django.shortcuts import get_object_or_404, get_list_or_404, render, redirect
from django.http import HttpResponse
from .models import Exercise
from django.contrib.auth.decorators import login_required
from django.contrib import messages

@login_required()
def index(request):
    exercises = Exercise.objects.all()

    context = {
        'exercises': exercises,
    }
    return render(request, 'rxWod/index.html', context)

We modify the template to display the exercises:

vi rxWod/templates/rxWod/index.html

{% extends 'rxWod/base.html' %}
{% load render_bundle from webpack_loader %}
{% load webpack_static from webpack_loader %}

{% block base_head %}
    {% render_bundle 'index' 'css'%}
{% endblock %}

{% block base_body %}
        {% if exercises %}
            {% for exercise in exercises %}
                <div class="col-xs-12 col-sm-12 col-md-6 col-lg-3 p-2 mb-2">
                    <div class="card text-center text-white bg-dark border border-primary rounded">
                        <div class="card text-center text-white bg-dark border border-primary rounded">
                            <div class="card-header">
                            <img class="text-center" src="{% webpack_static 'django-logo-positive.png' %}" width="150" height="90" alt="rxWod">
                        </div>
                        <div class="card-body">
                            <h5 class="card-title">{{ exercise.name }}</h5>
                            <h3>{{ exercise.description }}</h3>
                        </div>
                    </div>
                </div>
            {% endfor %}
        {% else %}
            <p>No routines available.</p>
        {% endif %}
{% endblock %}

Now when we access the website with the browser configured in English, we will see the content coming from the database translated:

However, if we access it in Spanish, we will see it in Spanish:

If you liked the article, you can treat me to a RedBull here