Как добавить переводы для django сайта (python, js, шаблоны и модели) SearchResultParser ч. 5

Часы
06.02.2025
Часы
17.03.2025
Часы
17 минут
Глазик
118
Сердечки
0
Соединённые точки
0
Соединённые точки
0
Соединённые точки
0

Вступление

Ты не поверишь, но я уже давно хотел написать данную статью. А знаешь почему? Потому что это первое что я сделал для своего сайта и первое в чём я разобрался. В этой статье мы будем переводить мой сайт парсинга результатов поиска. И я попытаюсь охватить все возможные проблемы и ситуации которые могут приключиться с тобой, когда ты тоже захочешь локализовать свой сайт.

Как это вообще работает

При локализации надо понимать, как именно ты хочешь показать другую версию языка. И хорошей идеей будет отобразить это непосредственно в URL. Есть варианты:
  1. Создать отдельный сайт с соответствующим доменом. По типу example.de. Это слишком дорого и не практично.
  2. Добавить поддомен. По типу ru.example.com
  3. Добавить директорию. По типу example.com/en/
  4. Или использовать URL параметр example.com?loc=en
У каждого из этих вариантов есть свои плюсы и минусы. И я не из тех, кто может говорить об этом. Всё довольно подробно описано здесь, в блоге гугла. Я опробовал только вариант с директориями, ибо ... ну, у меня один сервер и ограниченный бюджет. Поэтому в этой статье вы найдёте именно этот способ отображения переводов.
В любом случае, перевод сайта на django можно разделить на 4 условные группы. Это перевод строк в пайтон, перевод django-моделей, перевод строк в шаблонах и перевод строк в javascript коде.
Переводы строк для сайта, можно разделить на следующие этапы:
  1. Помечаем(выделяем) необходимые строки для перевода.
  2. Собираем все помеченные(выделенные) строки.
  3. Пишем переводы для них
  4. Компилируем специальный *.mo файл
Данные этапы не применимы при переводе и локализации django-моделей. У них своя специфика.

Базовая настройка

Но перед тем как мы начнём выделять и переводить, нужно ещё немного поднастроить наш сайт. И в первую очередь подключим соответствующий мидлвари. Его основная роль это редиректы на соответствующие локализованные страницы и вставка тех строк перевода, которые нужны для текущей локали сайта.
В файле settings.py:
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",
]
Ещё нужно записать код языка по умолчанию, он же текущий и подготовить список поддерживаемых локализаций(переводов) сайта. Так же добавь глобальную переменную USE_I18N. Последний пункт не обязателен, он включён по умолчанию, но я просто люблю чтобы всё было явно настроено. Так же в файле settings.py, где-нибудь в конце добавь:
LANGUAGE_CODE = 'en'
USE_I18N = True
LANGUAGES = [
('en', 'English'),
('ru', 'Russian'),
('de', 'German'),
('es', 'Spanish'),
('fr', 'French'),
('be', 'Belorussian')
]
Так как, этот сайт, по факту, обычное веб-приложение, то есть контент там публиковаться не будет, я могу позволить себе написать через гугл переводчик все необходимые переводы.
Последним штрихом будет, указание от каких путей мы хотим локализации. В данном случае, не имеет смысла локализовывать все api или admin пути, ну и уж тем более allauth пути. Это то, что останется не тронутым. То есть, будем локализовывать наш фронтенд. Это страницы about, contacts, главная и страницы подтверждения почты и сброса пароля.
В файле Website/urls.py добавь следующие строчки:
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')),
)
Кстати, функцию i18n_patterns можно использовать только в главном urls.py. Такие дела.
Предварительная настройка завершена. Теперь можно проверить как она работает. Зайди на мой сайт(Когда я размещу данный сайт на хостинге здесь появится ссылка), а если ты делаешь это параллельно мне, то открыв браузер по пути localhost:8000, тебя должно перенаправить на localhost:8000/en/.
То, на какую версию локализации тебя перенаправит зависит от настроек твоего браузера. У меня вот стоит английская версия. Если у тебя браузер на языке, который не поддерживает сайт, например украинская мова, то перенаправление будет на ту локализацию, которая указана в LANGUAGE_CODE.
Ты можешь зайти на любую языковую версию своего сайта, если ты конечно её добавил в LANGUAGES. Конечно, пока не на что смотреть, но потерпи до переводов. Всё будет \( ̄︶ ̄*\))

Выделяем строчки для перевода

Нам нужно указать что, собственно говоря, нужно перевести и для каждой части сайта, будь то шаблон, или python код, или javascript код, оно будет разным.

Переводы в python коде

Для того чтобы пометить строчки для перевода, необходимо импортировать следующие функции: gettext и gettext_lazy.
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as __
Дальше, нужно просто обернуть нужную тебе строчку в gettext или gettext_lazy. Сразу замечу, не вокруг каждой строки возможно обернуть функцию gettext. Например, переменной или вычисляемым значениям. То есть:
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))
В данном примере, django сможет обнаружить только первую строку "Usual string". Все остальные он проигнорирует.
Я так же сделал импорт такой функции как gettext_lazy. В чём его особенность и когда его применять? В общем и целом единственное отличие их друг от друга состоит в том, что второй берёт строку из *.po файла, только тогда, когда эта строка используется на сервере. Так по крайней мере написано на официальном сайте django.
Эту функцию стоит применять, при необходимости перевода django моделей и его аттрибутов, описаний для админки. Но в 90% случаев, gettext вам хватит. Плюс, как я заметил раньше, чтобы сделать модели интернациональными, потребуется нечто большее чем встроенные утилиты django.
На текущий момент разработки сайта, SearchResultParsre, на стороне бэкенда нет ни одной строчки, которую необходимо было бы перевести. А если у вашего проекта есть такие строчки, то просто оберните их в gettext и идём дальше.

Переводы в шаблонах

Для перевода текста в шаблонах тебе нужно знать только три тега:
{% 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 %}
В начале шаблона тебе нужно будет загрузить специальный модуль, i18n. Каждый раз, когда тебе будет нужно перевести некий текст в шаблоне, загружай модуль. Даже если этот шаблон наследуется от другого шаблона, который уже загрузил i18n.
После того как модуль i18n был загружен. У тебя есть два варианта. Первый это использовать однострочный перевод . Второй использовать многострочный перевод . Разница между ними такова, что между тегами и всё попадёт под перевод, в том числе и другие теги. Будь в курсе.
Теперь выделим все необходимые строчки для перевода в app.html, base.html, about.html и contacts.html. Покажу на примере как будет выглядеть уже обработанный шаблон; файл Frontend/templates/Frontend/about.html:
{% 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 %}

Переводы в JS файлах, используя Django

Настройка переводов javascript кода, наверное, самое сложное и муторное. Хотя на первый взгляд, вроде и туториал есть и не требуется ничего экстра ординарного. Но об этом позже. Давай я сначала покажу как настроить переводы JS через django.
Первое, что нужно сделать это подключить соответствующие пути в Website/urls.py. Причём замечу, если ты пользуешься функцией i18n_patterns, то и подключать JavaScriptCatalog класс нужно там.
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')),
)
Класс JavaScriptCatalog, будет генерировать для нас мини-js библиотеку с каталогом строчек перевода. Да, это поубавит несколько скорость загрузки страницы поэтому в будущем надо будет не забыть кешировать это. Теперь подключи её в base.html шаблоне и мы готовы переводить.
<script src="{% url 'javascript-catalog' %}"></script>
Настройка бэкенда для перевода готова. Теперь нужно пометить строки, которые мы хотим перевести. Итак, нам потребуется только функция gettext(). Давай покажу на примере одной из моих функций, 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);
})

}
Так же в JSX нужно будет оборачивать функцию gettext в такие вот скобочки {}. Пример из функции fetchProfile:
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>
)
}
И по сути всё, мы готовы и можно собирать строки и компилировать их в *.mo файлы. Давай посмотрим, что получилось. Вот страница оригинала, на английском:
А вот переведённая страница на русский. Найдёшь что-нибудь странное?
Ага, не всё переведено. Даже больше, django не смог найти мои строчки для перевода. По этому я и не смог записать перевод. Почему так? Скорее всего из-за Реакта, он ведь собирается в один пакет и используется именно он, а не написанные мною компоненты.
Я перерыл интернет для решения данной проблемы и нашёл парня у которого была похожая проблема. На медиуме он сделал соответствующий пост о том как заставить команду makemessages находить строчки. Если вкратце, ему пришлось лезть в исходный код Django и добавлять параметр --language=python при указанном домене djangojs. По какой-то, не известной мне причине, xgettext находит больше строчек для перевода при указанном языковом флаге python. ¯\(°_o)/¯
В любом случае, данный способ мне не подходит, слишком не надёжный. Да и ломать Django ради того, чтобы перевести пару строчек ... Нее, лучше я реализую перевод JS файлов через React. Об этом через одну главу. А пока ...

Собираем, Переводим и Компилируем переводы

После настроек для переводов шаблонов, python и javascrip кода (используя django), нужно собрать все отмеченные строки в файлы *.po. Предварительно создав директорию locale в каждом приложении в котором есть строки для перевода. После создания нужно запустить следующую команду
python .\manage.py makemessages -l ru
Если ты ещё переводил строчки в JS, то необходимо запустить ещё одну команду. В этой команде мы используем флаг -d со значением djangojs. Это обязательный флаг если ты хочешь чтобы это сработало.
python .\manage.py makemessages -d djangojs -l ru -i node_modules/*
Пожалуйста, ещё используй флаг -i или --ignore. Смотри, я использую Node.js и ReactJS. Все они находятся в директории node_modules и не только они на самом деле. Если ты не поставишь данный флаг django со всей ответственностью проверит каждый грёбаный файл в твоём проекте, а их будет десятки тысяч. И это займёт время, так что я тебя предупредил.
В результате этих двух команд мы получим файлы django.po - от первой команды и djangojs.po - в качестве результата второй команды. Там будут вот такие строки:
#: .\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"
" "
Просто хочу сделать пару замечаний, когда ты начнёшь делать переводы. Ничего не трогай внутри msgid, это первое. И второе, при переводе многострочных элементов (это те которые начинаются с "\n") после msgid, msgstr так же должен начинаться с "\n".
В любом случае, если и будут ошибки при компиляции, то они очень информативны и у тебя не будет проблем их решить самостоятельно.
Последним этапом переводов является их компиляция в *.mo файлы. Чтобы скомпилировать переводы, вне зависимости от того переводы это шаблонов, python или js кода, используй следующую команду:
python .\manage.py compilemessages
Поздравляю! Мы закончили переводить наш сайт и теперь он может поддерживать множество других языков, вне зависимости, какие и где эти строчки находятся. Но если, качество переводов JS файлов тебя не устраивает и ты столкнулся с той же проблемой, что и я, то продолжи читать следующую главу про переводы JS строк через i18next реакт модуль.

Переводы в JS файлах, через i18next фреймворк

В отличии перевода JS файлов через Django, настроить реакт для этого, гораздо сложнее. Но зато нахождение всех необходимых строчек гарантированно. Установим необходимы пакеты для начала:
npm install i18next-http-backend i18next-browser-languagedetector react-i18next i18next-parser i18next --save
Разберём каждый пакет, который установили:
  1. i18next - функциональное ядро для переводов
  2. react-i18next - связующее звено между React и i18next
  3. i18next-browser-languagedetector - позволяет определять текущий язык пользователя в браузере и его изменение
  4. i18next-http-backend - занимается тем, что доставляет переводы до конечного клиента
  5. i18next-parser - собирает все строчки для переводов
Теперь всё это нужно будет настроить. Начнём с парсера, создай файл i18next-parser.config.js в Frontend директории, это там где у тебя node_modules директория. После вставь следующий контент.
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,
}
Если сравнивать с оригиналом, я многое что поменял.
  1. Во-первых, поменял лексер, lexers: для JS файлов.
  2. Во-вторых добавил локали, locales:, для Английского, Русского, Белорусского, Немецкого, Испанского и Французского.
  3. В третьих, я изменил путь сохранения результатов, output. Я помещаю все файлы переводов в директорию static. Спрашивается, почему? Когда я размещу на сервере этот сайт, все статические файлы должны обслуживаться этим сервером. Соответственно, все эти файлы должны будут перемещены в другую локацию. И у джанго уже есть встроенная команда для копирования всех статических файлов в эту локацию - collectstatic. Правильно, в том числе и наши локализации.
  4. В четвёртых, я проставил все значения для сепараторов на false. Сделал я это потому, что не использую их функции переводов, как это демонстрируется в их туториале. Я оборачиваю строчки и предложения в соответствующую функцию для перевода, вместо того, чтобы придумывать некие особенные ключи для каждого случая.
Следующее, что нам нужно сделать, это создать файл i18n.js рядом с index.js. И вставить следующий контент.
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;
В этом файле мы настраиваем модуль i18n. Определяем как мы будем доставлять до конечного пользователя статические файлы (Backend модуль) и как мы будем определять язык пользователя (LanguageDetector модуль). Во-втором случае важно указать, чтобы 'path' было первым, ведь именно так, мы указываем изменение языка.
Делаем импорт данного компонента:
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';
В самом верху, ибо нам нужно будет его использовать в других модулях
Теперь на примере компонента EmailVerify.js я покажу как пометить строчки, которые необходимо перевести. В первую очередь делаем импорт функции перевода:
import { useTranslation } from "react-i18next";
Дальше в функции-компонента, используем хук:
export default function EmailVerify(){
  const { t } = useTranslation();

...
И теперь, чтобы обернуть необходимую строку для перевода:
<Typography>{t('To finish email verification click the button below')}</Typography>
<Button onClick={()=>{onConfirmEmail(key, containerRef, t)}} variant='text'>{t('proceed')}</Button>
Как ты мог заметить, я отправляю функцию t дальше, в качестве аргумента обработчика onConfirmEmail. Сделал я это потому, что переводы необходимы не только в реакт компонентах, но и в обычных обработчиках и функциях. Я не смог найти другой способ глобально определить функцию перевода, поэтому в каждой функции, которая нуждается в переводе я определяю дополнительный параметр - обработчики переводов t.
Полный пример:
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>);
}
А теперь пометь все свои строчки, которым требуется перевод. Я конечно же не буду вываливать всё сюда, но уже переведённую версию сайта ты найдёшь в этом архиве.
Соберём все выделенные мною строчки в файлы переводов. Для этого мы и устанавливали i18n-parser модуль. И чтобы и дальше нам было просто разрабатывать наш сайт, напишем отдельный скрипт для выполннения переводов в 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'"
},
...
}
Необходимо указать только, где и что искать используя специальный паттерн. ** - значит ищи во всех папках и подпапках. *.js - значит ищи любые файлы с расширением .js
Запустим скрипт, и он найдёт нам и за нас все строчки для переводов и создаст все, ранее указанные, файлы локализации.
npm run trans
Все файлы переводов ты найдёшь по пути static/locales/LOCALE/translation.json. И вот пример, сгенерированного файла локализации для русского языка:
{
"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": "Не могу спарсить данные"
}
Дальше посмотрим на вывод в консоль и то как выглядит страница при переводе на немецкий язык.
Как видно из консоли мы сделали XMLHttpRequest (или XHR) запрос два раза, чтобы получить немецкую и английскую локализацию. Почему две, а не одну? Всё потому, что мы указали в i18n.js, fallbackLng: 'en' . То есть если не удастся получить немецкую локализацию, мы будем использовать локаль по умолчанию.
Ещё видно, как модуль LanguageDetector сработал и заметил изменение языка пользователя. Всё, переводы JS кода завершены.

Переводы в django-моделях

В отличие от переводов строчек, перевод полей django моделей подразумевает создание дополнительных миграций для переводимой модели. Как это работает? Всё довольно просто, если поле необходимо перевести на другой язык, то создаётся копия этого поля, но с приставкой локали, на которую необходимо его перевести. Ну а в конце создать и применить миграцию.
Когда и при каких обстоятельствах это может пригодиться? Это на самом деле очень простой вопрос, на который я отвечу так. У тебя есть статья и ты хочешь написать её на 2-х языках. НУ и тебе нужны два разных заголовка для одной статьи. Это не возможно сделать используя такие встроенные команды в django как makemessages и compilemessages.
Ну ладно, я немного приврал. Это возможно, но управлять и тем более верстать такие "переводимые странички" просто жуть как сложно и неудобно. Мой первый сайт про историю был так построен. Чтобы django мог обнаруживать шаблоны статей, приходилось создавать soft-ссылки на развёрнутой системе ... короче, это та ещё боль в ж**е.
Ну давай настроим переводы моделей. Для начала установи пакет modeltranslation:
pip install modeltranslation
Подключи установленное приложение в settings.py. Его лучше всего подключать первым в списке.
INSTALLED_APPS = [
# model translation
'modeltranslation',
#
Чтобы данный модуль знал для каких языков требуется создать переведённые модели, у тебя должны быть указаны языки в LANGUAGES (Смотри в начале статьи). Дальше нужно будет создать файл translation.py в директории приложения, модели которого ты хочешь перевести.
Вот пример содержания этого файла:
from modeltranslation.translator import translator, TranslationOptions
from .models import Post


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


translator.register(Post, PostTranslationOptions)
В этом файле мы указываем для какой модели хотим сделать переводы, и какие поля мы хотим перевести. В конце просто регистрируем указанную переведённую модель.
Осталось только сделать миграцию базы данных и и применить эти миграции.
python manage.py makemigrations
python manage.py migrate
На этом можно было бы и закончить, но давай покажу ещё изменённый файл admin.py для отображения переведённой модели в django-админке.
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)
Если мы хотим поддерживать 2 языка, например английский и русский, то каждое поле для перевода будет иметь три варианта, один изначальный плюс ещё два с приставками *_en и *_ru. Главное, что запомнить, не использовать изначальное поле, а его варианты с приставками.Вот поэтому я и исключаю данные поля из админки.

Эпилог

В этой статье мы с тобой разобрали, все возможные случаи, где нужно будет сделать перевод. Переводы в шаблонах или python коде не представляют сложность. Хотя настройка и создание переводов для JS кода, тоже легко, только нужно учитывать, что он хорошо работает с ванильным JS. Со всем остальным могут возникнуть трудности и не точности.
Про переводы моделей. На твоём месте я бы старался избегать этого. Но Дим, как допустим писать посты на нескольких языках? Ведь будет создаваться много дублей пустых страниц. Например, статья по адресу /ru/articles/art1 будет иметь вариант /en/articles/art1. И если я написал статью на русском по первому адресу, то и на втором тоже должна быть статья. Я ведь не смогу сделать ещё одну статью с идентичным URL. А, собственно говоря, и не нужно, скажу я тебе. Пишем, ту же самую статью, но по новому адресу, например /en/articles/art1_but_in_english/, а со страницы /en/articles/art1 настраиваем 301 редирект на неё. Вот как пример.
Ну и в заключении хочу добавить, что перевод дело не простое и долгое. Вот даже сейчас я сижу и жду чуда, авось статья сама себя переведёт. В любом случае, вот обновлённая версия сайта(архив), со всеми переводами и настройками. Увидимся в следующей части.
\( ̄︶ ̄*\))


Комментарии

(0)

captcha
Отправить
Сейчас тут пусто. Буть первым (o゚v゚)ノ

Другое