Cómo formatear precios y fechas con Intl
Para pintar un precio en pantalla seguramente hayas escrito algo así
const precio = 1234.5;
`${precio.toFixed(2).replace(".", ",")} €`; // "1234,50 €"Funciona. Hasta que llega un usuario de otro país, una fecha, o un porcentaje.
Intl ya viene en el navegador y en Node, sin instalar nada, y se encarga de todo eso.
Precios y monedas
const eur = new Intl.NumberFormat("es-ES", {
style: "currency",
currency: "EUR"
});
eur.format(1234.5); // "1234,50 €"
eur.format(1234567.891); // "1.234.567,89 €"El símbolo, la posición, el separador decimal y el de millares salen del idioma. Cambia el primer argumento y cambia todo
new Intl.NumberFormat("en-US", { style: "currency", currency: "EUR" }).format(1234567.891);
// "€1,234,567.89"Fíjate en que la moneda y el idioma son cosas distintas. Puedes mostrar dólares a un español
new Intl.NumberFormat("es-ES", { style: "currency", currency: "USD" }).format(1234.5);
// "1234,50 US$"El punto de millar que no aparece
Habrás notado algo raro arriba
eur.format(1234.5); // "1234,50 €" sin punto
eur.format(12345.5); // "12.345,50 €" con puntoNo es un bug, es que en español no se agrupan los números de cuatro dígitos. Y es una regla de nuestro idioma, no de Intl. El mismo importe en alemán o en francés sí lleva separador
const precio = (locale) =>
new Intl.NumberFormat(locale, { style: "currency", currency: "EUR" }).format(1234.5);
precio("es-ES"); // "1234,50 €"
precio("it-IT"); // "1234,50 €"
precio("de-DE"); // "1.234,50 €"
precio("fr-FR"); // "1 234,50 €"
precio("en-US"); // "€1,234.50"Esto sale de CLDR, la base de datos de convenciones locales que usan los navegadores. Ahí cada idioma declara a partir de cuántos dígitos empieza a agrupar. Y es justo el tipo de detalle que nunca vas a acertar a mano.
Si aun así prefieres agrupar siempre, pídelo
new Intl.NumberFormat("es-ES", {
style: "currency",
currency: "EUR",
useGrouping: "always"
}).format(1234.5); // "1.234,50 €"Porcentajes, unidades y números compactos
El mismo objeto sirve para bastante más que dinero
const n = (opts) => new Intl.NumberFormat("es-ES", opts);
n({ style: "percent", maximumFractionDigits: 1 }).format(0.256); // "25,6 %"
n({ style: "unit", unit: "kilometer-per-hour" }).format(120); // "120 km/h"
n({ notation: "compact" }).format(1234567); // "1,2 M"En percent le pasas la fracción, no el número ya multiplicado. Le das 0.256 y escribe 25,6 %.
Fechas
Misma idea, otro constructor
const fecha = new Date("2026-09-08T14:30:00Z");
const opts = { timeZone: "Europe/Madrid" };
new Intl.DateTimeFormat("es-ES", { dateStyle: "long", ...opts }).format(fecha);
// "8 de septiembre de 2026"
new Intl.DateTimeFormat("es-ES", { dateStyle: "short", ...opts }).format(fecha);
// "8/9/26"
new Intl.DateTimeFormat("es-ES", { dateStyle: "long", timeStyle: "short", ...opts }).format(fecha);
// "8 de septiembre de 2026, 16:30"Pasa siempre la timeZone. Si no la pones, se usa la del navegador y el mismo instante se ve distinto según quién mire.
Hace 3 días
Esta es la que ahorra más código propio
const rtf = new Intl.RelativeTimeFormat("es", { numeric: "auto" });
rtf.format(-1, "day"); // "ayer"
rtf.format(-3, "day"); // "hace 3 días"
rtf.format(-5, "minute"); // "hace 5 minutos"
rtf.format(2, "week"); // "dentro de 2 semanas"Con numeric: "auto" escribe "ayer" en lugar de "hace 1 día". Con numeric: "always" no lo hace nunca.
Listas
Y para no volver a pelearte con la coma final
new Intl.ListFormat("es", { type: "conjunction" }).format(["React", "Vue", "Svelte"]);
// "React, Vue y Svelte"
new Intl.ListFormat("es", { type: "disjunction" }).format(["React", "Vue", "Svelte"]);
// "React, Vue o Svelte"Crea el formateador una sola vez
Esto importa. Construir un formateador es caro, formatear con él es barato.
// mal
lista.map(p => new Intl.NumberFormat("es-ES", { style: "currency", currency: "EUR" }).format(p));
// bien
const eur = new Intl.NumberFormat("es-ES", { style: "currency", currency: "EUR" });
lista.map(p => eur.format(p));En una prueba de 50.000 iteraciones la primera versión tardó unos 1.340 ms y la segunda 24 ms. Unas 56 veces más lento por crear el objeto dentro del bucle.
Declara tus formateadores una vez, arriba del módulo, y reutilízalos.
Intl formatea, no calcula
Un último aviso
eur.format(0.1 + 0.2); // "0,30 €"Se ve bien, pero por dentro sigue siendo 0.30000000000000004. Intl decide cómo se muestra un número, no lo arregla.
Si estás sumando importes, el problema es anterior y lo tienes en errores de coma flotante en JavaScript.
Formatea al final, en el borde de la aplicación. Nunca en medio de un cálculo.
