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

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.

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

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'
Dit betekent dat ik geen inline Javascript meer in de templates kan hebben, alle Javascript code moet worden verplaatst naar bestanden. De oplossing ligt voor de hand. Genereer een taalbestand met vertalingen voor elke taal en voeg het juiste bestand toe. De taalbestanden zijn:
de.js
en.js
es.js
fr.js
nl.js
ru.js
Dan doen we aan het einde van het basissjabloon zoiets als:
<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>
Merk op dat ik een tijdstempel toevoeg om caching van de browser te voorkomen. Hier houdt mlmanager.js het object vast dat wordt gebruikt om de talen te laden en te krijgen. Het bestand locales/<taal>.js is het bestand met de vertalingen en base.js is het bestand met alle code. Een eerste versie ziet er zo uit:
// 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

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.