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 .
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'
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,'\"');
};
};
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
Leer más
Babel Flask Javascript Jinja2
Recientes
- Cómo ocultar las claves primarias de la base de datos UUID de su aplicación web
- Don't Repeat Yourself (DRY) con Jinja2
- SQLAlchemy, PostgreSQL, número máximo de filas por user
- Mostrar los valores en filtros dinámicos SQLAlchemy
- Transferencia de datos segura con cifrado de Public Key y pyNaCl
- rqlite: una alternativa de alta disponibilidad y dist distribuida SQLite
Más vistos
- Usando Python's pyOpenSSL para verificar los certificados SSL descargados de un host
- Usando UUIDs en lugar de Integer Autoincrement Primary Keys con SQLAlchemy y MariaDb
- Conectarse a un servicio en un host Docker desde un contenedor Docker
- Usando PyInstaller y Cython para crear un ejecutable de Python
- SQLAlchemy: Uso de Cascade Deletes para eliminar objetos relacionados
- Flask RESTful API validación de parámetros de solicitud con esquemas Marshmallow