Esta pagina se ve mejor con JavaScript habilitado

Django Traducciones

 ·  🎃 kr0m

Django proporciona mecanismos para la traducción de las aplicaciones, tanto de los textos de las vistas, los de los templates, los mostrados por código JavaScript y la meta información de los modelos pero esto no incluye el contenido en base de datos, para traducir dicho contenido utilizaremos el módulo django-modeltranslation. En este artículo mostraremos un ejemplo muy sencillo pero ilustrativo sobre como utilizar todos los recursos mencionados.

Antes de comenzar es recomendable leer los artículos anteriores sobre Django ya que son los pasos previos a este artículo:


Como siempre activamos el venv del proyecto:

cd rxWod
source bin/activate
cd rxWodProject/

Para soportar varios idiomas en Django debemos habilitar el middleware django.middleware.locale.LocaleMiddleware:

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',
    ]

Cambiamos el lenguaje por defecto a inglés de este modo cuando no encuentre traducciones para un idioma por defecto mostrará los textos en inglés:

vi rxWodProject/settings.py

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

Django determina el idioma a utilizar mediante las siguientes comprobaciones:

  • Prefijo en la URL, para que esto funcione debemos habilitar en la configuración de URLs de Django los i18n_patterns
  • Existencia de la Cookie: django_language
  • Cabecera HTTP Accept-Language
  • Si todo lo anterior falla utiliza el lenguaje por defecto: LANGUAGE_CODE

Para realizar las pruebas de forma mas cómoda es recomendable instalar algún addon de cambio de idioma en el navegador.


Como ejemplo muy sencillo vamos a traducir un message y un texto desde la vista index, para ello necesitamos importar gettext e indicar lo que deseamos traducir con _

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)

En el template index vamos a traducir otro texto de prueba, para ello cargamos la internacionalización:

{% load i18n %}

Y utilizamos el tag translate:

{% translate "Text translated in template" %}

Quedaría del siguiente modo:

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 %}

La metainformación de la base de datos también se puede traducir, tan solo debemos importar gettext e indicar la traducción con _

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)

Generamos el directorio donde se almacenarán los ficheros de traducción:

mkdir usersAuth/locale
mkdir rxWod/locale

Instalamos gettext si no lo tenemos ya instalado:

pkg install gettext

Generamos los ficheros de traduccions:

cd rxWod
django-admin makemessages -l es

processing locale es

Dejamos las traducciones preparadas en el fichero django.po

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"

Compilamos las traducciones:

django-admin compilemessages

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

La traducción de textos generados por código JavaScript resulta un poco mas complicada ya que este se ejecuta en el navegador del cliente y no tiene acceso a gettext por lo tanto las traducciones deben ser solicitadas al servidor.

Django resuelve el problema mediante una librería JS y una vista especial que imita el comportamiento de la interfaz gettext, de este modo el cliente podrá obtener los textos traducidos mediante dicha librería.

Esto puede suponer un problema de rendimiento por lo tanto debemos tener varios aspectos en cuenta:

Generamos el directorio locale en el directorio raíz de nuestro proyecto, es necesario crearlo en esta localización porque es donde residen nuestros assets JS, cuando se ejecute el comando makemessages o compilemessages se generarán los ficheros en este nivel del árbol de directorios y no dentro de cada app.

cd /home/kr0m/rxWod/rxWodProject
mkdir locale

Para generar la vista especial debemos definir una URL para tal fin:

 # 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)),
]

Importamos la librería JS en nuestro template mediante un import clásico.

<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 %}

Ahora ya podemos hacer uso de ella mediante la función gettext:

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');

Para generar los catálogos JS debemos generar los ficheros con el comando make messages pero debemos indicarle que ignore los JS instalados mediante yarn:

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

processing locale es

Dejamos las traducciones JS preparadas en el fichero djangojs.po

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"

Compilamos las traducciones:

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

Las traducciones de los ficheros JS se habrán generado en el directorio locale/es/LC_MESSAGES/ pero cada app las necesita en su propio directorio así que las copiamos a cada una de ellas(en este ejemplo solo 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

Al ejecutar el comando makemessages puede que se detecte una traducción similar a una ya existente, si esto ocurre en el fichero .po generado veremos un comentario #, fuzzy, si lo dejamos como fuzzy el texto NO se traducirá, si el texto debe traducirse debemos eliminar el comentario.

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

Como resumen los comandos a ejecutar para generar todas las traducciones del proyecto son los siguientes:

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

En el artículo anterior programamos un pequeño script para desplegar cambios en producción de forma mas fácil y sencilla, añadimos la parte de compilación de traducciones:

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

En la documentación de Django podemos encontrar una sección de troubleshooting acerca de las traducciones , esta resulta muy útil en caso de problemas inesperados.

Compilamos los bundles de WebPack y arrancamos el servidor integrado:

yarn dev-build

Comprobamos que las traducciones funcionen accediendo a:
http://localhost:8000/

Primero vemos en el JS que el texto aparece en inglés, ya que el navegador está configurado en inglés.

El resto de la web también aparece en inglés:

Por otro lado si cambiamos el idioma veremos los textos traducidos:

El resto de la web también:

NOTA: Podemos ver que Django de forma automática ha cambiado el formato de fecha sin que nosotros hayamos indicado nada al respecto.


Django es capaz de traducir los plurales de forma automática, para ello tan solo debemos indicar las posibles opciones mediante ngettext:

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

Modificamos la vista index para que haga uso de 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)

Modificamos el template asociado a la vista para que muestre el texto pasado vía contexto:

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 %}

Generamos las traducciones:

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

Traducimos las nuevas traducciones:

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"

Compilamos las traducciones:

cd ..
django-admin compilemessages

Compilamos los bundles de WebPack y arrancamos el servidor de pruebas:

yarn dev-build

Ahora si accedemos al index pasándole el parámetro count vía GET, si es singular aparecerá en singular, si es plural en plural:

En cambio si count vale mas de uno:

Por supuesto en inglés se comporta igual:



En cuanto a la base de datos estamos traduciendo metainformación pero todavía queda el contenido de la propia base de datos, para traducirlo tendremos que instalar uno de los módulo disponibles .

Yo personalmente he optado por django-modeltranslation .

Instalamos el módulo y congelamos las dependencias:

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

Añadimos la app modeltranslation:

vi rxWodProject/settings.py

INSTALLED_APPS = [
    'modeltranslation',
    ...
]

E indicamos los lenguajes que vamos a soportar, el primero será el idioma por defecto:

vi rxWodProject/settings.py

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

Indicamos los campos del modelo a traducir, en mi caso name y 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)

Sincronizamos la base de datos para que genere los campos adicionales:

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

NOTA: Debemos ejecutar este comando cada vez que se añada un lenguaje nuevo al parámetro settings.LANGUAGES y cada que se añada un campo nuevo a traducir en rxWod/translation.py

Comprobamos que los nuevos campos existan en la base de datos:

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)

Podemos ver que los campos adicionales están vacíos:

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)

Para poblar los campos adicionales podemos hacerlo manualmente o copiar los campos originales a las traducciones .

El siguiente comando nos copiará los datos al idioma por defecto, en este caso EN:

python manage.py update_translation_fields

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

Comprobamos que haya copiado los datos:

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)

Si además queremos copiarlo a otro idioma podemos indicarlo:

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'>'

Comprobamos que haya copiado los datos:

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)

Cada vez que introduzcamos un ejercicio nuevo debemos copiar los datos al idioma por defecto:

python manage.py update_translation_fields

Y rellenar la traducción a español del ejercicio correspondiente desde la interfaz de admin:

Los campos que provienen del modelo y se muestran en la interfaz de admin como puede ser la categoria también aparecen traducidos según las preferencias de idioma del navegador:


Modificamos la vista index para que consulte los ejercicios:

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)

Modificamos el template para que muestre los ejercicios:

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 %}

Ahora cuando accedemos a la web con el navegador configurado en inglés veremos el contenido proveniente de la base de datos traducido:

En cambio si accedemos en español lo veremos en español:

Si te ha gustado el artículo puedes invitarme a un RedBull aquí