[Перевод] Мега-Учебник Flask Глава 13: I18n и L10n (издание 2024)
Это тринадцатая часть серии мега-учебника Flask, в которой я собираюсь рассказать вам, как расширить Microblog для поддержки нескольких языков. В рамках этой работы вы также узнаете о создании собственных расширений CLI для команды flask.
Оглавление
Темами этой главы являются интернационализация и локализация, обычно сокращаемые как I18n и L10n. Чтобы сделать мое приложение удобным для людей, которые не говорят по-английски, я собираюсь реализовать рабочий процесс перевода, который с помощью команды переводчиков позволит мне предлагать пользователям приложение с выбором языка.
Ссылки на GitHub для этой главы: Browse, Zip, Diff.
Введение в Flask-Babel
Как вы, наверное, догадываетесь, существует расширение Flask, которое очень упрощает работу с переводами. Расширение называется Flask-Babel и устанавливается с помощью pip:
(venv) $ pip install flask-babel
В рамках этой главы я собираюсь показать вам, как перевести приложение на испанский, поскольку я говорю на этом языке. Я также мог бы работать с переводчиками, свободно владеющими другими языками. Чтобы отслеживать список поддерживаемых языков, я собираюсь добавить переменную конфигурации:
config.py: Список поддерживаемых языков.
class Config:
# ...
LANGUAGES = ['en', 'es']
Я использую двухбуквенные коды языков для этого приложения, но если вам нужно уточнить, можно добавить и код страны. Например, вы могли бы использовать en-US
, en-GB
и en-CA
для поддержки американского, британского и канадского английского как разных языков.
Экземпляр Babel
инициализируется аргументом locale_selector
, в который должна быть передана функция, которая будет вызываться для каждого запроса. Функция может просмотреть запрос пользователя и выбрать наилучший языковой перевод для использования в этом запросе. Вот инициализация расширения Flask-Babel:
app/__init__.py: Инициализируем Flask-Babel.
from flask import request
# ...
from flask_babel import Babel
def get_locale():
return request.accept_languages.best_match(app.config['LANGUAGES'])
app = Flask(__name__)
# ...
babel = Babel(app, locale_selector=get_locale)
# ...
Здесь я использую атрибут request
объекта Flask под названием accept_languages
. Этот объект предоставляет высокоуровневый интерфейс для работы с заголовком Accept-Language, который клиенты отправляют с запросом. Этот заголовок определяет настройки языка и локали клиента в виде списка весов. Содержимое этого заголовка можно настроить на странице настроек браузера, при этом значение по умолчанию обычно импортируется из языковых настроек операционной системы компьютера. Большинство людей даже не знают о существовании такой настройки, но это полезно, поскольку пользователи могут предоставить список предпочитаемых языков, каждый из которых имеет определенный вес. На случай, если вам интересно, вот пример сложного заголовка Accept-Languages
:
Accept-Language: da, en-gb;q=0.8, en;q=0.7
Здесь говорится, что датский (da
) является предпочтительным языком (с весом по умолчанию = 1,0), за ним следует британский английский (en-GB
) с весом 0,8 и в качестве последнего варианта общий английский (en
) с весом 0,7.
Чтобы выбрать лучший язык, вам необходимо сравнить список языков, запрошенных клиентом, с языками, поддерживаемыми приложением, и, используя предоставленные клиентом веса, найти лучший язык. Логика для этого несколько сложна, но все это заключено в методе best_match()
атрибута request.accept_languages
, который принимает список языков, предлагаемых приложением, в качестве аргумента и возвращает наилучший выбор.
Разметка текста для перевода в исходном коде Python
Итак, теперь плохие новости. Обычный рабочий процесс при создании приложения, доступного на нескольких языках, заключается в том, чтобы отметить в исходном коде все тексты, которые нуждаются в переводе. После того, как тексты будут помечены, Flask-Babel отсканирует все файлы и извлечет эти тексты в отдельный файл перевода с помощью инструмента gettext. К сожалению, это утомительная задача, которую необходимо выполнить, чтобы включить переводы.
Я собираюсь показать вам здесь несколько примеров этой разметки, но вы можете получить полный набор изменений по ссылке на репозиторий GitHub для этой главы, прилагаемой выше.
Тексты для перевода помечаются путем их переноса в вызов функции, которая, как принято, называется просто подчеркиванием _()
. Простейшие случаи — это те, когда в исходном коде появляются строки-литералы. Вот пример инструкции flash()
:
from flask_babel import _
# ...
flash(_('Your post is now live!'))
Идея заключается в том, что функция _()
переносит текст на базовый язык (в данном случае на английский). Эта функция будет использовать язык, выбранный функцией get_locale()
, чтобы найти правильный перевод для данного клиента. Затем функция _()
возвращает переведенный текст, который в этом случае станет аргументом для flash()
.
К сожалению, не все случаи настолько просты. Рассмотрим другой вызов flash()
из приложения:
flash(f'User {username} not found.')
Этот текст содержит динамический компонент, который вставляется в середину статического текста. Функция _()
имеет синтаксис, поддерживающий этот тип текстов, но он основан на более старом синтаксисе подстановки строк из Python.:
flash(_('User %(username)s not found.', username=username))
Есть еще более сложный случай для обработки. Некоторые строковые литералы назначаются вне веб-запроса, обычно при запуске приложения, поэтому во время оценки этих текстов невозможно определить, какой язык использовать. Примером этого являются метки, связанные с полями формы. Единственное решение для обработки этих текстов — найти способ отложить вычисление строк до тех пор, пока они не будут использованы, что будет выполняться по фактическому запросу. Flask-Babel предоставляет ленивую ознакомительную версию_()
, которая называется lazy_gettext()
:
from flask_babel import lazy_gettext as _l
class LoginForm(FlaskForm):
username = StringField(_l('Username'), validators=[DataRequired()])
# ...
Здесь я импортирую эту альтернативную функцию перевода и переименовываю ее в _l()
, чтобы она выглядела похожей на оригинал _()
. Эта новая функция заключает текст в специальный объект, который запускает перевод, который будет выполнен позже, когда строка будет использована внутри запроса.
Расширение Flask-Login выдает сообщение каждый раз, когда перенаправляет пользователя на страницу входа в систему. Это сообщение на английском языке и исходит от самого расширения. Чтобы убедиться, что это сообщение также будет переведено, я собираюсь переопределить сообщение по умолчанию и предоставить свою собственную оболочку с функцией _l()
отложенной обработки:
login = LoginManager(app)
login.login_view = 'login'
login.login_message = _l('Please log in to access this page.')
Разметка текста для перевода в шаблонах
В предыдущем разделе вы видели, как отмечать переводимые тексты в исходном коде Python, но это только часть этого процесса, поскольку файлы шаблонов также содержат текст. Функция _()
также доступна в шаблонах, поэтому процесс довольно похож. Для примера рассмотрим этот фрагмент HTML из 404.html:
File Not Found
Версия с поддержкой перевода:
{{ _('File Not Found') }}
Обратите внимание, что здесь в дополнение к переносу текста с помощью функции _()
необходимо добавить конструкцию {{ ... }}
, чтобы заставить функцию _()
вычисляться вместо того, чтобы считаться литералом в шаблоне.
Для более сложных фраз, содержащих динамические компоненты, также можно использовать аргументы:
{{ _('Hi, %(username)s!', username=current_user.username) }}
В шаблоне _post.html есть особенно сложный случай, разобраться в котором мне потребовалось некоторое время:
{% set user_link %}
{{ post.author.username }}
{% endset %}
{{ _('%(username)s said %(when)s',
username=user_link, when=moment(post.timestamp).fromNow()) }}
Проблема здесь в том, что я хотел, чтобы username
был ссылкой, указывающей на страницу профиля пользователя, поэтому мне пришлось создать промежуточную переменную с именем user_link
, используя директивы шаблона set
и endset
, а затем передать ее в качестве аргумента функции перевода.
Как я упоминал выше, вы можете загрузить версию приложения со всеми текстами, переводимыми в исходном коде Python и шаблонах.
Извлечение текста для перевода
После того, как у вас будет приложение со всеми _()
и _l()
на месте, вы можете использовать команду pybabel
, чтобы извлечь их в файл .pot, который расшифровывается как portable object template. Это текстовый файл, включающий все тексты, помеченные как требующие перевода. Назначение этого файла — служить шаблоном для создания файлов перевода для каждого языка.
Для процесса извлечения необходим небольшой файл конфигурации, который сообщает pybabel
, какие файлы следует сканировать на предмет наличия переводимых текстов. Ниже вы можете увидеть babel.cfg, который я создал для этого приложения.:
babel.cfg: файл конфигурации PyBabel.
[python: app/**.py]
[jinja2: app/templates/**.html]
Эти строки определяют шаблоны имен файлов Python и Jinja соответственно. Flask-Babel будет искать любые файлы, соответствующие этим шаблонам, и сканировать их на наличие текстов, обернутых для перевода.
Чтобы извлечь все тексты в файл .pot, вы можете использовать следующую команду:
(venv) $ pybabel extract -F babel.cfg -k _l -o messages.pot .
Команда pybabel extract
считывает файл конфигурации, указанный в опции -F
, затем сканирует весь код и файлы шаблонов в каталогах, соответствующих настроенным источникам, начиная с каталога, указанного в команде (текущий каталог или .
в данном случае). По умолчанию, pybabel
будет искать _()
в качестве текстового маркера, но я также использовал отложенную версию, которую я импортировал как _l()
, поэтому мне нужно сказать инструменту, чтобы он искал их с помощью -k _l
. Параметр -o
указывает имя выходного файла.
Я должен отметить, что файл messages.pot — это не тот файл, который нужно включать в проект. Это файл, который можно легко создать заново, когда это необходимо, просто снова выполнив приведенную выше команду. Таким образом, нет необходимости передавать этот файл в систему управления версиями.
Создание языкового каталога
Следующим шагом в процессе является создание перевода для каждого языка, который будет поддерживаться в дополнение к базовому, которым в данном случае является английский. Я сказал, что собираюсь начать с добавления испанского (код языка es
), так что вот команда, которая делает это:
(venv) $ pybabel init -i messages.pot -d app/translations -l es
creating catalog app/translations/es/LC_MESSAGES/messages.po based on messages.pot
Команда pybabel init
принимает файл messages.pot в качестве входных данных и записывает новый языковой каталог в каталог, указанный в опции -d
для языка, указанного в опции -l
. Я собираюсь установить все переводы в каталог app/translate, потому что по умолчанию Flask-Babel ожидает, что именно там будут находиться файлы переводов. Команда создаст подкаталог es внутри этого каталога для файлов данных на испанском языке. В частности, появится новый файл с именем app/translations/es/LC_MESSAGES/messages.po, именно там необходимо выполнить переводы.
Если вы хотите поддерживать другие языки, просто повторите приведенную выше команду с каждым из нужных вам языковых кодов, чтобы для каждого языка было создано собственное хранилище с файлом messages.po .
Этот файл messages.po
, создаваемый в репозитории каждого языка, использует формат, который является стандартным для языковых переводов, формат, используемый утилитой gettext. Вот несколько строк из начала испанского текста messages.po:
# Spanish translations for PROJECT.
# Copyright (C) 2021 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR , 2021.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-06-29 23:23-0700\n"
"PO-Revision-Date: 2021-06-29 23:25-0700\n"
"Last-Translator: FULL NAME \n"
"Language: es\n"
"Language-Team: es \n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.5.1\n"
#: app/email.py:21
msgid "[Microblog] Reset Your Password"
msgstr ""
#: app/forms.py:12 app/forms.py:19 app/forms.py:50
msgid "Username"
msgstr ""
#: app/forms.py:13 app/forms.py:21 app/forms.py:43
msgid "Password"
msgstr ""
Если вы пропустите заголовок, то увидите, что ниже приведен список строк, которые были извлечены из вызовов _()
и _l()
. Для каждого текста вы получаете ссылку на расположение текста в вашем приложении. Затем строка msgid
содержит текст на базовом языке, а следующая строка msgstr
содержит пустую строку. Эти пустые строки необходимо отредактировать, чтобы получить версию текста на целевом языке.
Существует множество приложений для перевода, которые работают с файлами .po
. Если вы чувствуете себя комфортно при редактировании текстового файла, то этого достаточно, но если вы работаете с большим проектом, может быть рекомендовано поработать со специализированным редактором переводов. Самым популярным приложением для перевода является программа с открытым исходным кодом poedit, которая доступна для всех основных операционных систем. Если вы знакомы с vim, то воспользуйтесь плагином po.vim, который предоставляет некоторые сопоставления ключей, которые упрощают работу с этими файлами.
Ниже вы можете увидеть часть испанского языка в messages.po после того, как я добавил переводы:
#: app/email.py:21
msgid "[Microblog] Reset Your Password"
msgstr "[Microblog] Nueva Contraseña"
#: app/forms.py:12 app/forms.py:19 app/forms.py:50
msgid "Username"
msgstr "Nombre de usuario"
#: app/forms.py:13 app/forms.py:21 app/forms.py:43
msgid "Password"
msgstr "Contraseña"
Пакет для загрузки для этой главы также содержит этот файл со всеми имеющимися переводами, так что вам не придется беспокоиться об этой части для этого приложения.
Файл messages.po является своего рода исходным файлом для переводов. Если вы хотите начать использовать эти переведенные тексты, этот файл необходимо скомпилировать в формат, который эффективно будет использоваться приложением во время выполнения. Чтобы скомпилировать все переводы для приложения, вы можете использовать команду pybabel compile
следующим образом:
(venv) $ pybabel compile -d app/translations
compiling catalog app/translations/es/LC_MESSAGES/messages.po to
app/translations/es/LC_MESSAGES/messages.mo
Эта операция добавляет файл messages.mo рядом с messages.po в репозитории каждого языка. Файл .mo — это файл, который Flask-Babel будет использовать для загрузки переводов в приложение.
После создания файла messages.mo для испанского или любых других языков, добавленных вами в проект, эти языки готовы к использованию в приложении. Если вы хотите посмотреть, как приложение выглядит на испанском языке, вы можете изменить языковую конфигурацию в своем веб-браузере, чтобы испанский был предпочтительным языком. Для Chrome это находится на странице расширенных настроек:
Если вы предпочитаете не изменять настройки своего браузера, другой альтернативой является принудительное использование языка, заставив функцию get_locale()
всегда возвращать язык, который вы хотите использовать. Для испанского вы сделали бы это следующим образом.:
app/__init__.py: Выбор лучшего языка.
def get_locale():
# return request.accept_languages.best_match(app.config['LANGUAGES'])
return 'es'
Функция get_locale()
возвращает es
, теперь все надписи в приложении будут отображаться на Испанском.
Обновление переводов
Одна из распространенных ситуаций при работе с переводами заключается в том, что вы можете захотеть начать использовать файл перевода, даже если он неполный. Это совершенно нормально, вы можете скомпилировать неполный файл messages.po, в таком случае будет использоваться файл и любые доступные переводы, в то время как для всех отсутствующих будет использоваться базовый язык. Затем вы можете продолжить работу над переводами и снова скомпилировать для обновления файла messages.mo по мере продвижения.
Другая распространенная ситуация возникает, если вы пропустили какой-то текст при добавлении функции _()
. В этом случае вы увидите, что те тексты, которые вы пропустили, останутся на английском, потому что Flask-Babel ничего о них не знает. В этой ситуации вам захочется добавить _()
или _l()
, когда вы обнаруживаете тексты, в которых их нет, а затем выполнить процедуру обновления, которая включает в себя два шага:
(venv) $ pybabel extract -F babel.cfg -k _l -o messages.pot .
(venv) $ pybabel update -i messages.pot -d app/translations
Команда extract
идентична той, которую я опубликовал ранее, но теперь она сгенерирует новую версию messages.pot со всеми предыдущими текстами плюс все новое, что вы недавно добавили в _()
или _l()
. Вызов update
принимает новый файл messages.pot
и объединяет его со всеми файлами messages.po , связанными с проектом. Это будет интеллектуальное объединение, при котором все существующие тексты будут оставлены в покое, а записи, которые были добавлены или удалены в messages.pot будут затронуты.
После обновления messages.po вы можете продолжить перевод любых новых текстов, затем скомпилировать messages.mo еще раз, чтобы сделать их доступными для приложения.
Перевод даты и времени
Теперь у меня есть полный перевод на испанский для всех текстов в исходном коде и шаблонах страниц приложения, но если вы запустите приложение на испанском и будете хорошим наблюдателем, вы заметите, что все еще есть несколько вещей, которые отображаются на английском. Я имею в виду временные метки, сгенерированные Flask-Moment и moment.js, которые, очевидно, не были включены в процесс перевода, поскольку ни один из текстов, сгенерированных этими пакетами, не является частью исходного кода или шаблонов приложения.
Библиотека moment.js поддерживает локализацию и интернационализацию, поэтому все, что мне нужно сделать, это настроить соответствующий язык. Flask-Babel возвращает выбранный язык и локаль для данного запроса через функцию get_locale()
, итак, что я собираюсь сделать, это добавить поле locale к объекту g
в обработчике before_request
, чтобы затем я мог получить к нему доступ из базового шаблона:
app/routes.py: Сохранение выбранного языка в flask.g.
# ...
from flask import g
from flask_babel import get_locale
# ...
@app.before_request
def before_request():
# ...
g.locale = str(get_locale())
Функция get_locale()
из Flask-Babel возвращает объект locale, но я просто хочу иметь код языка, который можно получить, преобразовав объект в строку. Теперь, когда у меня есть g.locale
Я могу получить к нему доступ из базового шаблона для настройки moment.js с правильным языком:
app/templates/base.html: Настройка локали для moment.js.
...
{{ moment.include_moment() }}
{{ moment.lang(g.locale) }}