Cómo hacer una web app multiidioma en Angular: escalabilidad y últimos detalles (III)

Juan Martín03-Ago, 2020

Hasta ahora hemos visto cómo adaptar todos los textos de nuestra web app al idioma del usuario. Lo siguiente que veremos es cómo utilizar ngx-translate de forma eficiente, así como dar solución a algunos problemas que pueden surgir al trabajar con distintos idiomas.

Trataremos tres temas en esta parte:

  1. Organización del archivo JSON de claves.
  2. Uso de parámetros con ngx-translate.
  3. Diferencias gramaticales entre idiomas y cómo darles solución.

Organización del archivo JSON de claves

Conforme el proyecto va creciendo y se van añadiendo más y más claves a los archivos JSON de idiomas, se hace necesario implementar algún tipo de organización para las mismas. Suele haber 2 opciones: organizar claves por la sección de la web a la que pertenecen o, por su función.

La organización por sección consiste en agrupar claves por la sección de la web donde se utilicen. Por ejemplo, si tenemos una sección en la web para el admin, todas las claves que se usen en ella irían agrupadas dentro de un objeto admin. Suele conllevar algunos problemas. Por ejemplo, una clave que se cree para una sección concreta pero después tenga que ser usada también en otra distinta. Esto implicaría duplicar esa clave en el JSON, moverla a una sección “general” (con el trabajo de refactorización que implica) o usarla en dos secciones, aunque por la estructura del JSON parezca que solo pertenece a una de ellas (con lo cual dicha agrupación pierde el sentido).

La organización por función, por otro lado, consiste en agrupar claves principalmente por el uso que se les va a dar, y solo en casos concretos por la sección. Como grupos tendríamos, por poner algunos ejemplos:

  1. Fields: Campos (o partes) de formularios o de tablas en la BBDD: name, city, country
  2. Actions: Acciones en la web, normalmente textos en botones y enlaces: create, delete, play, stop, download
  3. Entities: Se corresponden con tablas en la BBDD: users, coursers, notifications
  4. Views: vistas en la web que no se corresponden con una única entidad: profile, home, dashboard

Y cualquier otra función que fuese necesaria (messages, errors…).

Ocasionalmente también habría agrupaciones por la sección a la que pertenecen. Auth, por ejemplo, sería un grupo también, pero en este caso el significado no sería tanto “textos que solo se usan en el módulo Auth” sino más bien “textos que están relacionados con el módulo Auth”. Si tenemos 4 botones distintos por toda la web pidiendo al usuario que inicie sesión, el texto de todos ellos pertenece a Auth, independientemente de las secciones concretas donde se estén usando.

Uso de parámetros con ngx-translate

Cuando comencemos a añadir botones y mensajes en la web, es muy probable que surjan textos del tipo “Nuevo usuario”, “Editar usuario” o “¿Está seguro de que desea eliminar este usuario?”, los cuales se van a repetir una y otra vez por cada entidad en la página.

Una forma de lidiar con esto es crear claves con el texto completo en cada caso (“¿Está seguro de que desea eliminar este curso?”, “¿Está seguro de que desea eliminar esta notificación?”, etc) lo cual hará que haya muchísimo texto repetido en el JSON, cuando realmente lo único que está cambiando es una palabra.

Otra forma es traducir por separado la parte general de la específica y unirlas, de esta forma:

Sin embargo, esto da por hecho que en todos los idiomas el orden de las palabras va a ser siempre el mismo, lo cual puede no ser cierto. Un ejemplo de esto sería “Datos del usuario”. En inglés querríamos mostrar “User data”, pero el orden en este caso es el opuesto. Aquí es donde entra el uso de parámetros. Si hacemos que la entidad sea un parámetro que se pasa al texto del campo data, no tendremos problema cuando el orden varíe entre idiomas. Lo haríamos de la siguiente forma:

Definimos la clave var_data en el JSON indicando con doble llave el lugar donde se colocará el parámetro. Para inglés hacemos lo mismo, pero con el orden que tiene sentido en ese idioma:

Y después simplemente pasamos el parámetro cuando utilicemos ese texto.

Ejemplo de uso con translate pipe:

Ejemplo utilizando TranslateService:

En este caso, como el parámetro es también una clave que hay que traducir, se debe traducir por separado la clave de la entidad antes de pasarla como parámetro al texto principal. Si el parámetro fuese un número o una fecha, por ejemplo, no habría necesidad de utilizar dos veces la pipe o el servicio.

Diferencias gramaticales entre idiomas y cómo darles solución

En el ejemplo anterior hemos visto cómo dar solución a un problema gramatical (distinto orden a la hora de construir frases) que puede producirse a la hora de hacer una web multiidioma y utilizar parámetros, pero hay otros y de hecho hemos dado con otro de ellos en ese mismo ejemplo: palabras con género.

En español todos los sustantivos tienen género: masculino o femenino, y eso marca el género que deben tener los artículos y adjetivos que se utilicen con ellos. En el ejemplo anterior hemos puesto “Datos del {{entity}}”, dando por hecho que la entidad que estamos pasando va a tener género masculino. Si quisiéramos mostrar una frase que dijera “Datos de la sesión” tendríamos un problema, ya que no tenemos versión en femenino de la clave var_data.

Una solución rápida es crear una clave nueva (por ejemplo, var_data_f, de “femenino”) con el texto “Datos de la {{entity}}” y utilizarla cuando sepamos que la entidad es de género femenino. Si el objetivo es localizar la web únicamente a español e inglés no hay problema, ya que el inglés no tiene género, con lo cual definimos las claves en base a los géneros en español. Pero si hay que localizar la web a más idiomas con género, tenemos un problema.

Incluso aunque los idiomas adicionales a los que demos soporte tengan también género masculino y femenino, las palabras en esos idiomas pueden tener un género distinto al que tienen en español. En francés, por ejemplo, ratón es femenino y rata masculino (puedes ver más ejemplos de esto en este enlace). El sueco, por otro lado, tiene también 2 géneros, pero no son masculino y femenino sino común y neutro (antiguamente tenía 3: masculino, femenino y neutro). “Hombre” y “mujer” en sueco son palabras con el mismo género (“un hombre” sería en man, y “una mujer” en kvinna: las dos palabras tienen el mismo género).

Se requiere, por tanto, una solución especial ya que con lo visto hasta ahora no tenemos forma de decir que una palabra tiene un género en un idioma y otro en otro. Para solventar este problema necesitamos saber qué género tiene la palabra que estamos usando como parámetro y en base a eso elegir la frase con el género correspondiente. Este ejemplo sería una posible solución válida:

Aquellas palabras que se vayan a utilizar como parámetros se definen como objetos donde se indica el género que tienen. Los fragmentos de texto que aceptarán parámetros con género se definen con tantas versiones como géneros tenga el idioma (2 en este caso), de forma que se elija la versión correcta.

Nota: se usan “x” e “y” como géneros en vez de “m” y “f” para hacer hincapié en que esta solución serviría para idiomas con más de 2 géneros. En ese caso sería posible añadir una tercera versión del fragmento de texto “z” para un tercer género.

La versión inglesa no haría referencia alguna a géneros puesto que el idioma no los tiene:

Definiremos una pipe y servicio propios, de forma que cuando utilicemos claves nos olvidemos del género de la palabra y simplemente utilicemos la versión “base”, que será la inglesa.

Creamos un servicio adicional (por ejemplo, CustomTranslateService) que extienda de TranslateService, en el cual implementaremos la lógica que hará posible traducir de esta forma. Asimismo, creamos una pipe (CustomTranslatePipe) que haga uso de dicho servicio:

A la hora de utilizarlos, nos olvidamos por completo del género de las claves y lo haríamos tal cual lo haríamos en inglés:

El servicio detecta el género del parámetro (si el idioma lo tiene) y escoge el fragmento de texto correcto, de forma que pasando “user” como parámetro obtenemos “Nuevo usuario”, pero pasando “session” obtenemos “Nueva sesión”.

En la solución propuesta se da por hecho que solo se va a recibir un parámetro que pueda ser un objeto con género. El motivo es práctico: si abusamos de parámetros y tenemos que definir tantas versiones de un texto como posibilidades de géneros hubiese entre sus parámetros, estaríamos ante un caso clásico de ser peor el remedio que la enfermedad. El uso de parámetros es muy útil es frases cortas de un solo parámetro que se repiten por toda la aplicación, pero tampoco es algo que convenga utilizar en exceso. A más parámetros, más combinaciones posibles, y más problemas pueden surgir al adaptarlos a otros idiomas, por lo que es algo que hay que tener presente.

 

Con esto llegamos al final de esta serie de artículos sobre cómo adaptar un sitio web a múltiples idiomas con Angular. Dependiendo de los requerimientos es muy posible que ni siquiera sea necesario llegar a este nivel de complejidad. Afortunadamente la mayoría de veces solo será necesario dar soporte a inglés y español, y al ser el inglés un idioma tan sencillo (en comparación al español, al menos) no tendremos excesivos problemas a la hora de adaptar la web. Al fin y al cabo, la mejor solución suele ser la más simple.

Juan Martín