Como seguramente ya tengas claro, la traducción y la localización son conceptos relacionados pero muy distintos.

Para traducir tu aplicación basada en Web mucho me temo que no te queda más remedio que usar archivos de lenguaje de algún tipo y alguna biblioteca especializada. Sin embargo para la localización, es decir, la adaptación de la aplicación a las particularidades de cada idioma, todo lo que necesitas viene incluido con tu navegador.

El objeto Intl

Mucha gente no lo sabe, pero JavaScript dispone de un objeto global específico para ayudarnos con la localización de aplicaciones a otros idiomas y culturas: Intl:

El objeto Intl mostrado en la consola de Chrome

Podemos usar sus diferentes objetos asociados, mostrados en la figura anterior, para averiguar mucha información sobre localización en cualquier idioma.

Vamos a verlos...

Intl.Collator: para comparar cadenas de texto

El objeto Collator sirve para hacer comparaciones de cadenas teniendo en cuenta las particularidades locales.

Raramente se utiliza ya que no suele ser necesario, gracias a que la clase String tiene un método específico para llevar a cabo este tipo de comparaciones: localeCompare().

Solo lo utilizaremos si tenemos que realizar muchísimas comparaciones en un bucle o algo así (algo muy poco habitual), ya que nos daría más rendimiento. En el resto de los casos puedes hacer caso omiso de él.

Intl.DateTimeFormat: para dar formato a fechas y horas

Como su propio nombre sugiere, nos ayuda a dar formato a las fechas y las horas según las particularidades de cada país.

Como todos los objetos de Intl se instancia pasándole como argumento una cadena de texto en formato IETF BCP 47, que suena muy complicado pero en general no es más que el nombre abreviado internacional del idioma (es, en, it...) para idiomas genéricos, o lo anterior seguido de un guión y la abreviatura del país/cultura en mayúscula (es-ES, es-AR, en-US, en-UK...). Como ves, muy fácil.

Así que, por ejemplo, para obtener una fecha bien formateada en varios idiomas sólo tenemos que hacer esto:

var fecha = new Date(2019, 6, 30, 16, 30, 0);
var dtfEs = new Intl.DateTimeFormat('es-ES');
var dtfEnUs = new Intl.DateTimeFormat('en-US');
var dtfArMa = new Intl.DateTimeFormat('ar-MA');
console.log(dtfEs.format(fecha));
console.log(dtfEnUs.format(fecha));
console.log(dtfArMa.format(fecha));

que nos devolverá por consola esa fecha (30 de julio de 2019, fíjate en que los meses se numeran desde el 0) en español, inglés americano y árabe de Marruecos (que tienen un formato bien complicado):

El resultado de los formatos anteriores

Fíjate en que no nos devuelve la hora, ni tampoco hemos podido determinar el formato exacto de cada componente que queremos obtener. Eso lo controlaremos gracias a las opciones del constructor, que he omitido en el fragmento anterior.

Todos los objetos de Intl tienen un segundo argumento opcional para las opciones (valga la redundancia). En el caso de DateTimeFormat tiene un montón de propiedades posibles que no voy a detallar porque las tienes en la MDN. Pero vamos a ver un ejemplo de cómo usarlos:

var fecha = new Date(2019, 6, 30, 16, 30, 0);
var opciones = {
        weekday: 'long',
        month: 'long',
        year: 'numeric',
        hour: '2-digit',
        minute: '2-digit',
        second: '2-digit',
        timeZoneName: 'long'
    };
var dtfEs = new Intl.DateTimeFormat('es-ES', opciones);
var dtfEnUs = new Intl.DateTimeFormat('en-US', opciones);
var dtfArMa = new Intl.DateTimeFormat('ar-MA', opciones);
console.log(dtfEs.format(fecha));
console.log(dtfEnUs.format(fecha));
console.log(dtfArMa.format(fecha));

con el resultado siguiente:

El resultado del código anterior

Fíjate en que este código es exactamente igual al anterior solo que le hemos pasado como segundo argumento del constructor un objeto con las opciones pertinentes. Al haber especificado el formato en el que nos interesaba cada componente de la fecha, incluyendo las horas (para que las muestre), lo ha transformado adecuadamente y con las palabras apropiadas en cada idioma, e incluso escrito de derecha a izquierda en el caso del árabe de Marruecos.

Si queremos podemos utilizar el método formatToParts() para obtener cada una de las partes de la fecha, de modo que podamos utilizarlas en cualquier formato personalizado si lo necesitásemos (aunque no te lo recomiendo, pues para eso tienes las facilidades que da el objeto, sin recurrir a formatos propios):

El método formatToParts te lo da desglosado en una matriz

y podemos, en cada idioma, obtener exactamente cada parte de la cadena final, en función de las opciones que hayamos elegido.

Intl.RelativeTimeFormat: para facilitar la lectura de intervalos de tiempo relativos

Otra necesidad muy común en la mayor parte de las aplicaciones es la de expresar intervalos de tiempo relativos a la fecha actual. Por ejemplo, si tenemos un listado de tareas, en la columna de la fecha de vencimiento podemos poner tal cual la fecha o bien ser mucho más amigables para el usuario y escribir cosas como "Vence en 3 días" o "Caducada hace 5 horas"...

Esto es mucho más complicado de hacer de lo que parece de una manera consistente, y si además debemos hacerlo en varios idiomas no te quiero ni contar. Por suerte Intl nos ofrece también funcionalidad apropiada para lograrlo de manera sencilla.

Al igual que antes, lo único que tenemos que hacer es instanciar la clase pasándole el identificador del idioma a utilizar para la localización:

var rtf = new Intl.RelativeTimeFormat('es-ES');

Ahora podemos obtener los intervalos apropiados en ese idioma llamando al método format(), y pasándole el número de intervalos y el tipo de intervalo, que es una cadena en ingles. Por ejemplo:

rtf.format(1, 'day')    //dentro de 1 día
rtf.format(-3, 'day')   //hace 3 días
rtf.format(0, 'day')    //dentro de 0 días
rtf.format(5, 'hour')   //dentro de 5 horas

Resultado del código anterior

Esto es genial y ahorra muchas KB de bibliotecas JavaScript que ya no tendremos que descargarnos.

Además, en el constructor podemos establecer algunas opciones para especificar cómo queremos que generen esos intervalos. Por ejemplo, a mi no me gusta el estilo por defecto que tienen, usando siempre números, así que lo puedo cambiar estableciendo la propiedad numeric como 'auto':

var rtf = new Intl.RelativeTimeFormat('es-ES', { numeric: 'auto' });

y así conseguir que, por ejemplo, si es algo de hace un día ponga "ayer" y si es en un día obtenga "mañana", haciéndolo aún más natural:

Resultado del código anterior

Como vemos, de gran utilidad.

Como antes, también existe el método formatToParts() para obtener una matriz con cada uno de los fragmentos del formato por separado.

Intl.NumberFormat: para dar formato a números y dinero

Seguimos con necesidades habituales de localización, en este caso con los números. Como sabes, cada idioma tiene formatos diferentes para muchas cosas con los números y las cantidades monetarias. Por ejemplo, en España los separadores de mil son puntos y el decimal es una coma, y la moneda se pone trás la cantidad. Sin embargo en EEUU es justo al revés: los miles se separan con comas, los decimales con puntos y la moneda va delante de la cantidad.

¿Cómo gestionamos esto de manera sencilla para cualquier idioma del planeta? Antes era complicadísimo. Ahora es muy sencillo gracias a Intl.NumberFormat.

Como todos los anteriores se instancia pasándole una cadena con el idioma (si no ponemos nada se usará el idioma del sistema operativo):

var nfEs = new Intl.NumberFormat('es-ES');
var nfEn = new Intl.NumberFormat('en-EU');
var nfFr = new Intl.NumberFormat('fr');
console.log(nfEs.format(123456.78));
console.log(nfEn.format(123456.78));
console.log(nfFr.format(123456.78));

y como podemos comprobar genera los separadores en el formato adecuado a cada caso:

Resultado del código anterior

Fíjate en cómo los franceses utilizan com separador de miles un espacio, por ejemplo.

En cuanto a las opciones podemos establecer incluso el sistema de numeración que no tiene por qué sel arábigo, el tipo de moneda si va a ser una cantidad de dinero, y también la forma de nombrar las monedas, entre otras muchas opciones. La más importante es style que nos permite seleccionar si queremos mostrar decimales ('decimal', valor por defecto), monedas ('currency') o porcentajes ('percent').

Por ejemplo, para mostrar una cantidad en euros o dólares escribiríamos:

var nfEs = new Intl.NumberFormat('es-ES', {style: 'currency', currency: 'EUR'});
var nfEn = new Intl.NumberFormat('en-EU', {style: 'currency', currency: 'USD'});
var nfFr = new Intl.NumberFormat('fr', {style: 'currency', currency: 'EUR', currencyDisplay: 'name'});
console.log(nfEs.format(123456.78));
console.log(nfEn.format(123456.78));
console.log(nfFr.format(123456.78));

Fíjate en cómo adapta perfectamente el formato a cada idioma y cómo además usa el símbolo o el nombre según las opciones indicadas:

Resultado del código anterior

Intl.ListFormat: par dar formato a listas

Otra necesidad clásica en las aplicaciones: partir de una lista o array de elementos y generar una lista legible para cada idioma.

Por ejemplo, si tenemos esta matriz, que generalmente en una aplicación la habremos obtenido de un servicio remoto:

var beatles = ['John', 'Paul', 'George', 'Ringo'];

y queremos meterlos en una lista amigable para el usuario para formar la frase: 'Los Beatles eran John, Paul, George y Ringo'. Algo tan simple como esto requiere bastante trabajo si queremos adaptarlo a diversos idiomas. No todos usan las comas para separar y desde luego el último elemento no tiene que ser un "y" tampoco.

Con Intl.ListFormat la cosa es muy sencilla:

var beatles = ['John', 'Paul', 'George', 'Ringo'];
var lfEs = new Intl.ListFormat('es-ES');
var lfDe = new Intl.ListFormat('de-DE');
console.log(lfEs.format(beatles));
console.log(lfDe.format(beatles));

Como vemos nos devuelve la lista formateada para cada localización, incluyendo en este caso la palabra "y" en el idioma correspondiente:

Resultado del código anterior

Por supuesto no siempre querremos que la lista sea inclusiva, sino que a veces podemos necesitar que sea una lista de opciones y que ese "y" se convierta en un "o", por ejemplo. Para cambiar este comportamiento en las opciones del constructor tenemos la propiedad type que puede tomar los valores:

  • 'conjunction', para listas de tipo "y"
  • 'disjunction' para listas de tipo "o"
  • 'unit' si la lista es de unidades de medida, que se suelen poner en forma de lista de modo diferente.

Así, con la lista anterior podemos poner esto:

var beatles = ['John', 'Paul', 'George', 'Ringo'];
var lfEs = new Intl.ListFormat('es-ES', {type:'disjunction'});
var lfDe = new Intl.ListFormat('de-DE', {type:'disjunction'});
console.log(lfEs.format(beatles));
console.log(lfDe.format(beatles));

para tenerla de tipo "o":

Resultado del código anterior

Si fuesen unidades, por ejemplo la longitud de una viga en una aplicación de construcción pondríamos:

var medidas = ['3 metros', '12 centímetros'];
var lfEs = new Intl.ListFormat('es-ES', {type:'unit'});
var lfDe = new Intl.ListFormat('de-DE', {type:'unit'});
console.log(lfEs.format(medidas));
console.log(lfDe.format(medidas));

Resultado del código anterior

Fíjate en un detalle importante: aunque la localización ha funcionado perfectamente porque las listas tienen el formato adecuado para cada idioma, la traducción está mal ya que en alemán sigue poniendo las medidas en español. Obviamente esto no es responsabilidad de Intl ya que se trata de traducción y es responsabilidad de la aplicación. Antes de crear la lista de cadenas deberemos asegurarnos de que las medidas están en el idioma apropiado.

Hay algunos parámetros más para las opciones del constructor, pero lo importante es lo que hemos visto.

Intl.PluralRules: para pluralización

Esta ya es una característica avanzada. Al contrario que las otras clases que hemos visto no está pensada para pasarle una cadena y que nos las devuelva en plural, sino que es a más bajo nivel. Lo que hace es facilitarnos la forma de plural que corresponde a cada número que se le pase a su método select().

Por ejemplo, en español, inglés u otros idiomas occidentales una viga mide 1 metro (singular), 3 metros (plural) o, curiosamente, 0 metros (plural aunque sea cero). Sin embargo en árabe tiene otras acepciones para ciertos números.

Si lo probamos con la clase PluralRules:

var prEs = new Intl.PluralRules('es-ES');
var prMa = new Intl.PluralRules('ar-MA');
console.log('ESPAÑOL:');
console.log(prEs.select(0));
console.log(prEs.select(1));
console.log(prEs.select(3));
console.log(prEs.select(0.5));
console.log('ÁRABE:');
console.log(prMa.select(0));
console.log(prMa.select(1));
console.log(prMa.select(3));
console.log(prMa.select(0.5));

veremos lo siguiente:

Resultado del código anterior

Como puedes observar, para los idiomas occidentales generalmente hay dos posibilidades: 'one' (singular) o 'other' (plural), y con eso podemos decidir si se le pone una "s" al final o no.

Nota: lo anterior es una simplificación enorme ya que en español es mucho más complicado que eso. A veces el plural lleva "s" (gato, gatos), otras veces lleva "es" (flor, flores), en ocasiones cambia la terminación de la palabra (pez, peces) y otras veces no lleva nada (¿cuál es el plural de "virus" en español? Pues "virus" también porque es invariable en plural, al contrario que en inglés, que es "viruses"). En ese sentido otros idiomas como el inglés son mucho más sencillos.

Pero en otros idiomas la cosa es mucho más compleja, como puedes comprobar con el árabe.

Por lo tanto, aunque está bien disponer de esta funcionalidad para algunas aplicaciones muy concretas, no te va a servir de gran ayuda a la hora de generar plurales "serios", así que generalmente no lo vas a utilizar.

Soporte

El soporte actual de navegadores es ya universal desde hace años, por lo que no deberías tener problemas para usarla. La excepción, como casi siempre, es Internet Explorer, pero incluso éste tiene soporte para la mayor parte de las clases en su versión 11. En esta tabla de MDN tienes un buen resumen detallado del soporte específico por clase y navegador.

También tienes un polyfill que puedes utilizar si fuese necesario en estos navegadores antiguos, aunque no es tan potente.

En resumen

Para casi todas las tareas comunes relacionadas con la localización de aplicaciones, JavaScript nos proporciona ayuda integrada y no vamos a necesitar utilizar bibliotecas externas que añaden complejidad, peso y que además con toda seguridad no serán tan buenas como el sistema operativo para estos menesteres. Dado que la API de internacionalización de JavaScript, a través del objeto global Intl, utiliza por debajo los servicios del sistema operativo para conseguirlo, podemos garantizar resultados rápidos y correctos.

Deberíamos acostumbrarnos a usa esta API ya que nos ayudará a conectar mejor con nuestros usuarios y a hacer las aplicaciones más amigables.

Por cierto, si te ha gustado este artículo, te encantará lo que puedes aprender con mi curso de JavaScript avanzado en campusMVP. Anímate a aprender JavaScript en serio y dejar de "tocar de oído" 😊 Además tendrás vídeos prácticos, prácticas sugeridas, evaluaciones, referencias cruzadas, hitos de aprendizaje.... y tendrás contacto directo conmigo y con el fenómeno Eduard Tomàs para contestar todas tus dudas y seguir tu progreso.

¡Espero que te sea útil!

Escrito por un humano, no por una IA