В миналото дадохме подробни уроци за инструменти за Gettext, както и за интегриране на Gettext с Python. Ще разширим знанията си, като използваме Babel и ще видим някои практически примери за използването му в python i18n. Също така ще видим как да го интегрираме с шаблони Jinja2 и как да интегрираме Phrase's In-Context Editor в приложение на 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 Common Locale Data Repository е стандартизирано хранилище на локални данни, използвани за форматиране, анализиране и показване на специфична за локала информация. Вместо да превеждате, например, имена на дни или имена на месеци за конкретен език или писменост, можете да използвате преводите, предоставени от локалните данни, включени в 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

Това ще създаде основен пот файл, който ще съдържа следните съобщения:

#: 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 в картината и да интегрираме Phrase in-text editor чрез използване на техника за замяна на gettext callables за средата на 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, преди да стартирате сървъра.

Когато навигирате до страницата, ще видите модал за влизане и след като бъдете удостоверени, ще видите преведените низове да се променят, за да включват бутони за редактиране до тях. Панелът за редактор в контекст също ще се покаже.

$ export FLASK_APP=web.py
$ flask run

Заключение

В тази статия видяхме как да направим локализация на Python с помощта на библиотеката babel на Python. Видяхме също как можем да го интегрираме с шаблони на Jinja и редактора в контекста на Phrase в нашия работен процес. Ако имате други въпроси, не се колебайте да публикувате коментар или да ми пишете. Благодарим ви за четенето и до нови срещи следващия път!

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