В прошлом мы давали подробные руководства по инструментам Gettext, а также по интеграции Gettext с Python. Мы собираемся расширить наши знания с помощью Babel и увидеть несколько практических примеров его использования в python i18n. Мы также увидим, как интегрировать его с шаблонами Jinja2 и как интегрировать Контекстный редактор фраз в приложение Flask, чтобы облегчить процесс перевода, просто просматривая веб-сайт и редактируя тексты. таким образом, делая ваш Python процесс локализации намного проще. Вы также можете найти код, описанный в этом руководстве, на Github.

О Бабеле

Babel предоставляет помощники и инструменты интернационализации (python i18n) и локализации (python l10n), которые работают в двух областях. Первый — это модуль gettext, который использует gettext для обновления, извлечения и компиляции каталогов сообщений и управления файлами PO. Второй — использование CLDR (Common Locale Data Repository) для предоставления методов форматирования валют, дат, чисел и т. д. на основе параметра локали. Оба аспекта направлены на то, чтобы помочь автоматизировать процесс интернационализации приложений Python, а также предоставить удобные методы доступа и использования этих данных.

Babel, по сути, работает как механизм абстракции для более крупной среды извлечения сообщений, поскольку вы можете расширять ее своими собственными экстракторами и стратегиями, не привязанными к конкретной платформе.

Установка и использование Babel

Установить Babel очень просто, используя pip

$ pip install Babel

Если у вас не установлен pip, вы можете получить его с помощью easy_install

$ sudo easy_install pip

Работа с данными локали

(CLDR) Общий репозиторий данных локали Unicode — это стандартизированный репозиторий данных локали, используемый для форматирования, анализа и отображения информации, относящейся к локали. Вместо перевода, например, названий дней или месяцев для определенного языка или сценария вы можете использовать переводы, предоставленные данными локали, включенными в Babel, на основе данных CLDR.

Давайте посмотрим на некоторые примеры…

Создайте имя файла loc.py и добавьте следующий код:

from babel import Locale
 
# Parsing
l = Locale.parse('de-DE', sep='-')
print("Locale name: {0}".format(l.display_name))
 
l = Locale.parse('und_GR', sep='_')
print("Locale name: {0}".format(l.display_name))
 
# Detecting
l = Locale.negotiate(['de_DE', 'en_AU'], ['de_DE', 'de_AT'])
print("Locale negociated: {0}".format(l.display_name))
 
print(Locale('it').english_name)
print(Locale('it').get_display_name('fr_FR'))
print(Locale('it').get_language_name('de_DE'))
print(Locale('de', 'DE').languages['zh'])
 
print(Locale('el', 'GR').scripts['Copt'])
 
# Calendar
locale = Locale('it')
month_names = locale.days['format']['wide'].items()
print(list(month_names))

Мы показываем несколько примеров класса Locale. Он используется для печати, согласования или идентификации языковых тегов. Если вы запустите этот пример, вы увидите следующий вывод:

$ python loc.py
Locale name: Deutsch (Deutschland)
Locale name: Ελληνικά (Ελλάδα)
Locale negociated: Deutsch (Deutschland)
Italian
italien
Italienisch
Chinesisch
Κοπτικό
[(6, 'domenica'), (0, 'lunedì'), (1, 'martedì'), (2, 'mercoledì'), (3, 'giovedì'), (4, 'venerdì'), (5, 'sabato')]

Они полезны, поскольку они предоставляются набором данных (CLDR) и не нуждаются в переводе.

Кроме того, есть несколько функций, которые форматируют дату, время, валюту, единицы измерения и т. д. Давайте посмотрим на несколько примеров:

from babel.dates import format_date, format_datetime, format_time
from babel.numbers import format_number, format_decimal, format_percent, parse_decimal
from babel.units import format_unit
 
from datetime import date, datetime, time
 
# Date, time
 
d = date(2010, 3, 10)
print(format_date(d, format='short', locale='it'))
print(format_date(d, format='full', locale='it'))
 
print(format_date(d, "EEEE, d.M.yyyy", locale='de'))
 
dt = datetime.now()
print(format_datetime(dt, "yyyy.MMMM.dd GGG hh:mm a", locale='en'))
 
# Numbers/ Units
 
print(format_decimal(123.45123, locale='en_US'))
print(format_decimal(123.45123, locale='de'))
 
print(format_unit(12, 'length-meter', locale='en_GB'))
print(format_unit(12, 'length-meter', locale='en_US'))
 
parse_decimal('2.029,98', locale='de')

Результат:

$ python loc.py
10/03/10
mercoledì 10 marzo 2010
Mittwoch, 10.3.2010
2018.August.28 AD 10:24 AM
123.451
123,451
12 metres
12 meters
2029.98

Извлечение сообщения

Babel имеет механизм извлечения, аналогичный gettext. Он работает, просматривая указанные каталоги, и на основе правил конфигурации применяет функции извлечения к этим файлам. Этот способ является более гибким, чем gettext, поскольку вы можете использовать мощь выражений Python для расширения инструмента.

Babel поставляется с несколькими встроенными экстракторами, такими как python, javascript и ignore (который ничего не извлекает), и вы можете создавать свои собственные экстракторы, и есть два разных интерфейса для доступа к этой функции:

  • Интерфейс командной строки
  • Интеграция Distutils/Setuptools

В этом уроке мы будем использовать интерфейс командной строки.

Чтобы использовать его, просто вызовите инструмент pybabel, например, чтобы распечатать все известные локали.

$ pybabel --list-locales:

Чтобы на самом деле использовать инструменты, давайте пройдемся по процессу извлечения сообщений с помощью pybabel :

Создайте файл с именем main.py и добавьте следующий код:

import gettext
_ = gettext.gettext
 
print(_('This is a translatable string.'))

Обратите внимание, что использование gettext удобно только потому, что экстрактор по умолчанию использует gettext за кулисами, но Babel, в общем, не привязан к этому.

Используйте команду paybabel extract для создания начального каталога сообщений:

$ mkdir locale
$ pybabel extract . -o locale/base.pot

Это создаст базовый файл pot, который будет содержать следующие сообщения:

#: main.py:4
msgid "This is a translatable string."
msgstr ""

Теперь вам не нужно редактировать этот файл.

Используя команду init, мы можем создать новый каталог переводов на основе этого файла шаблона POT:

$ pybabel init -l el_GR de_DE en_US -i locale/base.pot -d locale
creating catalog locale/el_GR/LC_MESSAGES/messages.po based on locale/base.pot
 
$ pybabel init -l de_DE en_US -i locale/base.pot -d locale
creating catalog locale/de_DE/LC_MESSAGES/messages.po based on locale/base.pot
 
$ pybabel init -l en_US -i locale/base.pot -d locale
creating catalog locale/en_US/LC_MESSAGES/messages.po based on locale/base.pot

Эти файлы готовы к переводу. Когда переводы будут выполнены, вы можете использовать команду компиляции, чтобы превратить их в MO файлов:

$ pybabel compile -d locale
compiling catalog locale/en_US/LC_MESSAGES/messages.po to locale/en_US/LC_MESSAGES/messages.mo
compiling catalog locale/el_GR/LC_MESSAGES/messages.po to locale/el_GR/LC_MESSAGES/messages.mo
compiling catalog locale/de_DE/LC_MESSAGES/messages.po to locale/de_DE/LC_MESSAGES/

Теперь, если вы внесете изменения в файл base.pot, вы сможете обновить остальные с помощью команды update. Например, добавьте следующую строку в base.pot

msgid "Hello world."
msgstr ""

Интеграция с шаблонами Jinja2

Babel может интегрироваться с шаблонами Jinja, используя расширение jinja2.ext.i18n.

Затем его можно использовать для маркировки и перевода сообщений из шаблонов, а также для интернационализации HTML-страниц.

Чтобы интегрировать оба этих инструмента вместе, вам необходимо предоставить некоторую конфигурацию.

Сначала установите jinja с помощью pip:

$ pip install Jinja2

Вам нужно указать babel анализировать шаблоны jinja при извлечении сообщений, и для этого вам нужно добавить файл конфигурации.

Создайте файл с именем babel-mapping.ini и добавьте следующий текст:

[python: **.py]
[jinja2: **/templates/**.html]
extensions=jinja2.ext.i18n,jinja2.ext.autoescape,jinja2.ext.with_

Итак, теперь, когда вы вызываете pypabel команд, ссылающихся на этот файл, он также будет извлекать сообщения из шаблонов Jinja.

Давайте посмотрим, как мы можем загрузить шаблоны Babel и Jinja вместе:

Создайте шаблон с именемindex.html, который будет использоваться для извлечения наших сообщений:

<title>{% trans %}{{title}}{% endtrans %}</title>
<p>{% trans count=mailservers|length %}
There is {{ count }} {{ name }} server.
{% pluralize %}
There are {{ count }} {{ name }} servers.
{% endtrans %}
</p>

Вызовите следующую команду для извлечения базовых сообщений:

¢ pybabel extract -F babel-mapping.ini -o locale/messages.pot ./

Это создаст следующий каталог:

# Translations template for PROJECT.
# Copyright (C) 2018 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2018-08-29 10:24+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
 
#: templates/index.html:1
#, python-format
msgid "%(title)s"
msgstr ""
 
#: templates/index.html:2
#, python-format
msgid ""
"\n"
" There is %(count)s %(name)s server.\n"
" "
msgid_plural ""
"\n"
" There are %(count)s %(name)s servers.\n"
" "
msgstr[0] ""
msgstr[1] ""# Translations template for PROJECT.
# Copyright (C) 2018 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2018-08-29 10:24+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
 
#: templates/index.html:1
#, python-format
msgid "%(title)s"
msgstr ""
 
#: templates/index.html:2
#, python-format
msgid ""
"\n"
" There is %(count)s %(name)s server.\n"
" "
msgid_plural ""
"\n"
" There are %(count)s %(name)s servers.\n"
" "
msgstr[0] ""
msgstr[1] ""

Теперь инициализируйте итальянские переводы с помощью команды init и укажите переводы:

$ pybabel init -d locale -l it -i locale/messages.pot
creating catalog locale/it/LC_MESSAGES/messages.po based on locale/messages.pot

Запустите команду компиляции, чтобы сгенерировать файлы MO:

$ pybabel compile -d locale -l it
compiling catalog locale/it/LC_MESSAGES/messages.po to locale/it/LC_MESSAGES/messages.mo

Создайте файл с именем app.py, чтобы соединить все вместе:

from jinja2 import Environment, FileSystemLoader, select_autoescape
from babel.support import Translations
 
templateLoader = FileSystemLoader( searchpath="templates" )
 
env = Environment(
  loader=templateLoader,
  extensions=['jinja2.ext.i18n', 'jinja2.ext.autoescape'],
  autoescape=select_autoescape(['html', 'xml'])
)
 
translations = Translations.load('locale', ['it'])
env.install_gettext_translations(translations)
 
template = env.get_template('index.html')
 
print(template.render(mailservers=range(10), name='mail'))

Мы используем компонент Translations для загрузки каталогов сообщений, которые мы скомпилировали ранее. Затем мы загружаем их в среду Jinja с помощью метода install_gettext_translations. Затем мы визуализируем шаблон.

Если вы запустите эту программу, вы увидите следующий вывод:

$ python app.py
<title>Titolo</title>
<p>Esistono 10 mail servers.
</p>

Если мы хотим изменить локаль, нам нужно проделать ту же процедуру. Например:

translations = Translations.load('locale', ['en_US'])
env.install_gettext_translations(translations)
 
template = env.get_template('index.html')
 
print(template.render())

Добавление контекстного редактора фраз с помощью Flask и Babel

Мы также можем ввести Flask в картину и интегрировать Контекстный редактор фраз, используя технику замены вызовов gettext для среды jinja.

Во-первых, нам нужно установить необходимые пакеты:

$ pip install flask, flask-babel

Создайте файл с именем web.py и добавьте следующий код:

from flask import Flask, render_template, request
from flask_babel import Babel
from phrase import Phrase, gettext, ngettext
 
class Config(object):
    LANGUAGES = {
        'it': 'Italian',
        'de_DE': 'Deutsch'
    },
    BABEL_DEFAULT_LOCALE= 'it'
    PHRASEAPP_ENABLED = True
    PHRASEAPP_PREFIX = '{{__'
    PHRASEAPP_SUFFIX = '__}}'
 
app = Flask(__name__)
app.config.from_object(Config)
 
babel = Babel(app)
phrase = Phrase(app)
 
@babel.localeselector
def get_locale():
    return request.accept_languages.best_match(app.config['LANGUAGES'][0].keys())
 
@app.route('/')
def index():
    return render_template('index.html', locale=get_locale() or babel.default_locale)

Сначала мы настраиваем некоторую конфигурацию, чтобы определить список поддерживаемых языков и специальные ключи PHRASEAPP_*. Что нужно контекстному редактору, так это обернуть переводимые строки определенными тегами '{{__' и'__}}' .

Теперь давайте посмотрим содержимое файла phrase.py '{{__ и __}}'.

from __future__ import print_function
try:
    from flask_babel import gettext as gettext_original, ngettext as ngettext_original
    from flask import current_app
except ImportError:
    print("Flask-Babel is required.")
 
class Phrase(object):
 
    def __init__(self, app=None):
        self.app = app
        app.jinja_env.install_gettext_callables(
            gettext,
            ngettext,
            newstyle=True
        )
 
def phrase_enabled():
    return current_app.config['PHRASEAPP_ENABLED']
 
def phrase_key(msgid):
    return current_app.config['PHRASEAPP_PREFIX'] + 'phrase_' + msgid + current_app.config['PHRASEAPP_SUFFIX']
 
def gettext(msgid):
    if phrase_enabled():
        return phrase_key(msgid)
    else:
        return gettext_original(msgid)
 
def ngettext(msgid1, msgid2, n, **kwargs):
    if phrase_enabled():
        return phrase_key(msgid1)
    else:
        return ngettext_original(msgid1, msgid2, n, **kwargs)

Нам нужно убедиться, что мы передаем исходные параметры исходным функциям gettext на случай, если мы отключим редактор.

Обновите index.htmlfile, чтобы включить скрипт для загрузки редактора:

<!DOCTYPE html>
<html lang="{{locale}}">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>{% trans %}title{% endtrans %}</title>
</head>
<body>
    <p>{% trans %}title{% endtrans %}</p>
    
        window.PHRASEAPP_CONFIG = {
            projectId: "YOUR-PROJECT-ID"
        };
        (function() {
            var phraseapp = document.createElement('script'); phraseapp.type = 'text/javascript'; phraseapp.async = true;
            phraseapp.src = ['https://', 'phraseapp.com/assets/in-context-editor/2.0/app.js?', new Date().getTime()].join('');
            var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(phraseapp, s);
        })();
      
</body>
</html>

Если вы еще этого не сделали, перейдите на https://phrase.com/ и зарегистрируйтесь, чтобы попробовать его бесплатно.

После настройки учетной записи вы можете создать проект и перейти к настройкам проекта, чтобы найти ключ projectId.

Используйте это, чтобы назначить переменную среды PHRASE_APP_TOKEN перед запуском сервера.

Когда вы перейдете на страницу, вы увидите модальное окно входа в систему, и после аутентификации вы увидите, что переведенные строки меняются, и рядом с ними появляются кнопки редактирования. Панель редактора In-Context также будет отображаться.

$ export FLASK_APP=web.py
$ flask run

Вывод

В этой статье мы увидели, как выполнить локализацию Python с помощью библиотеки Babel Python. Мы также увидели, как мы можем интегрировать его с шаблонами Jinja и контекстным редактором фраз в наш рабочий процесс. Если у вас остались другие вопросы, не стесняйтесь оставлять комментарии или напишите мне. Спасибо за прочтение и до новых встреч в следующий раз!

Первоначально опубликовано в The Phrase Blog.