How to add translations for django website (python, js, templates and models) p. 5

Clock
06.02.2025
Clock
17.03.2025
Clock
19 minutes
An eye
178
Hearts
0
Connected dots
0
Connected dots
0
Connected dots
0

Introduction

You won't believe it, but I've wanted to write this article for a long time. Do you know why? Because this is the first thing I did for my site and the first thing I figured out. In this article, we will translate my search results parsing site. And I will try to cover all the possible problems and situations that can happen to you when you also want to localize your site.

How does it work

When localizing, you need to understand how exactly you want to display a different version of the language. And it is a good idea to display this directly in the URL. There are options:
  1. Create a separate site with a corresponding domain. Like example.de. It is too expensive and impractical.
  2. Add a subdomain. Like ru.example.com
  3. Add a directory. Like example.com/en/
  4. Or use the URL parameter example.com?loc=en
Each of these options has its pros and cons. And I am not one of those who can talk about it. Everything is described in detail here, in the Google blog. I only tried the option with directories because ... well, I have one server and a limited budget. Therefore, in this article, you will find this particular method of displaying translations.
In any case, translating a site to Django can be divided into 4 conditional groups. These are string translation in Python, Django model translation, string translation in templates, and string translation in JavaScript code.
Translations of strings for a website can be divided into the following stages:
  1. Mark (highlight) the necessary strings for translation.
  2. Collect all marked (highlighted) strings.
  3. Write translations for them
  4. Compile a special *.mo files
These stages are not applicable when translating and localizing django models. They have their own specifics.

Basic setup

But before we start selecting and translating, we need to tweak our site a little bit. And first of all, we'll connect the corresponding middleware. Its main role is redirects to the corresponding localized pages and insertion of those translation strings that are needed for the current site locale.
In the settings.py file:
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',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'corsheaders.middleware.CorsMiddleware',
# BY ALLAUTH
"allauth.account.middleware.AccountMiddleware",
]
You also need to write down the default language code, which is also the current one, and prepare a list of supported localizations (translations) of the site. Also add the global variable USE_I18N. The last point is not required, it is enabled by default, but I just like everything to be explicitly configured. Also in the settings.py file, somewhere at the end, add:
LANGUAGE_CODE = 'en'
USE_I18N = True
LANGUAGES = [
('en', 'English'),
('ru', 'Russian'),
('de', 'German'),
('es', 'Spanish'),
('fr', 'French'),
('be', 'Belorussian')
]
Since this site is, in fact, a regular web application, that is, the content will not be published there, I can afford to write all the necessary translations through Google Translate.
The final touch will be to indicate which paths we want to localize. In this case, it makes no sense to localize all the API or admin paths, and even more so the allauth paths. This is what will remain untouched. That is, we will localize our frontend. These are the about, contacts, home pages, and the email confirmation and password reset pages.
In the Website/urls.py file, add the following lines:
from django.contrib import admin
from django.urls import path, include
from rest_framework import routers
from Backend import views
from django.conf.urls.i18n import i18n_patterns

router = routers.DefaultRouter()
router.register(r'results', views.BackendModelView, 'result')

urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include(router.urls)),
# BY ALLAUTH
path('accounts/', include('allauth.urls')),
path("_allauth/", include("allauth.headless.urls")),
]

urlpatterns += i18n_patterns(
path('', include('Authentication.urls')),
path('', include('Frontend.urls')),
)
By the way, the i18n_patterns function can only be used in the main urls.py. That's it.
The preliminary setup is complete. Now you can check how it works. Go to my site (when I place this site on the hosting, a link will appear here), and if you do this in parallel with me, then opening a browser at localhost:8000, you should be redirected to localhost:8000/en/.
The localization version you are redirected to depends on your browser settings. I have the English version. If your browser is in a language that the site does not support, for example, Ukrainian, then the redirection will be to the localization specified in LANGUAGE_CODE.
You can go to any language version of your site if, of course, you added it to LANGUAGES. Of course, there is nothing to look at yet, but be patient until the translations. Everything will be \( ̄︶ ̄*\))

Select lines for translation

We need to specify what, in fact, needs to be translated, and for each part of the site, be it a template, or python code, or javascript code, it will be different.

Translations in python code

In order to mark lines for translation, you need to import the following functions: gettext and gettext_lazy.
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as __
Next, you just need to wrap the line you need in gettext or gettext_lazy. I'll note right away that it's not possible to wrap the gettext function around every line. For example, a variable or calculated values. That is:
def translation_test():
str = _("Usual string")
var_for_str1 = "Variable for str1"
str1 = _(var_for_str1)
list_for_str2 = ['Variable', 'for', 'str2']
str2 = _("-".join(list_for_str2))
In this example, django will be able to detect only the first line, "Usual string". It will ignore all the rest.
I also imported a function such as gettext_lazy. What is it so special, and when to use it? In general, the only difference between them is that the second one takes a line from the *.po file only when this line is used on the server. At least that's what is written on the official django website.
This function should be used when you need to translate django models and their attributes and descriptions for the admin panel. But in 90% of cases, gettext will be enough for you. Plus, as I mentioned earlier, to make models international, you will need something more than the built-in django utilities.
At the current moment of development of the site, SearchResultParser, there is not a single line on the backend that would need to be translated. And if your project has such lines, then just wrap them in gettext and move on.

Translations in templates

To translate text in templates, you only need to know three tags:
{% load i18n %}

{% trans "YOUR TRANSLATION STRING" %}

{% blocktrans %}
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec quis hendrerit arcu, vitae lacinia arcu. Pellentesque habitant morbi tristique senectus et netus et malesuada fames.
{% endblocktrans %}
At the beginning of the template, you will need to load a special module, i18n. Every time you need to translate some text in the template, load the module. Even if those templates inherit from another template that has already loaded i18n.
After the i18n module has been loaded. You have two options. The first is to use a single-line translation . The second is to use a multi-line translation . The difference between them is that between the tags and everything will be translated, including other tags. Stay tuned.
Now let's select all the lines needed for translation in app.html, base.html, about.html and contacts.html. I will show you an example of what the processed template will look like; the Frontend/templates/Frontend/about.html file:
{% extends 'Frontend/base.html' %}
{% load static %}
{% load i18n %}

{% block head %}
<title>SearchResultParser - {% trans "About this project" %}</title>
<meta name="description" content="">
<link rel="canonical" href="{% url 'about' %}">
{% endblock %}

{% block header %}
<div data-type="inner-link"><a href="{% url 'main' %}">{% trans "main" %}</a></div>
{% endblock %}

{% block main %}
<div class="flex flex-col w-full h-full justify-center items-center">
<div class="flex flex-col max-w-3xl text-justify gap-2 p-1">
<h1 class="text-5xl mb-4">{% trans "About search result parser webtool" %}</h1>
<p>
{% blocktrans %}
Hello guest, this online tool is a kind of Frankenstein. On the one hand, it is an excellent <b>SEO tool</b> with which you can collect ultimate data about websites on top of search results pages and the distribution of keywords in titles and descriptions. On the other hand, I made this tool with open-source code and a <a href="https://timthewebmaster.com/en/articles/series-of-articles-about-search-result-parser-webtool/">step-by-step guide</a> on my own site. That is, it is also a <b>training resource</b> for all those who want to put together something similar. I hope they will.
{% endblocktrans %}
</p>

<p>
{% blocktrans %}
This tool works on the following stack. <b>Django 5.1</b> is its backend, the place where all the magic of processing user requests, managing user credentials, rendering templates that you see, and parsing results takes place. <b>React 18.1</b> is its frontend; all animations, transitions, and logic that occur are already on your machine; all this is React. One of the most popular JavaScript frameworks in the world. Also, in order not to plunge into CSS madness, this site uses <b>TailwindCSS</b>—as a way to avoid the inevitable. As a backend add-on to Django 5.1 for managing user authentication, I use such a Django application as <i>django-allauth</i>. All parsing is possible thanks to my numerous bots, mini-parsers, such a wonderful tool as <i>Selenium</i>, and no less wonderful library <i>BeautifulSoup4</i>.
{% endblocktrans %}
</p>

<p>
{% blocktrans %}
Although I am attracted by the policy of the Google search engine, supposedly all site owners should pretend that they do not know about Google and SEO and should accept the flow of visitors to the site as a fact and should just make good content; this is not true. Some even say that Google is a mirror of the entire Internet. Yes, but the mirror does not distort the headings and titles; the mirror does not require site owners to look beautiful and convenient. Maybe this was true before, but not now.
{% endblocktrans %}
</p>

<p>
{% blocktrans %}
That is why it is worth considering and knowing which sites are higher in search results and which are lower. How exactly do they differ from each other, and why does Google, for example, consider these sites more relevant than others. And that's why I made this tool to detect and help find patterns in Google's behavior through its search results. And also, I wanted to show that anyone, if they want, can write a similar tool for themselves, and it will even work.
{% endblocktrans %}
</p>
<p>ヾ( ̄▽ ̄)~~</p>
</div>
</div>
{% endblock %}

{% block scripts %}
{% endblock %}

Translations in JS files by Django

Configuring translations of JavaScript code is probably the most difficult and tedious. Although at first glance, there seems to be a tutorial, and nothing out of the ordinary is required. But more on that later. Let me first show you how to configure JS translations via Django.
The first thing you need to do is to include the corresponding paths in Website/urls.py. Note that if you use the i18n_patterns function, you also need to include the JavaScriptCatalog class there.
from django.contrib import admin
from django.urls import path, include
from rest_framework import routers
from Backend import views
from django.conf.urls.i18n import i18n_patterns
from django.views.i18n import JavaScriptCatalog

router = routers.DefaultRouter()
router.register(r'results', views.BackendModelView, 'result')

urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include(router.urls)),
# BY ALLAUTH
path('accounts/', include('allauth.urls')),
path("_allauth/", include("allauth.headless.urls")),
]

urlpatterns += i18n_patterns(
path("jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"),
path('', include('Authentication.urls')),
path('', include('Frontend.urls')),
)
The JavaScriptCatalog class will generate a mini-js library for us with a catalog of translation strings. Yes, this will reduce the page loading speed a little, so in the future you will need to remember to cache it. Now connect it in the base.html template, and we are ready to translate.
<script src="{% url 'javascript-catalog' %}"></script>
The translation backend setup is ready. Now we need to mark the strings we want to translate. So, we only need the gettext() function. Let me show you an example of one of my functions, onReqeustPasswordReset:
function onRequestPasswordReset(req){
Wait()
var form_data = new FormData()
var email = document.querySelector('#password_email').value
form_data.append('email', email)
axios.post(URLs.REQUEST_PASSWORD_RESET, form_data, {
headers: {
"accept": "application/json",
"X-CSRFToken": getCSRFToken(),
'Content-Type': "application/json",
}
}).then((data)=>{
StopWait(gettext(`Successfully send an email to ${email}`), 'success')
req.setModal(false);
}).catch((err)=>{
StopWait(`${err}`, 'error')
req.setModal(false);
})

}
Also in JSX you will need to wrap the gettext function in brackets like these {}. Example from the fetchProfile function:
function fetchProfile(req){
updateSession()

return (
<div>
{ session.is_authenticated ?
<Box>
<Typography>
{gettext('You are logged in as')} <b>{session.username}</b>
</Typography>
<Box className="flex flex-row gap-4 items-center">
<Button onClick={()=>{fetchPasswordChange(req)}} variant='text'>{gettext('Change password')}</Button>
<Button onClick={()=>{fetchPasswordReset(req)}} variant='text'>{gettext('Reset password')}</Button>
</Box>
<Typography>
{gettext('Your email are:')} <b>{session.email.address}</b> | {session.email.verified}
</Typography>
{ session.email.verified === 'not verified' &&
<Box className="flex flex-row gap-4 items-center">
<Button onClick={()=>{onRequestConfirmEmail(req, session.email.address)}} variant='text'>{gettext('verify')}</Button>
</Box>
}
<Box className="flex flex-row gap-4 items-center">
<Button onClick={()=>{onSignout(req)}} variant='text'>{gettext('Sign out')}</Button>
</Box>
</Box>
:
<Box>
<Typography>
{gettext('You are not logged in')}
</Typography>
<Box className="flex flex-row gap-4 items-center">
<Button onClick={()=>{fetchLogin(req)}} variant='text'>{gettext('Log in')}</Button>
<Typography>{gettext('or')}</Typography>
<Button onClick={()=>{fetchSignup(req)}} variant='text'>{gettext('Sign up')}</Button>
</Box>
</Box>
}
</div>
)
}
And that's basically it, we're ready, and we can collect the strings and compile them into *.mo files. Let's see what we got. Here's the original page, in English:
And here is the page translated into Russian. Can you find anything strange?
Yeah, not everything is translated. Even more, django couldn't find my strings to translate. That's why I couldn't write the translation. Why is that? Most likely because of React, it's compiled into one package, and it's used, not the components I wrote.
I searched the Internet to solve this problem and found a guy who had a similar problem. This dude made a corresponding post on Medium about how to make the makemessages command find strings. In short, he had to get into the Django source code and add the --language=python parameter when the djangojs domain was specified. For some reason unknown to me, xgettext finds more strings to translate when the python language flag is specified. ¯\(°_o)/¯
In any case, this method doesn't suit me, it's too unreliable. And breaking Django just to translate a couple of strings... No, I'd rather implement the translation of JS files through React. More on that in another chapter. But for now...

Collect, Translate and Compile Translations

After setting up translations of templates, python and javascript code (using django), you need to collect all the marked strings into *.po files. Having previously created a locale directory in each application that has strings for translation. After this, you need to run the following command:
python .\manage.py makemessages -l ru
If you have also translated the lines in JS, then you need to run another command. In this command, we use the -d flag with the value djangojs. This is a mandatory flag if you want this to work.
python .\manage.py makemessages -d djangojs -l ru -i node_modules/*
Please also use the -i or --ignore flag too. Look, I use Node.js and ReactJS. They are all in the node_modules directory and not only them, actually. If you don't set this flag, django will responsibly check every single fucking file in your project, and there will be tens of thousands of them. And it will take time, so I warned you.
As a result of these two commands, we will get the files django.po - from the first command and djangojs.po - as a result of the second command. There will be lines like this:
#: .\Frontend\templates\Frontend\base.html:25
msgid "profile"
msgstr "Profil"

#: .\Frontend\templates\Frontend\base.html:39
msgid "TimTheWebmaster ➥"
msgstr "TimTheWebmaster ➥"

#: .\Frontend\templates\Frontend\base.html:42
msgid ""
"\n"
" Made by <div id=\"ref_to_place\" class=\"pl-1 pr-1\"></div> "
"Copyright © 2024\n"
" "
msgstr ""
"\n"
" Erstellt von <div id=\"ref_to_place\" class=\"pl-1 pr-1\"></div> "
"Copyright © 2024\n"
" "
I just want to make a couple of comments when you start doing translations. Don't touch anything inside msgid, that's the first thing. And second, when translating multi-line elements (those that start with "\n") after msgid, msgstr should also start with "\n".
In any case, if there are errors during compilation, they are very informative and you will have no problem solving them by yourself.
The last stage of translations is their compilation into *.mo files. To compile translations, regardless of whether they are from a template, python or js, use the following command:
python .\manage.py compilemessages
Congratulations! We have finished translating our site, and now it can support many other languages, regardless of what and where these strings are located. But if the quality of the translations of JS files does not suit you and you have encountered the same problem as me, then continue reading the next chapter about translating JS strings using the i18next react module.

Translations in JS files, via i18next framework

Unlike translating JS files via Django, setting up React for this is much more difficult. But finding all the necessary strings is guaranteed. Let's install the necessary packages first:
npm install i18next-http-backend i18next-browser-languagedetector react-i18next i18next-parser i18next --save
Let's analyze each package that we installed:
  1. i18next - a functional core for translations
  2. react-i18next - a link between React and i18next
  3. i18next-browser-languagedetector - allows you to determine the current user language in the browser and change it
  4. i18next-http-backend - is responsible for delivering translations to the end client
  5. i18next-parser - collects all the lines for translations
Now all this will need to be configured. Let's start with the parser and create a file called i18next-parser.config.js in the Frontend directory, this is where you have the node_modules directory. Then paste the following content.
module.exports = {
contextSeparator: '_',
createOldCatalogs: false,
defaultNamespace: 'translation',
defaultValue: '',
indentation: 2,
keepRemoved: false,
keySeparator: false,
lexers: {
hbs: ['HandlebarsLexer'],
handlebars: ['HandlebarsLexer'],
htm: ['HTMLLexer'],
html: ['HTMLLexer'],
mjs: ['JavascriptLexer'],
js: ['JsxLexer'], // if you're writing jsx inside .js files, change this to JsxLexer
ts: ['JavascriptLexer'],
jsx: ['JsxLexer'],
tsx: ['JsxLexer'],
default: ['JavascriptLexer'],
},
lineEnding: 'auto',
locales: ['en', 'ru', 'be', 'de', 'es', 'fr'],
namespaceSeparator: false,
output: './static/locales/$LOCALE/$NAMESPACE.json',
pluralSeparator: false,
input: undefined,
sort: false,
verbose: false,
failOnWarnings: false,
failOnUpdate: false,
customValueTemplate: null,
resetDefaultValueLocale: null,
i18nextOptions: null,
yamlOptions: null,
}
Compared to the original, I changed it a lot.
  1. First, I changed the lexer, {lexers:} for JS files.
  2. Second, I added locales, {locales:}, for English, Russian, Belarusian, German, Spanish and French.
  3. Third, I changed the path to save the results, {output}. I put all the translation files in the static directory. The question is, why? When I will place this site on the server, all static files should be served by this server. Accordingly, all these files will have to be moved to another location. And Django already has a built-in command for copying all static files to this location - collectstatic. That's right, including our localizations.
  4. Fourth, I set all the values ​​for the separators to false. I did this because I do not use their translation functions, as demonstrated in their tutorial. I wrap strings and sentences in the appropriate function for translation instead of inventing some special keys for each case.
The next thing we need to do is to create a file, i18n.js next to index.js. And insert the following content.
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

import Backend from 'i18next-http-backend';
const HttpApi = new Backend(null, {
loadPath: '/static/locales/{{lng}}/{{ns}}.json',
});

import LanguageDetector from 'i18next-browser-languagedetector';
const languageDetector = new LanguageDetector(null, {
order: ['path', 'cookie', 'localStorage', 'sessionStorage', 'navigator', 'htmlTag', 'subdomain'],
});

i18n
.use(HttpApi)
.use(languageDetector)
.use(initReactI18next)
.init({
debug: true,
fallbackLng: 'en',
});

export default i18n;
In this file we configure the i18n module. We define how we will deliver static files to the end user (Backend module) and how we will detect the user's language (LanguageDetector module). In the second case, it is important to specify that 'path' is first, because this is how we specify the language change event.
Now, import this component:
import i18n from './i18n';
import Header from './components/Header';
import Footer from './components/Footer';
import AppSettings from './components/AppSettings';
import AppUtils from './components/AppUtils';
import AppActions from './components/AppActions';
import AppQueries from './components/AppQueries';
import Message from './components/Msg';
import Hint from './components/Tutorial';
import Auth from './components/Auth';
import EmailVerify from './components/EmailVerify';
import PasswordReset from './components/PasswordReset';
import Contacts from './components/Contacts';
At the very top, because we will need to use it in other modules
Now, using the EmailVerify.js component as an example, I will show how to mark the lines that need to be translated. First of all, we import the translation function:
import { useTranslation } from "react-i18next";
Next, in the component function, we use the hook:
export default function EmailVerify(){
  const { t } = useTranslation();

...
And now, to wrap the necessary string for translation:
<Typography>{t('To finish email verification click the button below')}</Typography>
<Button onClick={()=>{onConfirmEmail(key, containerRef, t)}} variant='text'>{t('proceed')}</Button>
As you may have noticed, I am sending the t function further as an argument to the onConfirmEmail handler. I did this because translations are needed not only in react components but also in regular handlers and functions. I couldn't find another way to globally define a translation function, so in each function that needs translation, I define an additional parameter - t.
Full example:
import * as React from 'react';
import axios from "axios";
import { createRoot } from 'react-dom/client';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button'
import {URLs, getCSRFToken} from './Auth'
import { Wait, StopWait, Msg } from './Waiter';
import { useTranslation } from "react-i18next";



function onConfirmEmail(key, container, t){
var form_data = new FormData()
form_data.append('key', key)
axios.post(URLs.VERIFY_EMAIL, form_data, {
headers: {
"accept": "application/json",
"X-CSRFToken": getCSRFToken(),
'Content-Type': "application/json",
}
}).then((data) => {
createRoot(container.current).render(
<Box className="flex flex-col gap-6 items-center justify-between">
<Typography>{t('You successfully verified your email')}</Typography>
</Box>
)
}).catch((err) => {
if (err.response.status === 401){
createRoot(container.current).render(
<Box className="flex flex-col gap-6 items-center justify-between">
<Typography>{t('You successfully verified your email')}</Typography>
</Box>
)
}
var errors = err.response.data.errors
const lines = []
errors.forEach((error) =>{
lines.push(<Typography>{error.message}</Typography>)
})
createRoot(container.current).render(
<Box className="flex flex-col gap-6 items-center justify-between">
<Typography>{t('The email verification token was invalid')}</Typography>
</Box>
)
});
}

export default function EmailVerify(){
const { t } = useTranslation();
const key = document.querySelector('#email-verify-block').dataset.key
const containerRef = React.useRef(null)
return(
<Box ref={containerRef} className="w-full h-full content-center">
<Box className="flex flex-col gap-6 items-center justify-between">
<Typography>{t('To finish email verification click the button below')}</Typography>
<Button onClick={()=>{onConfirmEmail(key, containerRef, t)}} variant='text'>{t('proceed')}</Button>
</Box>
</Box>
)
}

const email_verify_container = document.getElementById('email-verify-block')
if (email_verify_container){
const email_verify_root = createRoot(email_verify_container);
email_verify_root.render(<EmailVerify></EmailVerify>);
}
Now mark all your lines that need translation. Of course, I won't dump everything here, but you will find the already translated version of the site in this archive.
Let's collect all the lines I highlighted in translation files. This is why we installed the i18n-parser module. And to make it easy for us to continue developing our site, we will write a separate script for performing translations in package.json.
{
"name": "frontend",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"dev": "webpack --mode development --watch",
"tail": "tailwindcss -i ./src/index.css -o ./static/css/zero.css --watch",
"prod": "webpack --mode production",
"trans": "i18next 'src/**/*.js'"
},
...
}
You only need to specify where and what to search using a special pattern. ** - means search in all folders and subfolders. *.js - means search for any files with the .js extension
Let's run the script, and it will find all the lines for translations for us and create all the previously specified localization files.
npm run trans
You will find all translation files in the path static/locales/LOCALE/translation.json. And here is an example of a generated localization file for the Russian language:
{
"You must \"check\" one of these title, url, description": "Вы должны \"выбрать\" один из этих title, url, description",
"Query string cant be empty": "Строка запроса не может быть пустой",
"At least 1 query must be": "Не менее 1 запроса должно быть",
"Saving preset": "Сохранение пресета",
"Preset name": "Имя пресета",
"Successfully saved preset": "Пресет успешно сохранен",
"Cant save a preset": "Немогу сохранить пресет",
"At least 3 character long": "Не менее 3 символов в длину",
"Save": "Сохранить",
"Successfully parsed a data": "Успешно спарсил данные",
"Cant parse data": "Не могу спарсить данные"
}
Next, let's look at the console output and what the page looks like when translated into German.
As you can see from the console, we made an XMLHttpRequest (or XHR) request twice to get German and English localization. Why twice and not once? That's because we specified in i18n.js, fallbackLng: 'en' . That is, if we fail to get German localization, we will use the default one.
You can also see how the LanguageDetector module worked and noticed the change in the user's language. That's it, the JS code translations are complete.

Translations in django models

Unlike string translations, translating django model fields involves creating additional migrations for the translatable model. How does it work? It's pretty simple: if a field needs to be translated into another language, a copy of the field is created, but with the locale prefix it needs to be translated into. And then create and apply the migration.
When and under what circumstances might this be useful? This is actually a very simple question, to which I will answer this way. You have an article, and you want to write it in 2 languages. WELL, and you need two different titles for one article. This is not possible using such built-in django commands as makemessages and compilemessages.
Okay, I lied a little. It is possible, but managing and especially laying out such "translatable pages" is just terribly difficult and inconvenient. My first website about history was built like this. In order for django to be able to detect article templates, you had to create soft links on the deployed system... in short, it was a real pain in the a**.
Well, let's set up model translations. First, install the modeltranslation package:
pip install modeltranslation
Connect the installed application in settings.py. It is best to connect it first in the list.
INSTALLED_APPS = [
# model translation
'modeltranslation',
#
In order for this module to know for which languages ​​it needs to create translated models, you must have the languages ​​specified in LANGUAGES (see the beginning of the article). Next, you will need to create a file named translation.py in the directory of the application whose models you want to translate.
Here is an example of the contents of this file:
from modeltranslation.translator import translator, TranslationOptions
from .models import Post


class PostTranslationOptions(TranslationOptions):
fields = ('title', 'description', 'h1')


translator.register(Post, PostTranslationOptions)
In this file, we specify for which model we want to make translations and which fields we want to translate. At the end we simply register the specified translated model.
All that remains is to make a database migration and apply these migrations.
python manage.py makemigrations
python manage.py migrate
We could finish here, but let me show you the modified admin.py file to display the translated model in the django admin panel.
from django.contrib import admin
from .models import Post

class PostAdmin(admin.ModelAdmin):
prepopulated_fields = {"slug": ("title_en",)}
exclude = ('title', 'description', 'h1')
list_display = (
'slug',
'title_ru',
'title_en',
'description_ru',
'description_en',
'h1_ru',
'h1_en'
)
list_display_links = (
'slug',
)
search_fields = ('title_ru', 'title_en',)

admin.site.register(Post, PostAdmin)
If we want to support 2 languages, for example, English and Russian, then each translation field will have three options, one original plus two more with prefixes *_en and *_ru. The main thing to remember is not to use the original field but its options with prefixes. That's why I exclude these fields from the admin panel.

Epilogue

In this article, we have discussed all possible cases where you will need to make a translation. Translations in templates or python code are not difficult. Although setting up and creating translations for JS code is also easy, you just need to take into account that it works well with vanilla JS. With everything else, difficulties and inaccuracies may arise.
About model translations. If I were you, I would try to avoid this. But Dim, how can we write posts in several languages? After all, many duplicates of empty pages will be created. For example, an article at /ru/articles/art1 will have a variant /en/articles/art1. And if I wrote an article in Russian at the first address, then there should also be an article at the second one. After all, I will not be able to make another article with an identical URL. And, in fact, it is not necessary, I tell you. We write the same article, but at a new address, for example, /en/articles/art1_but_in_english/, and from the page /en/articles/art1, we set up a 301 redirect to it. Here's an example.
Well, in conclusion, I want to add that translation is not an easy and long matter. Even now I'm sitting and waiting for a miracle to happen, maybe the article will translate itself. In any case, here is the updated version of the site (archive), with all the translations and settings. See you in the next part.
\( ̄︶ ̄*\))


Comments

(0)

captcha
Send
It's empty now. Be the first (o゚v゚)ノ

Other