Flask, Babel en Javascript taalbestanden
Deze post beschrijft een methode om Javascript taalbestanden de.js, en.js, etc. te genereren en hoe deze toe te voegen aan uw meertalige Flask app.
Deze Flask website is meertalig. De implementatie is beschreven in eerdere berichten. Tot nu toe stonden al mijn vertalingen in de Python code en de HTML sjablonen. Op een paar plaatsen had ik enkele vertalingen nodig in Javascript en deed dit door deze Javascript code inline te trekken in het HTML sjabloon. Bijvoorbeeld voor formulieren die ik nodig had:
e.target.setCustomValidity('Please fill out this field.');
Ik heb deze Javascript in het sjabloon HTML getrokken en veranderd in HTML :
e.target.setCustomValidity("{{ _('Please fill out this field.') }}");
Dat was makkelijk en werkt prima.
Taal in Javascript -bestanden
Ik wist dat ik op een dag dit moest veranderen en multilanguage voor Javascript bestanden moest implementeren. Deze dag camera eerder dan verwacht omdat ik de Content Security Policy header wilde implementeren. Het minimum dat we moeten doen is inline scripts en de eval() functie verwijderen:
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,'\"');
};
};
Bij het maken van een nieuw ml object passeren we ook de vertalingen. Methode t is gebruik om een vertaling te krijgen. Een vertaald taalbestand, bijvoorbeeld de.js, ziet er zo uit:
// de.js
var ml = new ML({
'key2translations': {
'Content item': "Inhaltselement",
'Please fill out this field.': "Bitte füllen Sie dieses Feld aus.",
},
});
Tot slot, in het bestand met de eigenlijke Javascript code, base.js, wijzigen we de tekst waaruit moet worden vertaald:
e.target.setCustomValidity('Please fill out this field.');
naar:
e.target.setCustomValidity( ml.t('Please fill out this field.') );
Probleem: hoe genereren we de Javascript taalbestanden de.js, en.js, enz.
De standaard Babel documentatie vermeldt alleen commando's zoals init, extract, update, compileren. Wat we nodig hebben is een manier om:
- haal de te vertalen teksten uit de javascript-bestanden
- automatisch de taalbestanden de.js, en.js, etc. genereren.
Pak de te vertalen teksten uit de Javascript -bestanden uit.
Ik heb besloten om de Javascript bestanden niet te scannen, maar in plaats daarvan een nieuw HTML (template) bestand te maken, jsbase.html, waarin alle teksten voor de Javascript bestanden, bijvoorbeeld, staan:
var ml = new ML({
'key2translations': {
...
'Content item': "{{ _('Content item') }}",
'Please fill out this field.': "{{ _('Please fill out this field.') }}",
...
},
});
We zetten dit bestand in de templates directory zodat het gescand wordt door Babel wanneer we de standaard vertaalopdrachten geven:
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
Nu hebben we de vertaalde teksten voor de Javascript bestanden ergens in de messages.po bestanden. U kunt dit bijvoorbeeld controleren door een messages.po bestand te dumpen:
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')
Deze print een lijst van bericht id's en string:
...
message.id = Sub image, message.string = Unterbild
message.id = Sub image text, message.string = Unterbildtext
message.id = Select image, message.string = Bild auswählen
...
Genereer automatisch de taalbestanden Javascript , de.js, en.js, enz.
Wat we nodig hebben is een manier om deze jsbase.html buiten Flask naar onze talen te vertalen en de bestanden de.js, en.js, etc. te genereren. We zouden bovenstaande code kunnen gebruiken om de teksten uit Javascript bestanden te halen en de taalbestanden de.js, en.js, etc. te genereren. Maar dit is omslachtig en foutgevoelig.
Vervolgens heb ik een manier gevonden om een sjabloon weer te geven buiten Flask, zie de links hieronder. Het idee is om het jsbase.html sjabloon weer te geven zodat Babel er de juiste vertalingen in kan zetten. Dan hoeven we alleen nog maar het gerenderde resultaat naar de taalbestanden de.js, en.js, etc. te schrijven. Is het echt zo makkelijk? Hier is de code die dit doet:
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)
Deze functie laadt de geselecteerde taal, gebruikt render() om te vertalen en schrijft het resultaat als de.js, en.js, etc. Merk op dat ik meerdere apps gebruik in mijn setup, app_frontend, app_admin, met behulp van DispatcherMiddleware. Om alle Javascript taalbestanden voor alle apps en talen te genereren, roep ik het bovenstaande op in een andere functie:
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()
Merk op dat dit op dit moment een beetje dubbel is omdat frontend en admin dezelfde statische directory delen.
Problemen
Natuurlijk zijn er problemen. Toen de code Javascript in het sjabloon HTML stond, heb ik Jinja toegevoegd:
{% if ... %}
...
{% else %}
...
{% endif %}
om een bepaald deel van de code Javascript te gebruiken. We kunnen dit niet meer doen... :-(. Om specifieker te zijn, in mijn geval roept Javascript een andere pagina op met een url die al dan niet kan bestaan en de url is ook afhankelijk van de taal. Zo ziet de link in de Javascript inline code van het HTML sjabloon er als volgt uit:
{% if 'Privacy policy' in app_template_slug %}
moreLink: '{{ url_for('pages.page_view', slug=app_template_slug['Privacy policy']['slug']) }}',
{% else %}
moreLink: '',
{% endif %}
Wat ik heb gedaan is een totale herschrijving van de cookie toestemming. Voorlopig wordt de HTML niet meer gegenereerd in de Javascript maar in het sjabloon HTML . Het heeft nog meer werk nodig.
Samenvatting
Dit is een eerste implementatie, maar het werkt prima. De magie is het gebruik van de Babel en Jinja APIs. Mogelijke verbeteringen:
In plaats van het hebben van strings als index voor de vertaling:
ml.t('Please fill out this field')
willen we misschien objecten gebruiken:
ml.t( t.Please_fill_out_this_field )
En in plaats van een vertaling Javascript bestand met Javascript objecten willen we misschien een JSON bestand gebruiken dat alleen de vertalingen bevat. Hoe dan ook, de volgende stappen zijn het selectief toevoegen van meer aangepaste Javascript -bestanden en meer vertalingen.
Links / credits
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
Lees meer
Babel Flask Javascript Jinja2
Recent
- Database UUID primaire sleutels van je webapplicatie verbergen
- Don't Repeat Yourself (DRY) met Jinja2
- SQLAlchemy, PostgreSQL, maximum aantal rijen per user
- Toon de waarden in SQLAlchemy dynamische filters
- Veilige gegevensoverdracht met Public Key versleuteling en pyNaCl
- rqlite: een alternatief voor SQLite met hoge beschikbaarheid en distributed
Meest bekeken
- Met behulp van Python's pyOpenSSL om SSL-certificaten die van een host zijn gedownload te controleren
- Gebruik van UUIDs in plaats van Integer Autoincrement Primary Keys met SQLAlchemy en MariaDb
- Maak verbinding met een dienst op een Docker host vanaf een Docker container
- PyInstaller en Cython gebruiken om een Python executable te maken
- SQLAlchemy: Gebruik van Cascade Deletes om verwante objecten te verwijderen
- Flask RESTful API verzoekparametervalidatie met Marshmallow-schema's