angle-up arrow-clockwise arrow-counterclockwise arrow-down-up arrow-left at calendar card-list chat check envelope folder house info-circle pencil people person person-plus phone plus question-circle search tag trash x

Fichiers de langue Flask, Babel et Javascript

6 janvier 2020 à côté de Peter

Ce post décrit une méthode pour générer les fichiers de langue Javascript de.js, en.js, etc. et comment les ajouter à votre application multilingue Flask .

post main image
https://unsplash.com/@goran_ivos

Ce site Web Flask est multilingue. L'implémentation est décrite dans les messages précédents. Jusqu'à présent, toutes mes traductions étaient dans le code Python et les modèles HTML . À quelques endroits, j'avais besoin de quelques traductions dans Javascript et j'ai fait cela en tirant ce code Javascript en ligne dans le modèle HTML . Par exemple, pour les formulaires, j'avais besoin :

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

J'ai inséré ce Javascript dans le modèle HTML et je l'ai transformé en :

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

C'était facile et ça marche bien.

Langue dans les fichiers Javascript

Je savais qu'un jour je devais changer cela et implémenter le multilangage pour les fichiers Javascript . Ce jour est arrivé plus tôt que prévu car je voulais implémenter l'en-tête Content Security Policy . Le minimum que nous devrions faire est de supprimer les scripts en ligne et la fonction eval() :

Content-Security-Policy: script-src 'self'
Cela signifie que je ne peux plus avoir l'inline Javascript dans les modèles, tout le code Javascript doit être déplacé dans les fichiers. La solution semble évidente. Générez un fichier de langue avec les traductions pour chaque langue et incluez le fichier approprié. Les fichiers de langue sont :
de.js
en.js
es.js
fr.js
nl.js
ru.js
Ensuite, à la fin du modèle de base, nous faisons quelque chose comme
<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>
Notez que j'ajoute un horodatage pour éviter la mise en cache du navigateur. Ici mlmanager.js contient l'objet qui est utilisé pour charger et récupérer les langues. Le fichier locales/<language>.js est le fichier avec les traductions et base.js est le fichier avec tout le code. Une première version ressemble à ceci :
// 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,'\"');
	};
};

Lors de la création d'un nouvel objet ml, nous passons également les traductions. La méthode t est utilisée pour obtenir une traduction. Un fichier de langue traduit, par exemple de.js, ressemble à

//  de.js

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

Enfin, dans le fichier avec le code Javascript actuel, base.js, nous changeons le texte qui doit être traduit :

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

à :

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

Problème : comment générer les fichiers de langue Javascript , de.js, en.js, etc.

La documentation standard Babel ne mentionne que des commandes comme init, extract, update, compile. Ce qu'il nous faut, c'est un moyen de :

  • d'extraire les textes à traduire des fichiers javascript
  • générer automatiquement les fichiers de langue de.js, en.js, etc.

Extraire les textes à traduire des fichiers Javascript

J'ai décidé de ne pas scanner les fichiers Javascript mais de créer un nouveau fichier HTML (template), jsbase.html, contenant tous les textes des fichiers Javascript , par exemple :

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

Nous avons placé ce fichier dans le répertoire des modèles afin qu'il soit analysé par Babel lorsque nous lancerons les commandes de traduction standard :

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

Nous avons maintenant les textes traduits pour les fichiers Javascript quelque part dans les fichiers messages.po . Vous pouvez vérifier ceci par exemple en vidant un fichier 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')

Cela permet d'imprimer une liste d'identifiants et de chaînes de messages :

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

Générer automatiquement les fichiers de langue Javascript , de.js, en.js, etc.

Ce dont nous avons besoin est un moyen de traduire ce jsbase.html en dehors de Flask dans nos langues et de générer les fichiers de.js, en.js, etc. Nous pourrions utiliser le code ci-dessus pour obtenir les textes des fichiers Javascript et générer les fichiers de langue de.js, en.js, etc. Mais c'est lourd et sujet à erreur.

Puis j'ai rebondi sur une façon de rendre un template en dehors de Flask, voir les liens ci-dessous. L'idée est de rendre le modèle jsbase.html en laissant Babel y mettre les traductions appropriées. Ensuite, il suffit d'écrire le résultat rendu dans les fichiers de langue de.js, en.js, etc. C'est vraiment si facile ? Voici le code qui fait cela :

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)

Cette fonction charge la langue sélectionnée, utilise render() pour mettre translate et écrit le résultat sous la forme de.js, en.js, etc. Notez que j'utilise plusieurs applications dans mon installation, app_frontend, app_admin, en utilisant DispatcherMiddleware. Pour générer tous les fichiers de langue Javascript pour toutes les applications et les langues, j'appelle la fonction ci-dessus dans une autre fonction :

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

Notez que ceci est un peu double pour le moment car le frontend et l'admin partagent le même répertoire statique.

Problèmes

Bien sûr qu'il y a des problèmes. Lorsque le code Javascript se trouvait dans le modèle HTML , j'ai ajouté le code Jinja :

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

pour utiliser une certaine partie du code Javascript . On ne peut plus faire ça... :-(. Pour être plus précis, dans mon cas Javascript appelle une autre page avec une url qui peut exister ou non et l'url dépend aussi de la langue. Par exemple, le lien dans le code en ligne Javascript du modèle HTML ressemble à ceci :

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

Ce que j'ai fait est une réécriture totale du consentement du cookie. Pour l'instant, le modèle HTML n'est plus généré dans le modèle Javascript mais dans le modèle HTML . Il faut encore travailler dessus.

Résumé

Il s'agit d'une première mise en œuvre, mais elle fonctionne bien. La magie utilise les Babel et Jinja API. Améliorations possibles :

Au lieu d'avoir des chaînes de caractères comme index pour la traduction :

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

nous pourrions vouloir utiliser des objets :

ml.t( t.Please_fill_out_this_field )

Et au lieu d'avoir un fichier de traductions Javascript avec des objets Javascript , nous pouvons vouloir utiliser un fichier JSON contenant uniquement les traductions. De toute façon, les prochaines étapes seront d'ajouter sélectivement plus de fichiers Javascript personnalisés et plus de traductions.

Liens / crédits

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

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.