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.
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'
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
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
Mehr erfahren
Babel Flask Javascript Jinja2
Neueste
- Ausblenden der Primärschlüssel der Datenbank UUID Ihrer Webanwendung
- Don't Repeat Yourself (DRY) mit Jinja2
- SQLAlchemy, PostgreSQL, maximale Anzahl von Zeilen pro user
- Anzeige der Werte in den dynamischen Filtern SQLAlchemy
- Sichere Datenübertragung mit Public Key Verschlüsselung und pyNaCl
- rqlite: eine hochverfügbare und distverteilte SQLite -Alternative
Meistgesehen
- Verwendung von Pythons pyOpenSSL zur Überprüfung von SSL-Zertifikaten, die von einem Host heruntergeladen wurden
- Verwendung von UUIDs anstelle von Integer Autoincrement Primary Keys mit SQLAlchemy und MariaDb
- Verbindung zu einem Dienst auf einem Docker -Host von einem Docker -Container aus
- PyInstaller und Cython verwenden, um eine ausführbare Python-Datei zu erstellen
- SQLAlchemy: Verwendung von Cascade Deletes zum Löschen verwandter Objekte
- Flask RESTful API Validierung von Anfrageparametern mit Marshmallow-Schemas