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

Ficheros de idioma Flask, Babel y Javascript

Este post describe un método para generar los archivos de idioma Javascript , de.js, en.js, etc. y cómo agregarlos a su aplicación multilenguaje Flask .

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

Este sitio web Flask es multilingüe. La implementación está descrita en posts anteriores. Hasta ahora todas mis traducciones estaban en el código Python y en las plantillas HTML . En algunos lugares necesité algunas traducciones en Javascript y lo hice tirando este código Javascript en línea en la plantilla HTML . Por ejemplo, para los formularios que necesitaba:

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

Saqué esta Javascript en la plantilla HTML y la cambié a:

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

Eso fue fácil y funciona bien.

Idioma en los ficheros Javascript

Sabía que un día tenía que cambiar esto e implementar el multilenguaje para los archivos Javascript . Este día camer antes de lo esperado porque quería implementar la cabecera Content Security Policy . Lo mínimo que debemos hacer es eliminar los scripts en línea y la función eval() :

Content-Security-Policy: script-src 'self'
Esto significa que ya no puedo tener en línea Javascript en las plantillas, todo el código Javascript debe ser movido a los archivos. La solución parece obvia. Generar un archivo de idioma con las traducciones para cada idioma e incluir el archivo adecuado. Los archivos de idioma son:
de.js
en.js
es.js
fr.js
nl.js
ru.js
Luego al final de la plantilla base hacemos algo como:
<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>
Tenga en cuenta que añado una marca de tiempo para evitar el almacenamiento en caché del navegador. Aquí mlmanager.js contiene el objeto que se utiliza para cargar y obtener los idiomas. El archivo local/<idioma>.js es el archivo con las traducciones y base.js es el archivo con todo el código. Una primera versión se ve así:
// 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,'\"');
	};
};

Al crear un nuevo objeto ml le pasamos también las traducciones. El método t se utiliza para obtener una traducción. Un archivo de idioma traducido, por ejemplo de.js, tiene un aspecto similar:

//  de.js

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

Finalmente, en el archivo con el código real Javascript , base.js, cambiamos el texto del que se debe traducir:

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

a:

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

Problema: ¿cómo generamos los archivos de lenguaje Javascript , de.js, en.js, etc.?

La documentación estándar Babel sólo menciona comandos como init, extract, update, compile. Lo que necesitamos es una forma de hacerlo:

  • extraer los textos a traducir de los archivos javascript
  • generar automáticamente los ficheros de idioma de.js, en.js, etc.

Extraer los textos a traducir de los archivos Javascript

Decidí no escanear los archivos Javascript sino crear un nuevo archivo HTML (plantilla), jsbase.html, que contenga todos los textos para los archivos Javascript , por ejemplo:

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

Ponemos este archivo en el directorio de plantillas para que sea analizado por Babel cuando emitamos los comandos de traducción estándar:

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

Ahora tenemos los textos traducidos para los archivos Javascript en algún lugar de los archivos messages.po . Puede comprobarlo, por ejemplo, descargando un archivo 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')

Esto imprime una lista de identificadores de mensaje y cadena:

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

Generar automáticamente los ficheros de idioma Javascript , de.js, en.js, etc.

Lo que necesitamos es una forma de traducir este jsbase.html fuera de Flask a nuestros idiomas y generar los archivos de.js, en.js, etc. Podríamos utilizar el código anterior para obtener los textos de los archivos Javascript y generar los archivos de idioma de.js, en.js, etc. Pero esto es engorroso y propenso a errores.

Luego reboté en una forma de renderizar una plantilla fuera de Flask, ver enlaces abajo. La idea es renderizar la plantilla jsbase.html dejando que Babel ponga las traducciones adecuadas en ella. Luego sólo tenemos que escribir el resultado renderizado en los archivos de idioma de.js, en.js, etc. ¿Es realmente tan fácil? Aquí está el código que hace esto:

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)

Esta función carga el idioma seleccionado, utiliza render() para poner la traducción y escribe el resultado como de.js, en.js, etc. Tenga en cuenta que utilizo varias aplicaciones en mi configuración, app_frontend, app_admin, utilizando DispatcherMiddleware. Para generar todos los ficheros de idioma Javascript para todas las aplicaciones e idiomas llamo a lo anterior en otra función:

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

Tenga en cuenta que esto es un poco doble por el momento porque el frontend y el administrador comparten el mismo directorio estático.

Problemas

Por supuesto que hay problemas. Cuando el código Javascript estaba en la plantilla HTML añadí el código Jinja :

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

para utilizar una cierta parte del código Javascript . No podemos seguir haciendo esto... :-(. Para ser más específico, en mi caso Javascript está llamando a otra página con una url que puede existir o no y la url también depende del idioma. Por ejemplo, el enlace en el código en línea Javascript de la plantilla HTML tiene este aspecto:

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

Lo que hice es una reescritura total del consentimiento de la cookie. Por ahora, el HTML ya no se genera en el modelo Javascript sino en el modelo HTML . Todavía necesita más trabajo.

Resumen

Esta es una primera implementación pero funciona bien. La magia está usando los Babel y Jinja APIs. Posibles mejoras:

En lugar de tener cadenas como índice para la traducción:

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

podemos querer usar objetos:

ml.t( t.Please_fill_out_this_field )

Y en lugar de tener un archivo de traducciones Javascript con objetos Javascript podemos querer usar un archivo JSON que contenga sólo las traducciones. De todas formas, los siguientes pasos serán añadir selectivamente más archivos personalizados Javascript y más traducciones.

Enlaces / créditos

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

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.