Туда Сюда   Это тринадцатая часть серии Мега-Учебник Flask, в которой я расскажу вам, как реализовать поддержку нескольких языков для вашего приложения. В рамках этой работы вы также узнаете о создании собственных расширений CLI для flask. Для справки ниже приведен список статей этой серии. Примечание 1: Если вы ищете старые версии данного курса, это здесь. Примечание 2: Если вдруг Вы захотели бы выступить в поддержку моей(Мигеля) работы, или просто не имеете терпения дожидаться статьи неделю, я (Мигель Гринберг)предлагаю полную версию данного руководства(на английском языке) в виде электронной книги или видео. Для получения более подробной информации посетите learn.miguelgrinberg.com. Эта глава посвящена интернационализации и локализации, сокращенно I18n и L10n. Чтобы сделать приложение доступным для людей, не владеющих английским языком, будет реализован процесс перевода, который, с помощью сервисов-переводчиков языка, позволит мне предложить пользователям язык-приложения на выбор. Ссылки GitHub для этой главы: Browse, Zip, Diff. Как вы, наверное, уже догадались, существует расширение Flask, которое упрощает работу с переводами. Расширение называется Flask-Babel и устанавливается с помощью pip: Flask-Babel инициализируется, как и большинство других расширений Flask: В качестве примера, я расскажу вам, как перевести приложение на испанский язык, поскольку я, случается, говорю на этом языке. Я мог бы также работать с переводчиками, которые знают другие языки и поддерживают их. Чтобы отслеживать список поддерживаемых языков, следует добавить переменную конфигурации: Я использую двухбуквенные коды языков для этого приложения, но если вам нужно быть более конкретным, можно добавить код страны. Например, вы можете использовать en-US, en-GB и en-CA для поддержки английского с разными диалектами США, Великобритания или Канада. Экземпляр Babel предоставляет декоратор localeselector. Декорированная функция вызывается для каждого запроса, чтобы выбрать перевод языка для использования: Здесь я использую атрибут объекта Flask request, называемый accept_languages. Этот объект обеспечивает интерфейс высокого уровня для работы с заголовком Accept-Language, отправляемым клиентами с запросом. Этот заголовок указывает язык клиента и языковые предпочтения в виде средневзвешенного списка. Содержимое этого заголовка можно настроить на странице настроек браузера, при этом по умолчанию обычно импортируются из языковых настроек в операционной системе компьютера. Большинство людей даже не знают, что такая настройка существует, но это полезно, поскольку пользователи могут предоставить список предпочтительных языков, каждый из которых имеет вес. Если вам интересно, вот пример сложного заголовка Accept-Languages: Видим, что Датский (da) является предпочтительным языком (значение веса по умолчанию 1,0), а затем Британский английский (en-gb) с весом 0,8, и в качестве последнего варианта Общий Английский (en) с весом 0,7. Чтобы выбрать лучший язык, вам нужно сравнить список языков, запрашиваемых клиентом, с языками, которые поддерживает приложение, и, используя предоставленные клиентом веса, найти лучший язык. Возможно вам кажется эта логика слишком сложной, но все это инкапсулируется в метод best_match(), который принимает список языков, предлагаемых приложением в качестве аргумента и возвращает лучший выбор. Рано обрадовались. Теперь о грустном. Обычный рабочий процесс при создании приложения на нескольких языках заключается в разметке всех текстов, требующие перевода в исходном коде. После того, как тексты будут помечены, Flask-Babel будет сканировать все файлы и извлекать эти тексты в отдельный файл перевода, используя инструмент gettext. К сожалению, это утомительная задача, которая должна быть выполнена для перевода. Я собираюсь показать вам несколько примеров этой маркировки, но получить полный набор изменений вы можете из пакета для этой главы или репозитория GitHub. Способ, которым тексты помечены для перевода, заключается в обертывании их в вызов функции, которая вызывается как соглашение _(), просто подчеркивание. Простейшими случаями являются те, где литеральные строки появляются в исходном коде. Ниже приведен пример оператора flash()`: Идея заключается в том, что функция _() переносит текст на базовый язык (в данном случае английский). Она будет использовать лучший по ее мнению язык, выбранный функцией get_locale, декорированной функцией localeselector, чтобы найти правильный перевод для данного клиента. Затем функция _() вернет переведенный текст, который в этом случае станет аргументом для flash(). К сожалению, не все случаи так просты. Рассмотрим этот другой вызов flash() из приложения: Этот текст имеет динамический компонент, который вставлен в середине статического текста. Функция _() имеет синтаксис, поддерживающий этот тип текстов, но основанный на старом синтаксисе подстановки строк: Есть еще более трудный случай. Некоторые строковые литералы назначаются вне запроса, как правило, когда приложение запускается, поэтому в то время, когда эти тексты оцениваются, нет способа узнать, какой язык использовать. Примером этого являются метки, связанные с полями формы. Единственное решение для обработки этих текстов — найти способ отложить оценку строки до ее использования, которая будет находиться под фактическим запросом. Flask-Babel предоставляет версию (lazy evaluation) отложенного вычисления _(), которая называется lazy_gettext(): Здесь я импортирую альтернативную функцию перевода и переименовываю ее в _l (), так что она была схожа по названию с оригинальной _(). Эта новая функция переносит текст в специальный объект, содержащий метод перевода, который состоится позже, в момент использования строки. Расширение Flask-Login высвечивает сообщение при каждой переадресации пользователя на страницу входа. Это сообщение написано на английском языке и формируется в умолчаниях самого расширения. Чтобы убедиться, что это сообщение также переведено, я собираюсь переопределить сообщение по умолчанию и предоставить другой вариант декорированный функцией _l() для отложенного вызова: В предыдущем разделе вы видели, как разметить переводимые тексты в исходном коде модулей Python, но это только часть процесса, так как файлы шаблонов также содержат текст. Функция _() также доступна в шаблонах, поэтому процесс сильно похож. Например, рассмотрим этот фрагмент HTML из 404.html: Версия с поддержкой перевода: Обратите внимание, что здесь, помимо обертывания текста с помощью _(), необходимо добавить {{...}}, чтобы заставить _() вычислять вместо того, чтобы считаться литералом в шаблоне. Для более сложных фраз, содержащих динамические компоненты, можно использовать аргументы: В файле _post.html есть особенно сложный случай, который заставил меня разбираться: Проблема здесь заключается в том, что я хотел, чтобы имя пользователя было ссылкой, указывающей на страницу профиля пользователя, а не только именем, так что мне пришлось создать промежуточную переменную под названием user_link с помощью set и endset директивы шаблонов, а затем передать это как аргумент функции перевода. Как я уже упоминал выше, вы можете скачать версию приложения со всеми переводимыми текстами в исходном коде Python и шаблонах. После того, как у вас есть приложение со всеми _() и_l() на своих местах, вы можете использовать команду pybabel, чтобы извлечь их в файл a.pot, что означает portable object template. Это текстовый файл, содержащий все тексты, которые были помечены как нуждающиеся в переводе. Цель этого файла состоит в том, чтобы служить шаблоном для создания файлов перевода на любой другой язык. Для процесса извлечения требуется небольшой файл конфигурации, который сообщает pybabel, какие файлы следует сканировать для переводимых текстов. Ниже вы можете увидеть babel.cfg, который я создал для этого приложения: Первые две строки определяют имена файлов шаблонов Python и Jinja2 соответственно. Третья строка определяет два расширения, предоставляемые движком шаблонов Jinja2, которые помогают Flask-Babel правильно анализировать файлы шаблонов. Чтобы извлечь все тексты в .pot файл, вы можете использовать следующую команду: Команда pybabel extract считывает файл конфигурации, указанный в параметре -F, а затем сканирует все файлы py и html в каталогах, соответствующих настроенным источникам, начиная с каталога, указанного в команде (текущий каталог или . в этом случае.) По умолчанию, pybabel будем искать _() как текстовый маркер, но я также использовал lazy вариант, который я импортировал как _l(), так что мне нужно сказать об этом инструменту поиска опцией -k _l. Параметр -o указывает имя выходного файла. Должен отметить, что messages.pot не является файлом, который должен быть включен в проект. Это файл, который можно легко регенерировать в любое время, просто выполнив команду выше снова. Таким образом, нет необходимости передавать этот файл в систему управления версиями. Следующим шагом в процессе является создание перевода для каждого языка, который будет поддерживаться в дополнение к базовому, который в данном случае английский. Я сказал, что собираюсь начать с добавления испанского языка (код языка es), так что команда, которая делает это: Команда pybabel init принимает файл messages.pot в качестве входных данных и создает новый каталог для определенного языка, указанного в параметре -l в каталог, указанный в параметре -d. Я буду сохранять все переводы в директории app/translations, потому что там Flask-Babel будет искать файлы перевода по умолчанию. Команда создаст подкаталог es внутри этого каталога для данных на испанском. В частности, там появится новый файл с названием app/translations/es/LC_MESSAGES/messages.po. То есть там, где переводы должны быть сделаны. Если вы хотите поддержать другие языки, то повторите вышеуказанную команду с каждым из кодов языка. Таким образом, что бы каждый язык получил свой собственный репозитарий с файлом messages.po. Этот messages.po-файл, созданный в каждом языковом репозитории, использует формат, который является стандартом де-факто для языковых переводов, Формат, используемый утилитой gettext. Вот несколько строк начала испанского messages.po: Если пропустить заголовок, то видно, что ниже приведен список строк, которые были извлечены из вызовов _() и _l(). Для каждого текста вы получаете ссылку на расположение текста в приложении. Затем строка msgid содержит текст на базовом языке, а следующая строка msgstr содержит пустую строку. Эти пустые строки должны быть отредактированы, чтобы иметь текст на целевом языке. Есть много приложений, которые работают с переводом .po-файлов. Если вы чувствуете себя комфортно при редактировании текстового файла, то этого достаточно, но если вы работаете с большим проектом, то может быть рекомендовано работать со специализированным редактором. Наиболее популярным приложением для перевода является poedit с открытым исходным кодом, который доступен для всех основных операционных систем. Если вы знакомы с VIM, то po.vim плагин дает некоторые ключевые отображения, которые делают работу с этими файлами проще. Ниже вы можете увидеть часть испанской версии messages.po после того, как я добавил перевод: Пакет загрузки для этой главы также содержит этот файл, так что вам не придется беспокоиться об этой части приложения. Файл messages.po -это своего рода файл-источник для переводов. Если вы хотите начать использовать эти переведенные тексты, то файл должен быть скомпилирован в формат, который эффективен для использования приложением во время выполнения. Чтобы собрать все переводы для приложения, вы можете использовать команду компиляции pybabel compile следующим образом: Эта операция добавляет файл messages.mo рядом с messages.po в каждом языковом репозитории. Файл .mo — это файл, который Flask-Babel будет использовать для загрузки переводов в приложение. После создания messages.mo для испанского или любых других языков, добавленных в проект, эти языки готовы к использованию в приложении. Если вы хотите увидеть, как выглядит приложение на испанском языке, Вы можете изменить конфигурацию языка в веб-браузере, чтобы испанский язык был предпочтительным языком. Для Chrome это расширенная часть в настройках:  Если вы предпочитаете не изменять настройки браузера, другой альтернативой является принудительное использование языка, заставляя функцию localeselector всегда возвращать один и тот же. Для испанского это выглядит так: Запуск приложения в браузере, настроенном на испанский язык, или в случае принудительного присвоения значения es функции localeselector, заставит все тексты появляться на испанском языке в приложении. Одна из распространенных ситуаций при работе с переводами заключается в том, что вы можете начать использовать файл перевода, даже если он неполный. Это совершенно нормально, можно компилировать неполные файлы messages.po. В этом случае будут использоваться po-файлы и любые доступные переводы, а отсутствующие будут использовать базовый язык. Затем можно продолжить работу над переводами и выполнить компиляцию для обновления messages.mo. Другой распространенный случай возникает, если вы пропустили некоторые тексты при добавлении _() обертки. В этом случае вы увидите, что те тексты, которые вы пропустили, останутся на английском языке, потому что Flask-Babel ничего о них не знает. В этом случае необходимо добавить _() или _l() обертки при обнаружении текстов, которые не имеют их, а затем выполнить процедуру обновления, которая включает в себя два шага: Команда extract идентична той, которую я описывал ранее, но теперь она будет генерировать новую версию messages.pot со всеми предыдущими текстами плюс что-нибудь новое, которое вы недавно обернули с помощью _() или _l(). Вызов обновления принимает новый файл messages.pot и объединяет его во все файлы messages.po, связанные с проектом. Это будет интеллектуальное слияние, в котором любые существующие тексты будут оставлены в покое, в то время как будут затронуты только записи, которые были добавлены или удалены в messages.pot. После обновления messages.po вы можете продолжить и перевести все новые тесты, а затем скомпилировать сообщения еще раз, чтобы сделать их доступными для приложения. Теперь у меня есть полный испанский перевод для всех текстов в коде Python и шаблонах. Но если вы запустите приложение на испанском языке и будете хорошим наблюдателем, то вы заметите, что есть еще несколько мест, которые остались на английском языке. Я имею в виду временные метки, созданные Flask-Moment и moment.js, которые, очевидно, не были включены в перевод, потому что ни один из текстов, созданных этими пакетами, не является частью исходного кода или шаблона приложения. moment.js поддерживает локализацию и интернационализацию, поэтому все, что мне нужно сделать, это настроить правильный язык. Flask-Babel возвращает выбранный язык и локаль для такого случая с помощью функции get_locale(), поэтому я собираюсь добавить локаль в объект g, чтобы получить доступ к нему из базового шаблона: Функция get_locale() из Flask-Babel возвращает объект, но я просто хочу иметь код языка, который может быть получен путем преобразования объекта в строку. Теперь, когда у меня есть g.locale, я могу получить к нему доступ из базового шаблона, чтобы настроить moment.js с правильным языком: И теперь все даты и время должны появляться на том же языке, что и текст. Ниже вы можете увидеть, как приложение выглядит на испанском языке:  На этом этапе все тексты, кроме тех, которые были предоставлены пользователем в сообщениях блога или описаниях профиля, должны быть переведены на другие языки. Вы, вероятно, согласитесь со мной, что команды pybabel слегка длинны и их трудно запомнить. Я собираюсь использовать эту возможность, чтобы показать вам, как вы можете создавать пользовательские команды, интегрированные с командой flask. До сих пор вы видели использование flask run, flask shell, и несколько flask db суб-команды в Flask-Migrate. На самом деле легко добавить специфичные для приложения команды в flask. Итак, теперь я собираюсь создать несколько простых команд, которые запускают команды pybabel со всеми аргументами, которые специфичны для этого приложения. Команды, которые я собираюсь добавить: babel export не будет командой, потому что генерация файла messages.pot всегда является предварительным условием для выполнения команд init или update. Поэтому реализация этих команд будет генерировать файл шаблона перевода как временный файл. Flask полагается на Click для всех своих операций с командной строкой. Команды, такие как translate, которые являются корнем для нескольких подкоманд, создаются с помощью декоратора app.cli.group(). Я собираюсь поместить эти команды в новый модуль под названием app/cli.py: Имя команды происходит от имени декорированной функции, а справочное сообщение поступает из docstring. Поскольку это родительская команда, которая существует только для обеспечения базы для подкоманд, самой функции ничего не нужно делать. Update -обновление и compile -компиляцию легко реализовать, поскольку они не принимают никаких аргументов:  Обратите внимание, что декоратор из этих функций является производным от родительской функции translate. Это может показаться запутанным, так как translate() — это функция, но это стандартный способ, которым Click создает группы команд. Так же, как и в функции translate(), docstrings -строки документации для этих функций используются в качестве сообщения справки в выводе --help. Возможно вы заметили, что во всех командах, которые я запускаю есть проверка возвращаемого значения на ноль. Это означает, что команда выполнена и не вернула никакой ошибки. Если в команде ошибка, то я поднимаю RuntimeError, что приводит к остановке скрипта. Функция update() объединяет шаги извлечения и обновления в одной команде, и если все прошло успешно, она удаляет файл messages.pot после завершения обновления, так как этот файл может быть легко регенерирован при необходимости еще раз. Команда init принимает новый код языка в качестве аргумента. Вот реализация: Эта команда использует декоратор @click.argument для определения кода языка. Click передает значение, указанное в команде функции обработчика в качестве аргумента, а затем я включаю аргумент в команду init. Последним шагом для включения этих команд является их импорт, чтобы команды регистрировались. Я решил сделать это в файле microblog.py в каталоге верхнего уровня: Здесь единственное, что мне нужно сделать, это импортировать новый модуль cli.py, нет никакой необходимости делать что-либо с ним, так как импорт вызывает декораторы команды для запуска и регистрации команды. В этот момент, запуск flask --help передаст команду translate в качестве опции. И flask translate --help отобразит вывод трех суб-команд, которые я определил: Так что теперь, рабочий процесс гораздо проще и нет необходимости помнить длинные и сложные команды. Чтобы добавить новый язык, используйте: Обновить все языки после внесения изменений в маркеры _() и _l() : И компилировать все языки после обновления файлов перевода:  Туда Сюда