Logo de islavisual
Isotipo de islavisual IslaVisual
imagen de sección

Ultima revisión 16/02/2023

Formato de datos con Internacionalización

Hoy quiero hablaros de un problema que se sigue dando mucho que hablar en el desarrollo y diseño web a la hora de dar formato a números, fechas, tiempos o listas.

Por ejemplo, hay veces que necesitamos formatear un número con una longitud de ceros a la izquierda determinada. Para conseguir esta funcionalidad, bien podríamos recurrir al siguiente código:

function pad(num, padLength){
    var pad = new Array(1 + padLength).join('0');
    return (pad + num).slice(-pad.length);
}

pad(14, 5); // Devuelve: "00014"

¿Pero qué pasa si necesitamos añadir también ceros a la derecha? Pues para ello tendríamos que modificar nuestro código, el cual podría ser algo como:

function pad(num, padLength, decimals){
    var pad = new Array(1 + padLength).join('0');
    return (pad + num.toFixed(decimals)).slice(-pad.length);
}
pad(14.4, 5);    // Devuelve: "00014"
pad(14.4, 5, 2); // Devuelve: "14.40"
pad(14.4, 5, 4); // Devuelve: ".4000"

Sin embargo, como se puede ver en los comentarios de las llamadas, cuando a este código se le incluye la posibilidad de manejar decimales y el número de decimales es mayor que el valor del parámetro padLength, se producirán efectos no deseados como que nos cambie el valor o nos elimine una parte importante del valor.

Bien podríamos hacer una nueva versión del código algo más compleja que arreglase todo, pero, ¿y si ahora queremos el valor en notación de moneda o que los valores se muestren con "puntos de miles"? Pues tendríamos que complicar más el código haciendo que nuestra función contemplase todos los errores y nuevos requisitos, como el hecho de que el símbolo de la moneda vaya a la derecha o a la izquierda, dependiendo del país o moneda a manejar, o que muestre o no el valor con "puntos de miles".

Bueno, pues es en este punto donde la API de Internacionalización de ECMAScript o JavaScript nos puede ayudar y mucho. No sólo porque se vuelva más fácil de programar y de mantener, sino porque nos provee de multiples posibilidades para la comparación de cadenas, formato de números, de fechas, horas y listas con sensibilidad al lenguaje.

Veamos un ejemplo: Para este caso, podríamos hacer tan sencillo como lo siguiente:

// Sin decimales podríamos hacer una llamada como:
new Intl.NumberFormat('es-ES', { 
    minimumIntegerDigits: 5,
    minimumFractionDigits: 0,
    useGrouping: false
}).format(14) // Devuelve: "00014"

// O con 2 decimales podríamos hacer una llamada como:
new Intl.NumberFormat('es-ES', {
    minimumIntegerDigits: 5,
    minimumFractionDigits: 2,
    useGrouping: false
}).format(14) // Devuelve: "00014,00"

Como vemos, ya no tenemos que contemplar casuísticas especiales para los números, porque, Intl lo cubre casi todo. Por ejemplo, cuando establecemos el campo useGrouping a true, le estaremos indicando al sistema que se deben añadir puntos cada tres dígitos por la izquierda y el signo de separación de decimales, pero si es false, sólo pondrá el signo de sepración de decimales del país (coma en el caso de España).

¿Y qué pasa con la moneda? Pues podríamos hacer algo como:

new Intl.NumberFormat('es-ES', {
    style: "currency",
    currency: "EUR",
    minimumIntegerDigits: 5,
    minimumFractionDigits: 2,
    useGrouping: false
}).format(14) // Devuelve: "00014,00 €"

¿Y cómo hacemos todo esto usable y reutilizable ? Bueno, pues podríamos diseñar un componente personalizado como el que se muestra a continuación que nos permitiese manejar todo lo que necesitemos.

Primero definiremos el componente principal que sólo requiere de los parámetros de calendario, localización, sistema de números y zona horaria, además del valor que queramos formatear.

let format = function(value){
    format.opt = {
        calendar: "gregory",
        locale: navigator.language,
        numberingSystem: "latn",
        timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
    }
    format.value = value;
    return format;
}

Como parte automática de la configuración, podríamos incorporar el idioma del navegador como localización por defecto. Esto lo conseguimos con la propiedad language del objeto navigator. Luego, añadiríamos las funciones dependientes para formatear, números, monedas y porcentajes. El código podría ser algo como:

// Funcionalidad para manejar números
format.toNumber = function(minIntLength = 3, decimals = 2, thousandsSeparator = true){
    if(arguments.length == 1 && typeof arguments[0] == 'boolean'){
        minIntLength = 3;
        thousandsSeparator= arguments[0];
    }

    format.opt.style = "decimal";
    format.opt.minimumIntegerDigits = minIntLength;
    format.opt.minimumFractionDigits = decimals;
    format.opt.useGrouping = thousandsSeparator;

    return new Intl.NumberFormat(navigator.language, format.opt).format(format.value);
}

// Funcionalidad para manejar porcentajes
format.toPercent = function(minIntLength = 3, decimals = 2, thousandsSeparator = true){
    if(arguments.length == 1 && typeof arguments[0] == 'boolean'){
        minIntLength = 3;
        thousandsSeparator= arguments[0];
    }

    format.opt.style = "percent";
    format.opt.minimumIntegerDigits = minIntLength;
    format.opt.minimumFractionDigits = decimals;
    format.opt.useGrouping = thousandsSeparator;

    return new Intl.NumberFormat(navigator.language, format.opt).format(format.value);
}

// Funcionalidad para manejar monedas
format.toCurrency = function(minIntLength = 3, decimals = 2, currency = "EUR", thousandsSeparator = true){
    if(arguments.length == 1 && typeof arguments[0] == 'boolean'){
        minIntLength = 3;
        thousandsSeparator= arguments[0];
    }

    format.opt.style = "currency";
    format.opt.currency = currency;
    format.opt.minimumIntegerDigits = minIntLength;
    format.opt.minimumFractionDigits = decimals;
    format.opt.useGrouping = thousandsSeparator;

    return new Intl.NumberFormat(navigator.language, format.opt).format(format.value);
}

format(14).toNumber(false);  // Devuelve: "014,00"
format(14).toCurrency(2);    // Devuelve: "14,00 €"
format(14).toPercent(false); // Devuelve: "1400,00 %"

Y como queremos darle un poco de vida a este componente, lo que haremos, a continuación, es definir la funionalidad para fechas:

format.toDate = function(fmt){
    if(fmt == undefined) fmt = "DD-MM-YYYY";

    fmt = fmt.toUpperCase();

    format.opt.day = (fmt.indexOf("DD") != -1 || fmt.indexOf("D") == -1) ? "2-digit" : "numeric";
    format.opt.month = (fmt.indexOf("MM") != -1 || fmt.indexOf("M") == -1) ? "2-digit" : "numeric";
    format.opt.year = (fmt.indexOf("YYYY") != -1 || fmt.indexOf("Y") == -1) ? "numeric" : '2-digit';
    format.opt.hour = fmt.includes("HH") ? "2-digit" : (fmt.includes('H') ? "numeric" : undefined);
    format.opt.minute = fmt.includes("II") ? "2-digit" : (fmt.includes('I') ? "numeric" : undefined);
    format.opt.second = fmt.includes("SS") ? "2-digit" : (fmt.includes('S') ? "numeric" : undefined);
    format.opt.hour12 = false;

    return new Intl.DateTimeFormat(navigator.language, format.opt).format(format.value);
}
console.log(format(1675494541000).toDate(); // Devuelve: "04/02/2023");

Y para que las listas se muestren con lenguaje natural, podríamos hacer otro método como:

format.toList = function(unionType){
    if(unionType == undefined || unionType.toUpperCase() == "Y") unionType = "conjunction";
    if(unionType.toUpperCase() == "O") unionType = "disjunction";

    format.opt.style = 'long';
    format.opt.type = unionType;

    return new Intl.ListFormat(navigator.language, format.opt).format(format.value);
}

"Tengo conocimientos en " + format(['HTML', 'CSS', 'JS']).toList();
// Devuelve: "Tengo conocimientos en HTML, CSS y JS"

"Tengo conocimientos en " + format(['HTML', 'CSS', 'JS']).toList("o");
// Devuelve: "Tengo conocimientos en HTML, CSS o JS"

Con este método, si cambiamos el idioma en la propiedad locale de la función format, las conjunciones de unión también cambiarán de idioma y no tendremos que estar contemplando condicionantes específicos.

Y, además, contamos con otra ventaja más... Si la funcion format la incorporamos a otro objeto superior, como pueda ser el objeto $ que definimos en los artículos de "Mejorando el DOM, Cómo añadir funcionalidad a JavaScript sin frameworks", obtendremos una librería muy organizada, estructurada y fácil de mantener y usar.

Si deseáis más información sobre JavaScript o mejorar vuestras aplicaciones y webs con este lenguaje podéis adquirir el libro de Domine JavaScript 4ª Edición.

Esperando que os haya gustado, hasta más vernos.

Sobre el autor

Imagen de Pablo Enrique Fernández Casado
Pablo Enrique Fernández Casado

CEO de IslaVisual, Manager, Full Stack Analyst Developer y formador por cuenta ajena con más de 25 años de experiencia en el campo de la programación y más de 10 en el campo del diseño, UX, usabilidad web y accesibilidad web. También es escritor y compositor de música, además de presentar múltiples soft kills como la escucha activa, el trabajo en equipo, la creatividad, la resiliencia o la capacidad de aprendizaje, entre otras.

Especializado en proveer soluciones integrales de bajo coste y actividades de consultoría de Usabilidad, Accesibilidad y Experiencia de Usuario (UX), además de ofrecer asesoramiento en SEO, optimización de sistemas y páginas web, entre otras habilidades.