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

Flask, Babel und Javascript Sprachdateien

Dieser Beitrag beschreibt eine Methode, um Javascript Sprachdateien de.js, en.js, etc. zu erzeugen und wie Sie diese zu Ihrer mehrsprachigen Flask App hinzufügen können.

6 Januar 2020
post main image
https://unsplash.com/@goran_ivos

Diese Flask Website ist mehrsprachig. Die Implementierung ist in früheren Beiträgen beschrieben. Bisher waren alle meine Übersetzungen im Python Code und den HTML Vorlagen. An einigen wenigen Stellen brauchte ich einige Übersetzungen in Javascript und tat dies, indem ich diesen Javascript Code in der HTML Vorlage inline zog, z.B. für Formulare, die ich brauchte:

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

Ich habe diese Javascript in die Vorlage HTML gezogen und in HTML geändert:

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

Das war einfach und funktioniert gut.

Sprache in Javascript Dateien

Ich wußte, daß ich eines Tages dies ändern und die Mehrsprachigkeit für Javascript Dateien implementieren mußte. Dieser Tag kam früher als erwartet, weil ich den Content Security Policy Header implementieren wollte. Das Minimum, das wir tun sollten, ist, Inline-Skripte und die Funktion eval() zu entfernen:

Content-Security-Policy: script-src 'self'
Dies bedeutet, dass ich nicht mehr inline Javascript in den Templates haben kann, der gesamte Javascript Code muss in Dateien verschoben werden. Die Lösung sieht offensichtlich aus. Erzeugen Sie eine Sprachdatei mit Übersetzungen für jede Sprache und fügen Sie die entsprechende Datei ein. Die Sprachdateien sind:
de.js
en.js
es.js
fr.js
nl.js
ru.js
Am Ende des Basis-Templates machen wir dann so etwas wie:
<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>
Beachten Sie, dass ich einen Zeitstempel hinzufügen, um das Caching des Browsers zu vermeiden. Hier enthält mlmanager.js das Objekt, das zum Laden und Abrufen der Sprachen verwendet wird. Die Datei locales/<language>.js ist die Datei mit den Übersetzungen und base.js ist die Datei mit dem gesamten Code. Eine erste Version sieht so aus:
// 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,'\"');
	};
};

Bei der Erstellung eines neuen ml-Objektes übergeben wir auch die Übersetzungen. Methode t wird verwendet, um eine Übersetzung zu erhalten. Eine übersetzte Sprachdatei, z.B. de.js, sieht so aus:

//  de.js

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

Schließlich ändern wir in der Datei mit dem eigentlichen Javascript -Code, base.js, den Text, von dem aus übersetzt werden muss:

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

zu:

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

Problem: wie erzeugen wir die Javascript Sprachdateien de.js, en.js, etc.

Die Standard Babel Dokumentation erwähnt nur Befehle wie init, extract, update, compile. Was wir brauchen, ist ein Weg:

  • die zu übersetzenden Texte aus den Javascript-Dateien extrahieren
  • die Sprachdateien de.js, en.js, etc. automatisch generieren.

Extrahieren Sie die zu übersetzenden Texte aus den Javascript -Dateien

Ich habe mich entschieden, die Javascript Dateien nicht zu scannen, sondern stattdessen eine neue HTML (Template) Datei, jsbase.html, zu erstellen, die alle Texte für die Javascript Dateien enthält, zum Beispiel:

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

Wir haben diese Datei in das Vorlagen-Verzeichnis gelegt, so dass sie von Babel gescannt wird, wenn wir die Standard-Übersetzungsbefehle erteilen:

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

Jetzt haben wir die übersetzten Texte für die Javascript Dateien irgendwo in den messages.po Dateien. Sie können dies z.B. durch ein Dumping einer messages.po -Datei überprüfen:

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')

Dies gibt eine Liste von Message-IDs und String aus:

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

Automatische Generierung der Javascript Sprachdateien de.js, en.js, etc.

Was wir brauchen, ist eine Möglichkeit, diese jsbase.html außerhalb von Flask in unsere Sprachen zu übersetzen und die Dateien de.js, en.js, etc. zu generieren. Wir könnten den obigen Code verwenden, um die Texte aus den Javascript Dateien zu holen und die Sprachdateien de.js, en.js, etc. zu generieren. Aber das ist umständlich und fehleranfällig.

Dann bin ich in eine Möglichkeit geprallt, eine Vorlage außerhalb von Flask zu rendern, siehe Links unten. Die Idee ist, das jsbase.html Template zu rendern, indem Babel die richtigen Übersetzungen einfügt. Dann brauchen wir nur noch das gerenderte Ergebnis in die Sprachdateien de.js, en.js, etc. zu schreiben. Ist es wirklich so einfach? Hier ist der Code, der dies tut:

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)

Diese Funktion lädt die ausgewählte Sprache, verwendet render(), um translate zu setzen und schreibt das Ergebnis als de.js, en.js, etc. Beachten Sie, dass ich mehrere Anwendungen in meinem Setup, app_frontend, app_admin, mit DispatcherMiddleware verwende. Um alle Javascript Sprachdateien für alle Apps und Sprachen zu generieren, rufe ich die oben genannte Funktion in einer anderen Funktion auf:

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()

Beachten Sie, dass dies im Moment ein bisschen doppelt so viel ist, da sich Frontend und Admin das gleiche statische Verzeichnis teilen.

Probleme

Natürlich gibt es Probleme. Als der Code Javascript in der Vorlage HTML war, habe ich den Code Jinja hinzugefügt:

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

um einen bestimmten Teil des Codes Javascript zu verwenden. Wir können das nicht mehr tun ... :-(. Um genauer zu sein, in meinem Fall ist Javascript der Aufruf einer anderen Seite mit einer Url, die existieren kann oder nicht und die Url hängt auch von der Sprache ab. Zum Beispiel sieht der Link im Javascript Inline-Code der HTML -Vorlage wie folgt aus

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

Was ich getan habe, ist eine komplette Neufassung der Cookie-Einwilligung. Vorerst wird die HTML nicht mehr in der Vorlage Javascript , sondern in der HTML generiert. Es bedarf noch weiterer Arbeit.

Zusammenfassung

Dies ist eine erste Implementierung, aber sie funktioniert gut. Die Magie ist die Verwendung der Babel und Jinja APIs. Mögliche Verbesserungen:

Anstatt Strings als Index für die Übersetzung zu haben:

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

wollen wir vielleicht Objekte verwenden:

ml.t( t.Please_fill_out_this_field )

Und statt einer Übersetzungs Javascript -Datei mit Javascript -Objekten sollten wir eine JSON-Datei verwenden, die nur die Übersetzungen enthält. Wie auch immer, die nächsten Schritte werden sein, selektiv weitere benutzerdefinierte Javascript Dateien und weitere Übersetzungen hinzuzufügen.

Links / Impressum

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

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.