angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

языковые файлы Flask, Babel и Javascript

В этой заметке описывается способ генерации языковых файлов Javascript de.js, en.js и т.д., а также способ их добавления в ваше мультиязычное приложение Flask .

6 января 2020
post main image
https://unsplash.com/@goran_ivos

Данный веб-сайт Flask является многоязычным. Реализация описана в предыдущих сообщениях. До сих пор все мои переводы были в коде Python и в шаблонах HTML . В нескольких местах мне понадобились некоторые переводы в Javascript и я сделал это, потянув этот Javascript код внутри шаблона HTML . Например, для форм, которые мне понадобились:

	e.target.setCustomValidity('Please fill out this field.');

Я вытянул этот Javascript в шаблон HTML и изменил его на HTML :

	e.target.setCustomValidity("{{ _('Please fill out this field.') }}");

Это было легко и отлично работает.

Язык в файлах Javascript

Я знал, что однажды мне придется изменить это и реализовать мультиязык для файлов Javascript . В этот день камера появилась раньше, чем ожидалось, потому что я хотел реализовать заголовок Content Security Policy . Минимум, что мы должны сделать, это удалить встроенные сценарии и функцию eval() :

Content-Security-Policy: script-src 'self'
Это означает, что я больше не могу иметь в шаблонах встроенный код Javascript , весь код Javascript должен быть перемещен в файлы. Решение выглядит очевидным. Сгенерируйте языковой файл с переводами для каждого языка и включите соответствующий файл. Языковые файлы есть:
de.js
en.js
es.js
fr.js
nl.js
ru.js
Затем в конце базового шаблона мы делаем что-то вроде:
<script src="{{ url_for('static', filename='js/mlmanager.js') }}?v={{ et }}"></script>
<script src="{{ url_for('static', filename='js/locales/'  +  lang_code  +  '.js') }}?v={{ et }}"></script>
<script src="{{ url_for('static', filename='js/base.js') }}?v={{ et }}"></script>
Обратите внимание, что я добавляю метку времени, чтобы избежать кэширования браузера. Здесь mlmanager.js содержит объект, который используется для загрузки и получения языков. Файл locales/<язык>.js - файл с переводами, а файл base.js - файл со всем кодом. Первая версия выглядит так:
// mlmanager.js

var ML = function(params){

	this.key2translations = {};
	this.keys = []
	if(params.hasOwnProperty('key2translations')){
		this.key2translations = params.key2translations;
		this.keys = Object.keys(this.key2translations);
	}

	this.t = function(k){
		if(this.keys.indexOf(k) === -1){
			alert('key = '  +  k  +  ' not found');
			return;
		}
		s = this.key2translations[k];
		return s.replace(/"/g,'\"');
	};
};

При создании нового объекта ml мы передаем также переводы. Метод t используется для получения перевода. Переведенный языковой файл, например, de.js, выглядит так:

//  de.js

var ml = new ML({
	'key2translations': {
        'Content item': "Inhaltselement",
        'Please fill out this field.': "Bitte füllen Sie dieses Feld aus.",
	}, 
});

Наконец, в файле с реальным кодом Javascript , base.js, мы изменяем текст, который должен быть переведен:

	e.target.setCustomValidity('Please fill out this field.');

к:

	e.target.setCustomValidity( ml.t('Please fill out this field.') );

Проблема: как сгенерировать языковые файлы Javascript de.js, en.js и др.

В стандартной документации Babel упоминаются только такие команды, как init, extract, update, compile. Нам нужен способ:

  • извлекать тексты для перевода из файлов javascript
  • автоматически генерируют языковые файлы de.js, en.js и др.

Извлечь тексты для перевода из файлов Javascript

Я решил не сканировать файлы Javascript , а вместо этого создать новый файл HTML (шаблон), jsbase.html, содержащий все тексты для файлов Javascript , например:

var ml = new ML({
	'key2translations': {
		...
        'Content item': "{{ _('Content item') }}",
        'Please fill out this field.': "{{ _('Please fill out this field.') }}",
		...
	}, 
});

Мы поместили этот файл в каталог шаблонов так, чтобы он был просканирован Babel , когда мы выпустим стандартные команды перевода:

pybabel extract -F babel.cfg -k _l -o  messages.pot .

pybabel update -i  messages.pot -d app/translations

# do yourself: translate all texts in the po files either manual or automated

pybabel compile -d app/translations

Теперь у нас есть переведённые тексты для файлов Javascript где-то в файлах messages.po . Вы можете проверить это, например, дампом файла messages.po :

from babel.messages.pofile import read_po
import os

def show_catalog(lc):

    lc_po_file = os.path.join('app_frontend', 'translations', lc, 'LC_MESSAGES', 'messages.po')

    # catalog = read_po(open(lc_po_file, mode='r', encoding='utf-8'))
    # without encoding parameter works if the default encoding of the platform is utf-8
    catalog = read_po(open(lc_po_file, 'r'))
    for message in catalog:
        print('message.id = {}, message.string = {}'.format(message.id, message.string))

show_catalog('de_DE')

При этом печатается список идентификаторов сообщений и строк:

...
message.id = Sub image, message.string = Unterbild
message.id = Sub image text, message.string = Unterbildtext
message.id = Select image, message.string = Bild auswählen
...

Автоматически генерировать языковые файлы Javascript de.js, en.js и др.

Нам нужен способ перевести этот jsbase.html за пределами Flask на наши языки и сгенерировать файлы de.js, en.js и др. Мы могли бы использовать приведенный выше код для получения текстов из файлов Javascript и генерировать языковые файлы de.js, en.js, и т.д. Но это громоздко и склонно к ошибкам.

Затем я отскочил в способ отрисовки шаблона вне Flask, смотрите ссылки ниже. Идея заключается в том, чтобы отрисовать шаблон jsbase.html, позволяющий Babel помещать в него правильные переводы. Тогда всё, что нам нужно сделать, это записать результат рендеринга в языковые файлы de.js, en.js и др. Неужели это так просто? Вот код, который делает это:

from jinja2 import Environment, FileSystemLoader, select_autoescape
from babel.support import Translations

import os
import sys

def generate_translated_js_file(
        app_translations_dir, 
        language_region_code, 
        app_templates_dir, 
        template_file, 
        js_translation_file):

    template_loader = FileSystemLoader(app_templates_dir)

    # setup environment
    env = Environment(
        loader=template_loader,
        extensions=['jinja2.ext.i18n', 'jinja2.ext.autoescape'],
        autoescape=select_autoescape(['html', 'xml'])
    )

    translations = Translations.load(app_translations_dir, language_region_code)
    env.install_gettext_translations(translations)

    template = env.get_template(template_file)
    rendered_template = template.render()

    with open(js_translation_file, 'w') as f:
         f.write(rendered_template)

Эта функция загружает выбранный язык, использует render() для помещения перевода и записывает результат как de.js, en.js и т.д.. Обратите внимание, что в моей установке я использую несколько приложений, app_frontend, app_admin, используя DispatcherMiddleware. Для генерации всех языковых файлов Javascript для всех приложений и языков я вызываю вышеуказанную функцию в другой функции:

def generate_translated_js_files():

    # app translations directory has subdirectories de_DE, en_US, es_ES, ...
    # lang_code is language code used in the  Flask  app
    language_region_code2lang_codes = {
        'de_DE': 'de',
        'en_US': 'en',
        'es_ES': 'es',
        'fr_FR': 'fr',
        'nl_NL': 'nl',
        'ru_RU': 'ru',
    }

    template_file = 'jsbase.html'

    for app_name in ['app_frontend', 'app_admin']:

        # app/translations 
        app_translations_dir = os.path.join(app_name, 'translations')

        # app/templates
        app_templates_dir = os.path.join(app_name, 'templates')

        for language_region_code, lang_code in language_region_code2lang_codes.items():

            if not os.path.isdir( os.path.join(app_translations_dir, language_region_code)):
                print('error: not a directory = {}'.format( os.path.isdir( os.path.join(app_translations_dir, language_region_code) )))
                sys.exit()

            # shared/static/js/locales is the directory where we write  de.js,  en.js, etc.
            js_translation_file = os.path.join('shared', 'static', 'js', 'locales', lang_code  +  '.js')

            # translate
            generate_translated_js_file(
                    app_translations_dir, 
                    language_region_code, 
                    app_templates_dir, 
                    template_file, 
                    js_translation_file)

# do it
generate_translated_js_files()

Обратите внимание, что на данный момент это немного удваивается, так как фронтенд и админ имеют один и тот же статический каталог.

Проблемы

Конечно, есть проблемы. Когда код Javascript был в шаблоне HTML , я добавил код Jinja :

{% if ... %} 
	...
{% else %} 
	...
{% endif %} 

использовать определенную часть кода Javascript . Мы больше не можем этого делать... :-(. Если быть более точным, то в моем случае Javascript вызывает другую страницу с url, которая может существовать или не существовать, и url также зависит от языка. Например, ссылка в строчном коде Javascript шаблона HTML выглядит так:

	{% if 'Privacy policy' in app_template_slug %}
	moreLink: '{{ url_for('pages.page_view', slug=app_template_slug['Privacy policy']['slug']) }}',
	{% else %}
	moreLink: '',
	{% endif %}

То, что я сделал, это полностью переписал согласие на печенье. На данный момент HTML больше не генерируется в Javascript , а в HTML . Ему все еще нужно больше работы.

Резюме

Это первая реализация, но она отлично работает. Магия использует Babel и Jinja API. Возможные улучшения:

Вместо того, чтобы иметь строки в качестве индекса для перевода:

ml.t('Please fill out this field') 

мы, возможно, захотим использовать объекты:

ml.t( t.Please_fill_out_this_field )

И вместо того, чтобы иметь переводы Javascript файла с объектами Javascript , мы можем захотеть использовать JSON файл, содержащий только переводы. В любом случае, следующими шагами будет выборочное добавление дополнительных пользовательских файлов Javascript и больше переводов.

Ссылки / кредиты

Analyse your HTTP response headers
https://securityheaders.com

Babel documentation
https://readthedocs.org/projects/python-babel/downloads/pdf/latest/

Best practice for localization and globalization of strings and labels [closed]
https://stackoverflow.com/questions/14358817/best-practice-for-localization-and-globalization-of-strings-and-labels/14359147

Content Security Policy - An Introduction
https://scotthelme.co.uk/content-security-policy-an-introduction/

Explore All i18n Advantages of Babel for Your Python App
https://phrase.com/blog/posts/i18n-advantages-babel-python/

Give your JavaScript the ability to speak many languages
https://github.com/airbnb/polyglot.js

Оставить комментарий

Комментируйте анонимно или войдите в систему, чтобы прокомментировать.

Комментарии

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.