Fichiers de langue Flask, Babel et Javascript
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 .
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'
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,'\"');
};
};
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
En savoir plus...
Babel Flask Javascript Jinja2
Récent
- Masquer les clés primaires de la base de données UUID de votre application web
- Don't Repeat Yourself (DRY) avec Jinja2
- SQLAlchemy, PostgreSQL, nombre maximal de lignes par user
- Afficher les valeurs des filtres dynamiques SQLAlchemy
- Transfert de données sécurisé grâce au cryptage à Public Key et à pyNaCl
- rqlite : une alternative à haute disponibilité et dist distribuée SQLite
Les plus consultés
- Utilisation des Python's pyOpenSSL pour vérifier les certificats SSL téléchargés d'un hôte
- Utiliser UUIDs au lieu de Integer Autoincrement Primary Keys avec SQLAlchemy et MariaDb
- Connexion à un service sur un hôte Docker à partir d'un conteneur Docker
- Utiliser PyInstaller et Cython pour créer un exécutable Python
- SQLAlchemy : Utilisation de Cascade Deletes pour supprimer des objets connexes
- Flask RESTful API validation des paramètres de la requête avec les schémas Marshmallow