Café y webs multi-idioma

¡Hola!

Tras mucho currelar en esto ya toca el momento de hablar de Schiumato, un generador de sitios web estáticos que ya mencioné en mi entrada sobre el amado arte de afeitar animales lanudos.

Lo hice para gestionar la página web de ElenQ Technology, la empresa que estoy fundando y que también os mencioné in the past.

Como ya he hecho la web y esto ha funcionado es momento de contar cómo funciona y cómo se utiliza.

La verdad es que tiene una lista más larga de cosas pendientes que la lista de cosas hechas, pero es un programa tan extremadamente sencillo que da risa. Pero funciona. Como tiene que ser.

Lo podéis instalar directamente con:

npm install -g schiumato

Para usarlo también os recomiendo que instaléis un servidor HTTP para probar.

npm install -g live-server

Este servidor mola porque cuando se hacen cambios en sus ficheros te actualiza el browser automáticamente. ¡Para nuestro proceso vendrá perfecto!

Schiumato funciona de la siguiente manera:

Tienes un conjunto de plantillas de nunjucks con un par de movidas extra que yo he añadido: _() y filter translate sirven para traducir. Todo string bajo esas funciones será traducido tomando como referencia los ficheros de locales. Tenéis más explicaciones en la minidocumentación de Schiumato.

Cuando disparemos schiumato create, se procesarán esas plantillas y el resultado se copiará en la carpeta de destino de forma ordenada. Las plantillas se renderizan en orden, una vez por idioma, y se vuelcan en su carpeta correspondiente. Los ficheros de la carpeta estática se copian literalmente. Por ejemplo, con mi web:

www
├── en
│   ├── about.html
│   ├── contact.html
│   ├── index.html
│   ├── services.html
│   └── support.html
├── es
│   ├── about.html
│   ├── contact.html
│   ├── index.html
│   ├── services.html
│   └── support.html
├── eu
│   ├── about.html
│   ├── contact.html
│   ├── index.html
│   ├── services.html
│   └── support.html
└── static
    ├── css
    │   ├── fonts.css
    │   ├── normalize.css
    │   ├── skeleton.css
    │   └── style.css
    ├── files
    │   └── publickey.hello@elenq.tech.txt
    ├── fonts
    │   ├── LatoLatin-Light.eot
    │   ├── LatoLatin-Light.ttf
    │   ├── LatoLatin-Light.woff
    │   ├── LatoLatin-Light.woff2
    │   ├── LatoLatin-Regular.eot
    │   ├── LatoLatin-Regular.ttf
    │   ├── LatoLatin-Regular.woff
    │   ├── LatoLatin-Regular.woff2
    │   └── OFL.txt
    └── img
        ├── ElenQTechLogo.png
        ├── ElenQTechLogo.svg
        ├── faces
        │   └── ekaitz.jpg
        ├── liberapay.svg
        └── projects
            └── sotapatroi.png

10 directories, 34 files

Veis como se vuelca 3 veces el HTML, en Inglés, Español y Euskera. El contenido estático está sólo una vez porque no se traduce y así no se replica.

Con live-server arrancado en la carpeta www, podéis ir viendo cómo quedan las cosas cada vez que lanzáis una creación.

Y este es el rollo.

Si queréis ver las plantillas podéis verlas en el repo de la web aquí.

Para hacer el deploy copiáis toda la carpeta WWW y ya, eso sí, tenéis que seleccionar el idioma que queréis que se muestre por defecto para que os redireccione ahí vuestro servidor para que se vea algo al entrar a la raíz. Eso es fácil de hacer.

Y nada, esta es la web resultante:

http://elenq.tech

Esta también redirecciona a HTTPS 😉

Espero que os mole el rollo.

Sed buenos.

Traducciones y tripas

Hola chavales,

He sacado un rato para contaros una cosilla que me ha tocado investigar últimamente. Las herramientas de traducción de aplicaciones, también conocidas i18n y l10n por internationalization y localization (contad las letras de las palabras y entenderéis el número).

Esas herramientas de traducción no son simplemente eso. Sirven para traducir y localizar las aplicaciones, no sólo traducen las frases o palabras, también saben elegir si usar un singular o un plural dependiendo de un número o utilizar cuantificadores como “mucho” o “poco” (pluralization). A parte de eso, valen para transformar la moneda, el formato de las fechas, las unidades de medida y ese tipo de cosas. El l10n hace unas cosas y el i18n otras leed al respecto que no es malo informarse (y se escapa a lo que quiero contar aquí).

Hoy quiero contarlos cómo hacer una aplicación de éstas de forma simple, y sólo me centraré en las traducciones. Además, tendréis mi solución de i18n de la que hablé en la entrada anterior, que es rematadamente simple y trata exactamente de lo que hablaré aquí hoy1. Ni más ni menos.

Esta entrada pretende que se os ocurra a vosotros cómo hacer una librería de este estilo, tirando a lo simple, para que los más novatos entendáis cómo es posible hacer cosas útiles con el conocimiento que ya tenéis. No es tan difícil programar. Pensadlo: si lo fuera, posiblemente el que os escribe no sería capaz de hacerlo.

Vale, vamos a por ello.

¿Cómo creéis que funciona una mierda de éstas?

Pensad un poco conmigo. Recordad que sólo queremos que pueda traducir, no queremos pluralización ni extras (luego hablaremos también de eso).

A nivel de usuario de la librería, normalmente funcionan de la siguiente manera:

  1. Importamos la librería en el programa que queramos que pueda ser traducido.
  2. En todo el texto que vaya a ser visto por el usuario se pone un identificador de algún tipo. Normalmente se trata de una llamada a una función entregada por la librería. Ejemplos:

i18n("texto a traducir")

_("texto a traducir")

gettext("texto a traducir")

  1. De alguna manera, se vuelcan todos los strings para traducir en unos ficheros con espacio para su traducción. Uno por idioma.
  2. Se traducen.
  3. Durante la ejecución, la aplicación busca los strings a traducir antes de mostrarlos en los ficheros. Cuando los encuentra, usa la traducción adecuada al lenguaje configurado por el usuario.

Así es como funciona. Simple ¿No?

Si no me he explicado bien podéis buscar algunas librerías de i18n y mirar su documentación para ver qué formato de ficheros utiliza o qué herramienta extrae los strings para la traducción.

Una vez entendido esto, hay que empezar a darle vueltas a cómo lo implementaríamos. Durante el desarrollo de esto haré algunas trampitas, porque soy un poco malévolo, pero os prometo que tendrán sentido.

¿Cómo lo haríais vosotros? ¿Por dónde empezaríais? ¿Qué os parece lo más difícil?

Nosotros vamos empezar por el paso 2 de la lista anterior. La forma de indicar que los strings son traducibles. La mejor forma es meterlos en una función, aunque habría otras. Si le damos al usuario de la librería una función a utilizar será simple: la aplicará a los strings que le interesen y todo irá bien.

Hay muchas formas de hacer esto, la más fácil es que los ficheros de traducción sean simples ficheros tipo JSON donde las claves sean el texto a traducir y los valores la traducción:

{
"hola": "hi",
"adiós": "bye"
}

Con esto así, cuando se llame a la función con un string sería suficiente con buscar en ese JSON la clave y devolver el valor. En JavaScript y simplificando muchísimo:

var traduccion = carga_traduccion();
function _( string ){
  return( traduccion[string] );
}

Lo que no hemos hecho todavía es esa función carga_traduccion que vemos en el snippet y tampoco gestionamos cuál es el idioma actual del usuario.

Eso tampoco es taaan difícil ¿no?

Podemos tirar por un formato orientado a objetos para hacerlo sencillo. Suponemos que el sistema de traducciones es una clase/prototipo (no me voy a poner a discutir esto ahora) con un campo de “idioma actual” y lo de arriba se transformaría en algo un poco más complejo si suponemos que el JSON de idiomas tiene un nivel de más que referencia al idioma. En JavaScript de nuevo:

{
"hola": { "english": "hi", "euskera": "kaixo" }
"adiós": { "english": "bye", "euskera": "agur" }
}

//...
function _( string ){
  this.traduccion[string][this.idioma];
}
//...

Una vez tenemos esto, sólo necesitamos hacer que nuestra clase/prototipo pueda configurar el idioma, leer los ficheros de idioma automáticamente al iniciar (el carga_traduccion de antes) y que le entregue la función _ al hilo principal.

Cargar los ficheros en principio es sólo leer una carpeta y ya. Aunque si estamos en el browser tenemos que hacer alguna magia añadida que no debería costaros mucho. Tema resuelto.

¡Coño! ¿Y ya está?

En realidad no, porque el traductor tendría un desastre de ficheros de idioma, y encima tendría que rellenarlos a mano. Lo suyo es que haya una herramienta tipo gettext que extraiga todos los strings a traducir y los vuelque en ficheros que luego tengan que traducirse. Ésta es la trampa, este punto es el más difícil. En mi solución de i18n, como sé que el lugar donde va a usarse lo permite, la propia clase vuelca los ficheros (también llamados catalog) al terminar la traducción, pero esto no es factible siempre porque si la ejecución del programa nunca pasa por mostrar el string nunca aparecería en los catálogos. (¿Quizás con unos tests brutos podríamos hacerlo?)

Otro punto interesante es tener en cuenta que hemos usado strings con texto, en nuestro caso en castellano. Entonces si el usuario tuviese seleccionado el idioma en castellano no tendrían que traducirse y tendrían que entregarse igual. Hay dos formas sencillas de atacar este problema:

  1. Obligar al usuario a no poner el string en concreto y poner un identificador para siempre se busque en la traducción.
  2. Tener configurado un idioma por defecto que sea el original.

Otra cosa que quiero tocar es que esos ficheros de JSON pueden ser la muerte (a pesar de que algunas herramientas los usan) porque si el texto a traducir es muy largo nos quedaríamos con unas claves gigantescas en una única línea (JSON no permite claves multilínea). El contenido también sería difícil de traducir porque también sería en una sola línea y tendría también caracteres especiales por el medio para marcar los saltos. ¡Quizás sea mejor usar otro tipo de fichero! Mi solución i18n usa YAML1 pero también tiene un sistema diferente para gestionar las claves. Si os interesa me preguntáis o lo miráis.

Otro punto extra es que no tenemos ninguna información sobre lo que estamos traduciendo. La misma palabra puede tener que traducirse de dos maneras diferentes dependiendo de dónde esté porque el contexto puede ser distinto. En este caso, nuestro programa sólo contempla que textos iguales se traducen igual.

Tampoco nos dice en qué línea del programa original está lo que traducimos así que es difícil encontrarlo.

Ni nos da ningún tipo de detalle sobre la traducción, cosa que en algunas soluciones i18n existe: Te permiten poner palabras clave para definir el contexto mejor como guías para los traductores que se ignoran en el programa pero que se vuelcan en los ficheros de traducción. Ejemplo:

i18n("Abrir", "Abre un fichero"); // siendo Abrir el string a traducir

Ya os he hablado de la pluralización antes, y de la internacionalización también. Eso también complica las cosas…

Joder…

Bueno eso, que era fácil…

¿No?

😉

Un abrazo.

De sitios web, traducciones y vacas peludas

Hola chavalada,

Hace mucho que no os traigo nada fresco y me parece muy mal y más vale que a vosotros también porque si no no entiendo por qué os leéis esta mierda de sitio.

Con eso de la empresa necesito una página web. ¿Quién no necesita una hoy en día? ¡Si hasta las panaderías tienen!

Como soy un afeitador de yaks profesional me he dedicado un par de semanas a hacerlo. No sé parar. Os cuento cómo fue el rollo.

Necesitaba una página web para la empresa. Esto me llevó a querer escribirla, pero quería que fuera simple y estática y no quería tener que gestionar todo en HTML a pelo y repetir pedazos así que tenía que ser modular. Para esto necesitaba un generador de páginas web. Lo malo es que el creador de webs tenía que ser capaz de traducir todo de forma automática sin tener que reescribir toda la web.

Y esto no es muy extremo, la verdad. No tardé mucho en conseguirlo (¿un par de días en ratos sueltos?). Aquí la prueba.

Pero ya os he dicho que no sé parar y como el tipo de fichero de traducción generado por gettext no me gustaba, no podía automatizar todo dentro del mismo script porque tenía que llamar a los métodos internos de pyBabel que no estaban muy claros en la documentación y ahora me estoy saturando de python y quiero cambiar de aires pues… Se me fue la olla y decidí hacerlo en Node.js.

No en CoffeeScript, como he hecho otras cosas. Esta vez tenía que ser JavaScript. Porque sí. Además quería forzar un poco de programación funcional y ya que me ponía quería jugar con Underscore porque la quiero usar en otro proyecto y ya mato dos pájaros de un tiro.

Cuál fue mi sorpresa cuando me puse a investigar diferentes librerías de templating y de i18n (internacionalización, para las traducciones) y no me quedaba satisfecho.

Para el templating probé cosas parecidas a Jinja, que es lo que conocía y el proyecto que hice en Python que te he linkado antes. Fueron:

  • Jinjs: Una implementación en de Jinja en Node.js. Casi compatibles al 100% pero no tiene muy buena documentación. Te referencia a la de Jinja y me daba miedo basarme en cosas que luego no funcionaran bien.
  • Plate: No probé mucho, hice un ejemplo y funcionó bien. Son plantillas compatibles con las de Django (un framework web de Python), que al mismo tiempo es el projecto en el que se basan las de Jinja que usé en la prueba de concepto de antes.
  • Nunjucks: Un proyecto basado en Jinja. Muy similar y compatible en todo lo que necesitaba. Me quedé con estas porque tienen una docu bastante decente. Aunque luego una cosa que quise hacer no estaba bien documentada y la tuve que hacer de otra manera. 🙂
  • Y algunas otras diferentes por probar, pero no me gustaron para esta aplicación. Mustache y similares. Interesantes también para otras cosas.

Pero eso sólo solucionaba medio problema.

La mayor parte de soluciones de i18n que encontré tienen muchas cosas que no necesito como que te capturen automáticamente las locales y encima usan un fichero JSON para las traducciones que es muy difícil de gestionar con strings largos o multilínea. Este último problema en mi caso era grave porque traducir una web conlleva mucho contenido, no es lo mismo que una aplicación. Así que tenía que hacer algo.

Pensé que usar un YAML molaba mucho más porque gestiona los strings largos de una forma mucho más bonita. Para empezar, puede ajustarse el ancho del fichero a un límite de caracteres por defecto y, con eso, marca de forma distinta los strings que son muy largos y han sido ajustados al ancho del fichero añadiendo saltos de línea o los strings que contenían saltos de línea y deben ser leídos de forma literal. Esto para mí es oro puro y lo necesitaba.

Así que. Lo hice. Me dediqué unos días a crear mi propia librería de i18n (que tengo que documentar mejor, por cierto). Está aquí:

https://github.com/ekaitz-zarraga/i18n_yaml

Y la podéis instalar con:

npm install i18n_yaml

Y eso ya me habilitó el poder hacer el constructor de sitios webs estáticos con i18n á la Ekaitz y lo pude hacer y poner aquí:

https://gitlab.com/ElenQ/schiumato

Ahora mismo está un poco en bragas (tenéis una lista de cosas pendientes al final del README.md), pero tiene buena pinta y funciona. No os voy a contar de donde viene el nombre porque entonces tendría que hablar de otro gestor de sitios que… Ejem.

Todavía me queda el último salto de la recursión, claro, me queda hacer la web. Pero eso os lo cuento otro día.

Tampoco he entrado en profundidad a contar cómo funcionan estas cosas, como me suele gustar hacer. Igual lo hago pronto y aprovecho para documentarlas un poco mejor.

Un abrazo.

Seguid afeitando yaks y cambiando el mundo por el camino.