Как перевести Django-сайт на разные языки: плюсы, минусы, подводные камни
Рано или поздно любой сервис задумывается о расширении аудитории. И часто возникает вопрос языков, т.к. единого для всех стран не существует. В целом, это довольно стандартная задача для разработчиков, когда компания начинает работать на международную аудиторию. В случае с Django, который славится универсальностью, есть стандартное решение, но действительно ли оно хорошее, как можно его улучшить и с чем вообще придётся столкнуться во время процесса — обо всём этом расскажу. Меня зовут Камиль, я более трех лет был техническим директором и главным backend-программистом продукта Zonesmart, а с начала 2023 года продолжаю управлять разработкой этого продукта уже в составе Kokoc Group.
Zonesmart — это SaaS сервис для многоканальной онлайн-торговли в режиме одного окна на маркетплейсах. Система позволяет наглядно видеть общую статистику по всем площадкам и оперативно вносить изменения: заводить товары, управлять заказами, остатками на всех маркетплейсах, тем самым повышая их доходность.
Стандартное решениедля перевода, о котором я упомянул, это пакет django.utils.translation
. В основе этого пакета лежит модуль gettext
стандартной библиотеки Python, с документацией которого полезно ознакомиться (или даже с документацией библиотеки GNU gettext, которую “оборачивает” данный модуль).
Перевод веб-сайта – это широкий термин, который описывает различные практики адаптации сайта для пользователей из разных стран и языковых групп. Эта адаптация включает не только перевод текстового контента на язык пользователя, но также использование привычных форматов для дат, валюты, единиц измерения и прочего. Однако в данной статье мы сосредоточимся на переводе текста, поскольку это минимальное требование, которое может быть достаточным для многих продуктов.
Вообще я люблю этот вольный термин “перевод сайта” за его простоту, но есть нюансы. Поэтому расскажу про два официальных термина, которые используются в статьях W3C и в документации Django. Это термины “интернационализация” и “локализация” (которые часто сокращаются до аббревиатур “i18n” и “L10n”), взятые из русской википедии:
Интернационализация — это процесс разработки программных приложений, которые потенциально могут адаптироваться к различным языкам и регионам без инженерных изменений.
Локализация — это процесс адаптации интернационализированного программного обеспечения для определенного региона или языка путем добавления локальных компонентов и переведенного текста.
Грубо говоря, мы занимаемся интернационализацией, когда разрабатываем программный продукт (например, кодовую базу сайта), способный переводить свое текстовое содержимое на различные языки, чтобы, например, у сайта могло быть несколько языковых версий. А локализацией можем назвать процесс создания конкретной языковой версии сайта (посредством добавления переведенного на данный язык текста и т.д.). Но не всё так просто…
Настройка Django для стандартного перевода
В документации Django термин "интернационализация" относится к переводу текстов, в то время как "локализация" означает адаптацию форматов чисел и дат. Это немного расходится с официальными терминами и может вызвать путаницу у разработчика. Поскольку в контексте Django локализация не связана с переводом текста, эта статья не будет затрагивать этот аспект.
Для включения интернационализации в Django (что мы ранее обозначили как стандартный метод перевода), необходимо внести следующие изменения в настройки.
Установите параметр
USE_I18N = True
;Добавьте
middleware django.middleware.locale.LocaleMiddleware
в конфигурациюMIDDLEWARE
;В параметре
LANGUAGE_CODE
укажите язык сайта по умолчанию.
Например, если сайт ориентирован преимущественно на русскоязычную аудиторию и разрабатывается русскоязычными программистами, установите LANGUAGE_CODE = "ru"
. Если планируется переводить сайт с русского только на английский и немецкий, то список поддерживаемых языков может быть зафиксирован в настройке LANGUAGES
следующим образом:
LANGUAGES = [
("ru", "Русский"),
("en", "English"),
("de", "Deutsch"),
]
Базовые функции перевода в Django
Django предоставляет обширный набор инструментов для переводов, но не все из них являются актуальными для современных разработчиков. Например, возможность перевода встроенной административной панели и шаблонов Django можно считать устаревшей для новых проектов. В наше время, с разделением разработки сайтов на фронтенд и бэкенд, взаимодействующих через API, разработка интерфейса, в том числе административной панели, обычно выполняется с применением фронтенд-фреймворков. Для небольших проектов, где использование шаблонов Django допустимо, проблемы с интернационализацией, как правило, не возникают. Поэтому сфокусирую своё внимание на основных возможностях перевода текста, пропуская описание перевода моделей и шаблонов Django.
Основными функциями для перевода являются функции gettext
и gettext_lazy
(как и все функции стандартного способа перевода, они находятся в пакете django.utils.translation
). Функция gettext
получает на вход единственный параметр с текстом, который нужно уметь переводить, и возвращает перевод на нужный язык:
from django.utils.translation import gettext
text = gettext("Hello, World!")
print(text)
В примере выше в консоль может быть выведено “Привет, Мир!”, если в качестве языка перевода на момент выполнения данного кода установлен русский и строка “Привет, Мир!” сохранена в качестве перевода строки "Hello, World!". Функция gettext
может принимать не только строковые литералы, но в том числе и вычисляемые значения и переменные:
from django.utils.translation import gettext
# Вычисляемое значение
print(gettext(",".join(["Hello", "World!"])))
# Переменная
text = "Hello, World!"
print(gettext(text))
К сожалению, перевод f-строк не поддерживается (надеюсь, пока), но зато для перевода строки с параметрами сработает использование метода format
:
from django.utils.translation import gettext
errors_num: int = count_errors() # получение количества ошибок
# Так нельзя, поэтому закомментировали
# text = gettext(f"Количество ошибок: {errors_num}")
# Так можно
text = gettext("Количество ошибок: {errors_num}").format(errors_num=errors_num)
print(text)
Кстати, функция gettext_lazy
делает то же, что gettext
, но перевод является “ленивым”, то есть выполняется только непосредственно в момент использования переводимого текста в строковом контексте:
from django.utils.translation import gettext, gettext_lazy
def f():
# На данном этапе перевод еще не происходит
return gettext_lazy("Hello, World!")
# На данном этапе перевод все еще не происходит
text = f()
# Переменная с переводом используется в текстовом контексте,
# тут только и происходит перевод
print(text)
# Здесь перевод выполняется сразу, хотя текст еще нигде не используется
text = gettext("Hello, World!")
Использование "ленивого" перевода полезно для избежания ошибок в тех случаях, когда строка, подлежащая переводу, находится в участке кода, который выполняется на этапе импорта модулей, и перевод еще не может быть выполнен. Это обычно применяется для перевода моделей и форм Django, что, однако, не так важно для проектов, не включающих перевод административной панели и шаблонов. Для удобства, наиболее часто используемые функции перевода, в основном gettext
или gettext_lazy
, обычно импортируются как _
(нижнее подчеркивание).
from django.utils.translation import gettext as _
print(("One"))
print(("Two"))
print(_("Three"))
Дополнительные функции перевода в Django
В целом, функция gettext
является главной функцией стандартного перевода, а прочие функции устроены аналогично, но как бы расширяют ее функциональность (как вышеупомянутая gettext_lazy
). Стоит также отметить функции pgettext
и ngettext
(в свою очередь тоже имеющие “ленивые” варианты pgettext_lazy
и ngettext_lazy
). С помощью функции pgettext
(pgettext_lazy
) можно задать контекст перевода, когда, например, слово или текст может по-разному переводиться в зависимости от контекста использования:
from django.utils.translation import pgettext
print(pgettext("Мир", "Согласие, отсутствие вражды"))
print(pgettext("Мир", "Совокупность форм материи"))
В примере выше для одного и того же слова “Мир” можно задать два разных перевода и в зависимости от контекста будет использоваться нужный перевод (например, “Peace” либо “World” в случае перевода на английский).
С помощью функции ngettext
(ngettext_lazy
) можно задать правило формирования множественных чисел, если перевод текста с числовым параметром зависит от значения этого параметра:
from django.utils.translation import ngettext, gettext
errors_num: int = count_errors() # получение количества ошибок
if errors_num > 0:
text = ngettext(
"There is {errors_num} error",
"There are {errors_num} errors",
errors_num
).format(errors_num=errors_num)
print(text)
else:
print(gettext("No errors"))
В указанном примере исходным языком является английский, где присутствует только одна форма множественного числа (существительное во множественном числе отличается от существительного в единственном числе наличием окончания "s"). Если язык перевода — английский, то при errors_num == 1
выводится строка "There is 1 error", а при errors_num > 1
выводится соответствующая форма множественного числа ("There are 2 errors" или "There are 3 errors" и так далее). Однако, при переводе на русский язык необходимо учесть, что существует три формы множественного числа.
1, 21, 31… ошибка
2, 3, 4, 22, 23, 24, 32, 33, 34… ошибки
0, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 25, 26, 27, 28, 29, 30, 35, 36… ошибок
В некоторых других языках также присутствуют аналогичные особенности, которые требуется учитывать при переводе. Функция ngettext
предоставляет возможность определить версии перевода для каждой формы множественного числа. Эта тема достаточно сложна и остается слабо освещенной в документации Django. Полноценное освоение работы с функцией ngettext
, вероятно, достигается лишь на практике.
Однако рекомендую упрощать себе задачу и, по возможности, формулировать тексты с числовыми параметрами таким образом, чтобы их перевод не зависел от конкретного значения параметра. Вместо различных вариантов предложения вроде "There are {errors_num} errors", предпочтительнее использовать форму "Number of errors: {errors_num}". Такой подход обеспечивает единый перевод на русский, например, как "Количество ошибок: {errors_num}", независимо от значения переменной errors_num
.
Определение языка перевода Django
Давайте рассмотрим, как Django определяет, какую версию текста использовать в определенном контексте выполнения кода. Здесь выявляется существенный недостаток стандартного метода перевода: для автоматического определения языка перевода необходимо, чтобы код был запущен в результате вызова представления (view). Это обусловлено тем, что Django определяет язык на основе запроса клиента к конечной точке (через объект request
представления). Именно из этих данных можно явно выяснить языковые предпочтения пользователя, с которым связано выполнение программного кода.
Если Django не может определить язык пользователя по данным запроса к конечной точке, или если код выполняется не в ответ на запрос, то используется язык, указанный в настройках проекта как язык по умолчанию (настройка LANGUAGE_CODE
). Об алгоритме определения языка по данным запроса можно прочитать в документации, но если вкратце, то Django последовательно выполняет следующие проверки:
Путь эндпоинта проверяется на соответствие паттерну /[код языка]/[путь, которому соответствует представление], то есть проверяется наличие префикса с кодом языка. Язык из префикса и становится языком перевода. Чтобы это работало, нужна специальная настройка, подробнее о которой можно прочитать в документации. Заключается она всего навсего в том, что список путей в настройках роутинга нужно “обернуть” в функцию
i18n_patterns
из пакетаdjango.conf.urls.i18n
.Пример:
from django.conf.urls.i18n import i18n_patterns from django.urls import include, path from rest_framework_nested import routers from users.views import UserViewset from utils import PingView router = routers.SimpleRouter() urlpatterns = [ # Добавили эндпоинт для пинга, не требующий перевода path("ping", PingView.as_view()), ] # Собрали список эндпоинтов для работы с пользователями router.register("users", UserViewset, "users") users_patterns = router.urls # Добавили поддержку языковых префиксов для тех эндпоинтов, # для которых перевод актуал urlpatterns += i18n_patterns(users_patterns)
В данном случае для вызова
GET /en/users/
определится английский язык, а для вызоваGET /de/users/
определится немецкий язык.Выполняется проверка куки с именем
django_language
(имя можно изменить настройкойLANGUAGE_COOKIE_NAME
). Если значением является код языка, то он становится языком перевода.Выполняется проверка http-хэдера “Accept-Language”. В нем браузер обычно отправляет предпочитаемый язык пользователя.
Когда на одном из этапов удается определить язык, все последующие шаги не выполняются. Отследить, какой именно язык был выбран для перевода, можно по атрибуту LANGUAGE_CODE
объекта request
представления.
Однако, когда код запускается не в результате пользовательского запроса к API, а, к примеру, через воркер очереди задач или webhook от стороннего сервиса, установка языка перевода становится ручной задачей. Здесь на помощь приходят функции активации языка activate
и контекстный менеджер override
. Это мощные инструменты, которые позволяют явно управлять языком перевода в коде, предоставляя дополнительные возможности в сценариях, где автоматическое определение языка не осуществляется.
from django.utils import translation
# Определение установленного на текущий момент языка
current_lang = translation.get_language()
try:
# Установка английского языка
translation.activate(“en”)
# Выполнение перевода текста на английский
print(translation.gettext("Привет, Мир!"))
finally:
# Возврат первоначального языка после выполнения перевода на английский
translation.activate(current_lang)
# То же самое можно проще выполнить с помощью контекстного менеджера
with translation.override("en"):
# Внутри контекстного менеджера установлен английский язык
print(translation.gettext("Привет, Мир!))
# После выхода из контекстного менеджера возвращается первоначальный язык
Неизбежно разработчикам придётся анализировать, какие участки кода могут выполняться без автоматического определения языка, как в данном примере:
from django.http import HttpResponse
from django.utils.translation import override, gettext as _
# Импортируем приложение Celery, чтобы определить асинхронную задачу
from config.celery_app import app
# Импортируем модель пользователя
# Будем считать, что в ней есть поле "name" с именем,
# поле "email" c имейлом и поле "language" с языком пользователя
from my_proj.users.models import User
# Импортируем функцию отправки имейла
from my_proj.utils import send_email
# Функция, отправляющая имейл пользователю
def send_email(user: User):
text = _("Здравствуйте, {user_name}!").format(user_name=user.name)
send_email(email=user.email, text=text)
# Представление, вызывающее отправку имейла пользователю по его запросу
def my_view(request):
send_email(user=request.user)
return HttpResponse(_("Имейл отправлен вам."))
# Задача Celery, вызывающая отправку имейла пользователю
@app.task()
def my_celery_task(user_id: int, **kwargs):
user = User.objects.get(id=user_id)
with override(user.language):
send_email(user=user)
print(_("Имейл отправлен."))
Хотя пример может показаться несколько искусственным, в реальной практике подобные сценарии возникают достаточно часто. В представлении язык определяется автоматически на основе данных из объекта запроса. Однако, в контексте задачи Celery, где используется код для отправки электронной почты, необходимо явно указывать язык. В противном случае, при отправке электронного письма будет использован язык по умолчанию.
Этот пример подчеркивает, что необходимость ручной установки языка в стандартном методе перевода Django может усложнить код, снизить его переиспользуемость и способствовать появлению шаблонного кода.
Хранение и активация переводов
Все строки текста, переданные в функции перевода (такие как gettext
, ngettext
, pgettext
, и их "ленивые" версии), известные также как "translation strings" или "messages" согласно терминологии Django, собираются в специальные технические файлы с расширением .po
, называемые "message files". Для каждого поддерживаемого языка создается отдельный файл, который по умолчанию имеет следующий путь: locale/[код языка]/LC_MESSAGES/django.po
. Важно отметить, что создание и обновление .po-файла не происходит автоматически, и эту операцию необходимо выполнять вручную после каждого изменения текстов для перевода, включая добавление, удаление или редактирование. Пример команды для генерации файла с переводами на английский язык: python manage.py makemessages -l en
(подробнее о команде в документации).
Вообще файл с переводами на конкретный язык может быть не один на весь проект, ведь папку locale
можно создать в каждом приложении, тогда переводы из приложения будут попадать в соответствующую папку. Папка locale
на верхнем уровне проекта тоже создастся, но в нее попадут только те переводы, для которых не нашлось папки уровнем ниже. Например, пусть проект имеет следующую файловую структуру:
/
locale/
test_project/
init.py
locale/
app1/
apps.py
locale/
file1.py
...
app2/
apps.py
file2.py
...
file3.py
...
file4.py
...
В данном примере будет следующее распределение переводов:
В папку
locale
попадут переводы из файлаfile4.py
В папку
test_project/locale
попадут файлfile3.py
и файлы из папкиtest_project/app2
(file2.py
), не имеющей своей папкиlocale
В папку
test_project/app1/locale
попадут переводы из файлов приложенияtest_project/app1
(file1.py
)
Хотя создание локальных папок locale
под конкретные приложения делает файлы с переводами менее крупными и более узкотематичными, с такими файлами по понятным причинам становится сложнее работать, если их становится много. Разбросанные по разным папкам файлы придется собирать, отдавать переводчикам, а затем сохранять обратно. Если же сделать небольшое количество папок locale
, то они все равно рискуют разрастись настолько, что не будет принципиально разницы между ними и одной папкой locale
для всего проекта. Поэтому в рамках данной статьи будем предполагать, что папка locale
для всего проекта только одна. С деталями того, как устроен механизм поиска переводов, можно ознакомиться в документации.
Файл с переводами на английский может выглядеть следующим образом:
Файл состоит из строк с метаданными в начале и множества групп строк с переводами, разделенных между собой пустой строкой. Группу строк, соответствующих переводу одного текста (переданного в функцию перевода), будем называть фрагментом файла.
Например, если на строке 10 файла с путем /test_project/file.py
есть код gettext(”Привет, мир!”)
, то в .po-файле будет следующий фрагмент:
#: test_project/file.py:10
msgid "Привет, мир!"
msgstr ""
В примере выше msgid
— это оригинальный текст, а строка, начинающаяся с msgstr
, соответствует переводу на язык файла (например, на английский, если .po-файл находится в папке en
). Заполнить строки, начинающиеся с msgstr
, переводами текстов — задача разработчиков.
Фрагмент .po-файла, соответствующий тексту, может выглядеть и сложнее:
# Translators [Комментарий для переводчика]
#: [Место в коде, где находится строка]
#: [Другое место в коде, где находится та же строка]
#, fuzzy [Если изменилась исходная строка, но не изменился перевод]
#| msgid [Предыдущая исходная строка]
msgctxt [Контекст]
msgid [Исходная строка]
msgstr [Перевод]
Если над строкой, где есть gettext
, оставить комментарий, начинающийся с # Translators
, то данный комментарий будет добавлен в .po-файл. Так можно оставить касающийся данного текста пояснительный комментарий для переводчика:
# Translators: "мир" нужно перевести как "world", а не как "peace"
print(gettext("Привет, Мир!"))
Если сгенерировать .po-файл и задать для текста перевод (например, перевести “мир” как “world”), а затем изменить оригинальный текст (изменить gettext(”мир”)
на gettext(”Привет, мир!”)
), не изменив перевод (так и оставив перевод “world”), то Django посчитает это ошибкой и добавит в соответствующий тексту фрагмент в .po-файле строку, начинающуюся с #, fuzzy
, и строку с предыдущей версией текста, начинающуюся с | msgid
(| msgid “мир”
).
Одному и тому же тексту, находящемуся в нескольких местах проекта, будет соответствовать единственный фрагмент .po-файла, если перевод происходил без указания контекста (не функцией pgettext
/pgettext_lazy
). В данном фрагменте будет по строчке для каждого пути к тексту. Например, для кода gettext(“мир”)
, находящегося на десятой строке файла test_project/file1.py
и на двадцатой строке файла test_project/file2.py
, фрагмент в .po-файле может выглядеть следующим образом:
#: test_project/file1.py:10
#: test_project/file2.py:20
msgid "Мир"
msgstr ""
Если для перевода текста с помощью функции pgettext
задан контекст, то он сохранится в соответствующем фрагменте .po-файла в строке, начинающейся с msgctxt
. Например, для кода pgettext("Мир", "Наша планета")
во фрагменте будет строка msgctxt “Наша планета”
. Стоит обратить внимание, что одному и тому же тексту с разными контекстами будут соответствовать разные фрагменты в .po-файле. Например, для строки с кодом pgettext("Мир", "Отсутствие войны")
и строки с кодом pgettext("Мир", "Наша планета")
фрагменты в .po-файле могут выглядеть следующим образом:
#: test_project/file1.py:10
msgctxt "Отсутствие войны"
msgid "Мир"
msgstr "Peace"
#: test_project/file2.py:20
msgctxt "Наша планета"
msgid "Мир"
msgstr "World"
Но недостаточно только заполнить .po-файл переводами, его еще нужно скомпилировать в бинарный файл с расширением .mo
, который Django уже будет непосредственно использовать для перевода. Пример команды для компиляции .po-файла с переводами на английский язык: python manage.py compilemessages -l en
. Сгенерированный .mo-файл появится рядом с .po-файлом.
Таким образом, если у нас есть Django-проект “test_project”, который мы стандартным способом переводим с русского на английский и немецкий, то его файловая структура может выглядеть следующим образом:
/
manage.py
test_project/
init.py
...
locale/
en/
LC_MESSAGES/
django.po
django.mo
de/
LC_MESSAGES/
django.po
django.mo
...
Для данного проекта можно сформулировать воркфлоу создания переводов текстов стандартным способом:
Задаем настройки, нужные для стандартного перевода, в файле настроек Django.
Пример:USE_I18N = True LANGUAGE_CODE = "ru" LANGUAGES = [ ("ru", "Русский"), ("en", "English"), ("de", "Deutsch"), ] MIDDLEWARE = [ ... "django.middleware.locale.LocaleMiddleware", ... ]
Все тексты, которые требуется перевести, “оборачиваем” функциями перевода (gettext, ngettext, pgettext и их “ленивые” варианты и другие).
Пример:from django.utils.translation import gettext, pgettext print(gettext("Привет, мир!")) print(pgettext("Мир", "Отсутствие войны")) print(pgettext("Мир", "Наша планета"))
Создаем .po-файл для каждого поддерживаемого языка.
Пример:python manage.py makemessages -l en python manage.py makemessages -l de
Создадутся файлы
locale/en/LC_MESSAGES/django.po
иlocale/de/LC_MESSAGES/django.po
.
Пример содержимого .po-файла:... #: test_project/file.py:3 msgid "Привет, Мир!" msgstr "" #: test_project/file.py:4 msgctxt "Отсутствие войны" msgid "Мир" msgstr "" #: test_project/file.py:5 msgctxt "Наша планета" msgid "Мир" msgstr ""
Задаем переводы для текстов в .po-файле.
Пример содержимого .po-файла для перевода на английский после выполнения данного действия:#: test_project/file.py:3 msgid "Привет, Мир!" msgstr "Hello, World!" #: test_project/file.py:4 msgctxt "Отсутствие войны" msgid "Мир" msgstr "Peace" #: test_project/file.py:5 msgctxt "Наша планета" msgid "Мир" msgstr "World"
Компилируем .po-файлы.
Пример:
python manage.py compilemessages -l en python manage.py compilemessages -l de
Создадутся файлы locale/en/LC_MESSAGES/django.mo
и locale/de/LC_MESSAGES/django.mo
.
Если добавятся новые или удалятся/изменятся существующие тексты, “обернутые” в функции перевода, нужно будет заново последовательно запустить команды генерации .po-файлов, внести правки в .po-файлы и запустить команды компиляции.
Стандартный способ хранения переводов обладает серьезными недостатками:
Файлы с переводами неудобны для работы, особенно людям без программистских навыков, каковыми обычно являются переводчики и контент-менеджеры. Файл нельзя разбить по категориям настолько мелко, насколько может захотеться, все тексты приложения или сайта оказываются перемешанными в одном месте. Файл обладает уникальным форматом, который непросто конвертировать в какой-то более удобный и привычный формат типа таблицы, чтобы затем редактировать его через excel, например.
Если на сайте много текста, то файлы с переводами могут становиться огромными, из-за чего поиск недостающих переводов и ошибок перевода сильно осложняется. Более того, порядок фрагментов файла не фиксируется и может случайным образом меняться после каждого обновления файла, что еще больше мешает в нем ориентироваться.
Django не предлагает какого-либо валидатора для файлов с переводами. Например, если перевод какого-то текста случайно устанавливается с какой-то критичной опечаткой или в неправильном формате, то данный текст просто не будет переводиться и данная проблема никак не даст о себе знать. Чтобы по-настоящему контролировать ситуацию, придется написать множество тестов на проверку вывода правильного перевода текстов либо найти или реализовать самописный валидатор файлов переводов (у которых, напомню, очень непростая структура).
Конечно для стандартного перевода необязательно пользоваться только встроенными инструментами Django в чистом виде, их можно совмещать со сторонними опенсорсными или платными решениями. Есть ряд продуктов, облегчающих процесс перевода и, в частности, предоставляющих более удобный интерфейс для работы с .po-файлами: Rosetta, Weblate, Zanata и другие. Наиболее адаптированным под работу с Django является проект Rosetta, но и у него есть свои недостатки и нетривиальные требования к настройке инфраструктуры для полноценной работы. Рекомендую ознакомиться с полезным комментарием к статье, также посвященной теме перевода на Django, раскрывающим опыт разработчика в интернационализации сайта в целом и в работе с Rosetta в частности.
Личный опыт
В ходе нашей практики, моя команда и я экспериментировали с переводом сайта, варьируя от использования исключительно встроенных инструментов Django до их комбинации с открытыми и собственными решениями. Многочисленные недостатки стандартного метода перевода, подробно рассмотренные в данной статье, привели к тому, что наша команда стала все больше прибегать к собственному подходу к переводу текстов. Однако полного отказа от инструментов Django не произошло. Вот как сейчас преимущественно выглядит наш подход к переводам в проектах:
from django.http import HttpResponse
# Самописная функция перевода
from utils.translation import msg
# В реальном проекте такие словари с переводами хранятся в отдельных файлах,
# разбитых по категориям
TRANSLATIONS = {
"email_text": {
"ru": "Здравствуйте, {user_name}!",
"en": "Hello, {user_name}!",
"context": "Текст имейла",
},
"email_sent": {
"ru": "Имейл отправлен",
"en": "The email has been sent",
"context": "Эндпоинт для отправки имейла пользователю по его запросу",
},
}
# Функция, отправляющая имейл пользователю
def send_email(user: User):
send_email(email=user.email, text=msg("email_text", user_name=user.name))
# Представление, вызывающее отправку имейла пользователю по его запросу
def my_view(request):
send_email(user=request.user)
return HttpResponse(msg("email_sent"))
Наш собственный инструмент похож на стандартный инструмент по форме, но с ним удобнее и безопаснее работать:
Переводы хранятся в более удобном формате — в python-словарях вместо текстовых файлов. Так переводы легче читать и изменять, сложнее совершить ошибку при работе.
Переводы можно распределять по нескольким файлам, чтобы тексты были тематически сгруппированы и файлы были достаточно компактными, чтобы с ними было удобно работать. А при стандартном способе перевода хоть сколько-то крупного сайта приходится работать с огромными текстовыми файлами сложной структуры, в которых переводы хаотично перемешаны.
Наш инструмент требует, чтобы для каждого случая перевода был явно задан либо непосредственно язык, либо пользователь (язык которого мы храним в БД), либо данные запроса к API (тогда определяем язык по тому же алгоритму, что и Django). Из-за этого не возникает ситуации, когда язык перевода не удалось определить из-за технической недоработки, и достаточно легко написать тесты, гарантирующие, что везде в проекте язык перевода будет определяться правильно. При стандартном подходе нерелевантный язык перевода может подбираться по множеству причин и без какого-либо сигнализирования об этом.
Благодаря хранению переводов в удобном формате, тексты для перевода несложно преобразовывать в табличный формат и обратно. Например, мы умеем преобразовывать все тексты в нашем проекте в excel-таблицу, с которой удобно работать переводчикам. Переводчик возвращает разработчикам таблицу уже с переводами текстов и они без проблем сохраняются.
Благодаря формату хранения данных не составляет труда написать тесты, контролирующие, что никакие тексты в проекте не остались без перевода на каждый из поддерживаемых проектом языков.
Ну и в заключение: наш подход к переводам, хоть и не такой гибкий и производительный, как стандартный метод Django, но оправдывает себя благодаря высокому уровню удобства и безопасности, которого мы достигли. Хотя мы не публикуем наши инструменты для использования другими разработчиками в своих проектах, поскольку они не являются универсальными и адаптированы под наши конкретные потребности, архитектура создания и управления переводами, а также рабочий процесс с переводчиками, которых мы придерживаемся, могут быть полезными для многих других проектов.
Помните, что перевод сайта — это большая работа, требующая усилий как программистов, так и всей компании. Прежде чем приступать к интернационализации, важно тщательно оценить необходимость и обоснованность этого труда, а также выбрать наиболее подходящие инструменты, не забывая о том, что стандартный метод Django для переводов имеет свои нюансы.