Módulo 19: Fechas e internacionalización
Objetivos del módulo
- Crear, leer, modificar y comparar fechas con el objeto
Date, y conocer sus trampas - Entender la diferencia entre hora local, UTC, timestamps e ISO 8601
- Conocer el estado real de Temporal, la nueva API de fechas, y usarla (con polyfill cuando haga falta)
- Formatear números, monedas, porcentajes y unidades con
Intl.NumberFormat - Formatear fechas en cualquier idioma y zona con
Intl.DateTimeFormaty escribir “hace 3 días” conIntl.RelativeTimeFormat - Manejar plurales, listas, ordenación alfabética con acentos y ñ, segmentación de texto y nombres de países e idiomas
- Construir un calendario de eventos con cuenta atrás y fechas relativas
1. ¿Por qué las fechas son difíciles?
Parece sencillo: “el concierto es el 10 de octubre a las 21:00”. Pero para un programa esa frase está incompleta:
- ¿En qué zona horaria? Las 21:00 en Madrid son las 15:00 en Nueva York y las 4:00 del día siguiente en Tokio.
- Cambios de hora: en España, el último domingo de octubre el día tiene 25 horas y el último domingo de marzo, 23. “Sumar un día” y “sumar 24 horas” no siempre es lo mismo.
- Formato e idioma:
03/10/2026es 3 de octubre en España y 10 de marzo en Estados Unidos; y se escribe “sábado, 3 de octubre de 2026”, “Saturday, October 3, 2026” o “Samstag, 3. Oktober 2026”.
📘 Concepto: Hay dos tipos de “tiempo” que conviene distinguir siempre:
- Un instante exacto (el momento en que se hizo un pago): es el mismo en todo el mundo y se suele guardar en UTC.
- Una fecha u hora “de pared” (un cumpleaños, el horario de apertura de una tienda): depende del lugar y no tiene sentido convertirla de zona.
2. El objeto Date
📘 Concepto: Un
Daterepresenta un instante en el tiempo, guardado internamente como el número de milisegundos transcurridos desde el 1 de enero de 1970 a las 00:00 UTC (la llamada época Unix). Ese número es el timestamp. Todo lo demás (día, mes, hora…) se calcula a partir de él según la zona horaria del equipo.
Crear fechas
// 1. Ahora mismo
const ahora = new Date();
// 2. A partir de componentes (hora LOCAL): año, mes (¡desde 0!), día, hora, minuto, segundo
const concierto = new Date(2026, 9, 10, 21, 0); // 10 de OCTUBRE (mes 9) de 2026, 21:00
// 3. A partir de un timestamp en milisegundos
console.log(new Date(0).toISOString()); // '1970-01-01T00:00:00.000Z'
// 4. A partir de una cadena ISO 8601 (la única forma de texto fiable)
const pago = new Date('2026-10-03T16:30:00Z'); // Z = UTC
const reunion = new Date('2026-10-03T10:00:00+02:00'); // con desfase explícito
console.log(Date.now()); // timestamp actual sin crear un objeto, p. ej. 1791045000000
⚠️ Importante: En
new Date(año, mes, día)los meses empiezan en 0: enero es0y diciembre es11. Los días, en cambio, empiezan en 1. Es la trampa más famosa de JavaScript (heredada de Java en 1995).
console.log(new Date(2026, 12, 1).toDateString()); // 'Fri Jan 01 2027' (el mes 12 "desborda")
console.log(new Date(2026, 1, 30).toDateString()); // 'Mon Mar 02 2026' (30 de febrero → 2 de marzo)
console.log(new Date(2026, 2, 0).getDate()); // 28: el "día 0" de marzo es el último de febrero
Este desbordamiento automático es a veces útil (el último truco da los días de un mes) y a veces una fuente de errores silenciosos.
Getters y setters
Los ejemplos de esta sección se han ejecutado con la zona horaria Europe/Madrid:
const d = new Date(2026, 9, 3, 18, 30); // sábado 3 de octubre de 2026, 18:30 (hora local)
console.log(d.getFullYear(), d.getMonth(), d.getDate()); // 2026 9 3 ← mes 9 = octubre
console.log(d.getDay()); // 6 ← día de la semana (0 = domingo, 6 = sábado)
console.log(d.getHours(), d.getMinutes()); // 18 30
console.log(d.getTime()); // 1791045000000 (timestamp)
console.log(d.getUTCHours()); // 16 (en octubre Madrid está en UTC+2)
console.log(d.getTimezoneOffset()); // -120 (minutos que hay que SUMAR para llegar a UTC)
| Getter | Setter | Valor |
|---|---|---|
getFullYear() | setFullYear(a) | Año con 4 cifras |
getMonth() | setMonth(m) | 0-11 |
getDate() | setDate(d) | 1-31 |
getDay() | — | 0 (domingo) a 6 (sábado) |
getHours() / getMinutes() / getSeconds() / getMilliseconds() | setHours(...) / … | Hora local |
getTime() | setTime(ms) | Timestamp en ms |
getUTCFullYear(), getUTCHours()… | setUTC...() | Igual, pero en UTC |
⚠️ Importante:
Datees mutable: los setters modifican el objeto original. Si asignas una fecha a otra variable, ambas apuntan al mismo objeto.
const inicio = new Date(2026, 9, 3);
const fin = inicio; // ¡no es una copia!
fin.setDate(10);
console.log(inicio.getDate()); // 10 ← también ha cambiado
const copia = new Date(inicio); // así sí se copia
UTC frente a hora local, e ISO 8601
UTC (Tiempo Universal Coordinado) es la referencia mundial, sin cambios de hora. La hora local es la de la zona configurada en el equipo del usuario. Un mismo Date se puede mostrar de las dos formas:
const d = new Date(2026, 9, 3, 18, 30);
console.log(d.toString());
// 'Sat Oct 03 2026 18:30:00 GMT+0200 (hora de verano de Europa central)'
console.log(d.toISOString()); // '2026-10-03T16:30:00.000Z' ← siempre en UTC
console.log(JSON.stringify({ fecha: d })); // '{"fecha":"2026-10-03T16:30:00.000Z"}'
📘 Concepto: ISO 8601 es el formato estándar internacional:
AAAA-MM-DDTHH:mm:ss.sssZ. Se ordena alfabéticamente igual que cronológicamente y no es ambiguo. Úsalo siempre para guardar fechas y enviarlas a una API. LaZfinal significa UTC; también puede llevar un desfase como+02:00.
Date.parse y sus peligros
Date.parse(texto) (y new Date(texto)) devuelve el timestamp de una cadena. El problema es que solo el formato ISO está garantizado por el estándar; con cualquier otro, cada navegador hace lo que quiere:
console.log(new Date('03/10/2026').toDateString());
// 'Tue Mar 10 2026' ← lo interpreta como formato estadounidense (mes/día/año)
console.log(Date.parse('3 de octubre de 2026')); // NaN (no entiende español)
// Trampa sutil: una fecha ISO SIN hora se interpreta en UTC, pero CON hora sin zona, en local
console.log(new Date('2026-10-03').toString()); // '... 02:00:00 GMT+0200' (medianoche UTC)
console.log(new Date('2026-10-03T00:00').toString()); // '... 00:00:00 GMT+0200' (medianoche local)
// Fechas imposibles: V8 (Chrome, Node) "corrige" el 30 de febrero; Firefox devuelve Invalid Date
console.log(new Date('2026-02-30').toString()); // en Node: 'Mon Mar 02 2026 01:00:00 ...'
Una fecha no válida no lanza error: se crea un objeto “Invalid Date” cuyo getTime() es NaN.
function esFechaValida(fecha) {
return fecha instanceof Date && !Number.isNaN(fecha.getTime());
}
console.log(esFechaValida(new Date('hola'))); // false
console.log(esFechaValida(new Date('2026-10-03'))); // true
💡 Consejo: Si recibes fechas en formato español (
03/10/2026), no las pases anew Date(): descompón la cadena tú mismo y usanew Date(año, mes - 1, día), o usa Temporal (lo veremos enseguida).
Comparar y calcular diferencias
Los Date se pueden comparar con < y > (se convierten a timestamp), pero no con ===, que compara referencias:
const a = new Date(2026, 9, 3);
const b = new Date(2026, 9, 3);
console.log(a === b); // false (objetos distintos)
console.log(a.getTime() === b.getTime()); // true
console.log(a < new Date(2026, 11, 25)); // true
// Restar dos fechas da la diferencia en milisegundos
const navidad = new Date(2026, 11, 25);
const MS_POR_DIA = 24 * 60 * 60 * 1000;
console.log((navidad - a) / MS_POR_DIA); // 83.04166666666667 ← ¿83,04 días?
¿Por qué 83,04 y no 83? Porque entre el 3 de octubre y el 25 de diciembre hay un cambio de hora (25 de octubre): ese día tuvo 25 horas, y la diferencia real es de 83 días y 1 hora. Por eso, al contar días con Date, hay que redondear:
const diasEntre = (inicio, fin) => Math.round((fin - inicio) / MS_POR_DIA);
console.log(diasEntre(a, navidad)); // 83
Sumar días
function sumarDias(fecha, dias) {
const resultado = new Date(fecha); // copiamos para no mutar el original
resultado.setDate(resultado.getDate() + dias);
return resultado;
}
console.log(sumarDias(new Date(2026, 11, 30), 5).toDateString()); // 'Mon Jan 04 2027'
setDate respeta los cambios de hora. En cambio, sumar milisegundos no:
const sabado = new Date(2026, 2, 28, 12, 0); // 28 de marzo, 12:00 (al día siguiente cambia la hora)
console.log(sumarDias(sabado, 1).toString());
// 'Sun Mar 29 2026 12:00:00 GMT+0200' ✅ mismo reloj de pared
console.log(new Date(sabado.getTime() + MS_POR_DIA).toString());
// 'Sun Mar 29 2026 13:00:00 GMT+0200' ❌ ese domingo solo tuvo 23 horas
3. Los problemas de Date y por qué existe Temporal
Date se diseñó en 10 días en 1995, copiando la clase java.util.Date… que Java declaró obsoleta en 1997. Sus problemas:
Problema de Date | Consecuencia |
|---|---|
| Meses desde 0 | Errores constantes “por uno” |
| Mutable | Funciones que modifican fechas ajenas sin querer |
| Solo hora local o UTC | No puede representar “las 21:00 en Nueva York” |
| No distingue fecha sin hora | Un cumpleaños se convierte en un instante y puede “cambiar de día” al cambiar de zona |
| Parseo poco fiable | Cada navegador interpreta distinto los formatos no ISO |
| Sin aritmética de calendario | Sumar meses, contar días o calcular edades es manual y propenso a errores |
| Sin duraciones | No existe un tipo “2 horas y 30 minutos” |
Por eso durante años se usaron librerías como Moment.js (hoy en modo mantenimiento), date-fns, Luxon o Day.js. Y por eso el comité TC39 diseñó Temporal, una API nueva, completa e inmutable que sustituye a Date.
4. Temporal: la nueva API de fechas
Estado actual (octubre de 2026)
Conviene ser honestos: Temporal ya es estándar, pero todavía no está en todos los navegadores.
| Entorno | Soporte de Temporal |
|---|---|
| TC39 (estándar) | Stage 4 desde marzo de 2026: forma parte oficialmente del lenguaje. No entró en ECMAScript 2026 (cuyo contenido se cerró antes); se incorpora a la siguiente edición |
| Firefox | Sí, desde la versión 139 (mayo de 2025) |
| Chrome y Edge | Sí, desde la versión 144 (enero de 2026) |
| Safari | No en la versión estable (Safari 27 todavía no lo incluye); implementación en curso en Safari Technology Preview |
| Node.js 26 | Sí, activado por defecto |
| Node.js 24 LTS | No. Existe la opción --harmony-temporal, pero es experimental e incompleta: no la uses |
Es decir, Temporal no es Baseline todavía: si tu web tiene usuarios de iPhone o Mac, necesitas un polyfill.
Usar el polyfill
Hay dos polyfills de calidad: temporal-polyfill (de FullCalendar, muy ligero, unos 20 KB) y @js-temporal/polyfill (de los autores de la propuesta, más pesado). Usaremos el primero (npm install temporal-polyfill):
// Opción 1: instalarlo como global (usa el nativo si el navegador ya lo trae)
import 'temporal-polyfill/global';
console.log(Temporal.Now.plainDateISO().toString()); // '2026-10-03'
// Opción 2: importarlo sin tocar el global
import { Temporal } from 'temporal-polyfill';
Sin herramientas de build, puedes cargarlo solo cuando haga falta desde un CDN:
if (!('Temporal' in globalThis)) {
await import('https://cdn.jsdelivr.net/npm/temporal-polyfill@1/global.esm.js/+esm');
}
💡 Consejo: Todos los ejemplos de esta sección se han ejecutado en Node.js 24 con
import 'temporal-polyfill/global'al principio del fichero. En Chrome, Edge, Firefox y Node 26 funcionan sin polyfill.
Los tipos de Temporal
La gran idea de Temporal es tener un tipo distinto para cada necesidad, todos inmutables:
| Tipo | Representa | Ejemplo de texto |
|---|---|---|
Temporal.Instant | Un instante exacto (como un timestamp) | 2026-10-03T16:30:00Z |
Temporal.ZonedDateTime | Fecha y hora en una zona horaria concreta | 2026-10-03T18:30:00+02:00[Europe/Madrid] |
Temporal.PlainDate | Solo fecha, sin hora ni zona (cumpleaños, festivos) | 2026-10-03 |
Temporal.PlainTime | Solo hora “de pared” (horario de apertura) | 09:45:00 |
Temporal.PlainDateTime | Fecha y hora sin zona | 2026-10-03T18:30:00 |
Temporal.PlainYearMonth / PlainMonthDay | Mes de un año / día de un mes | 2026-10 / 12-25 |
Temporal.Duration | Una duración | PT2H30M (2 h 30 min) |
flowchart LR
I["Instant<br/>instante exacto"] -->|"+ zona horaria"| Z["ZonedDateTime<br/>fecha + hora + zona"]
Z -->|"toPlainDateTime()"| PDT["PlainDateTime<br/>fecha + hora"]
PDT -->|"toPlainDate()"| PD["PlainDate<br/>fecha"]
PDT -->|"toPlainTime()"| PT["PlainTime<br/>hora"]
Z -->|"toInstant()"| I
Temporal.Now: el momento actual
Temporal.Now.instant(); // instante actual
Temporal.Now.zonedDateTimeISO(); // fecha y hora actuales en la zona del sistema
Temporal.Now.plainDateISO(); // fecha de hoy, p. ej. 2026-10-03
Temporal.Now.plainTimeISO(); // hora actual
Temporal.Now.timeZoneId(); // 'Europe/Madrid'
PlainDate, PlainTime y PlainDateTime
const fecha = Temporal.PlainDate.from('2026-10-03'); // o new Temporal.PlainDate(2026, 10, 3)
console.log(fecha.year, fecha.month, fecha.day); // 2026 10 3 ← ¡aquí octubre es 10!
console.log(fecha.dayOfWeek); // 6 (lunes = 1 ... domingo = 7, norma ISO)
console.log(fecha.daysInMonth, fecha.dayOfYear, fecha.inLeapYear); // 31 276 false
// Las fechas imposibles dan error, en vez de "desbordar" en silencio
Temporal.PlainDate.from('2026-02-30'); // RangeError
// Con objetos, por defecto se ajusta al valor válido más cercano...
console.log(Temporal.PlainDate.from({ year: 2026, month: 2, day: 30 }).toString()); // '2026-02-28'
// ...salvo que pidas lo contrario
Temporal.PlainDate.from({ year: 2026, month: 2, day: 30 }, { overflow: 'reject' }); // RangeError
const hora = Temporal.PlainTime.from('09:45');
console.log(hora.add({ minutes: 30 }).toString()); // '10:15:00'
console.log(hora.add({ hours: 16 }).toString()); // '01:45:00' (da la vuelta al reloj)
const fechaHora = Temporal.PlainDateTime.from('2026-10-03T18:30');
console.log(fechaHora.add({ hours: 8 }).toString()); // '2026-10-04T02:30:00'
console.log(fechaHora.toPlainDate().toString()); // '2026-10-03'
Aritmética: add, subtract, with, until y since
Como son inmutables, todos los métodos devuelven un objeto nuevo:
const fecha = Temporal.PlainDate.from('2026-10-03');
console.log(fecha.add({ days: 30 }).toString()); // '2026-11-02'
console.log(fecha.subtract({ weeks: 2 }).toString()); // '2026-09-19'
console.log(fecha.with({ day: 1 }).toString()); // '2026-10-01' (primer día del mes)
console.log(fecha.toString()); // '2026-10-03' ← el original no cambia
// Sumar meses respeta el calendario: 31 de enero + 1 mes = 28 de febrero
console.log(Temporal.PlainDate.from('2026-01-31').add({ months: 1 }).toString()); // '2026-02-28'
// Diferencias: until (desde esta fecha HASTA otra) y since (desde otra hasta esta)
const navidad = Temporal.PlainDate.from('2026-12-25');
console.log(fecha.until(navidad).days); // 83 (¡exacto!)
console.log(fecha.until(navidad, { largestUnit: 'months' }).toString()); // 'P2M22D'
console.log(navidad.since(fecha).toString()); // 'P83D'
// Calcular una edad
const nacimiento = Temporal.PlainDate.from('1990-05-20');
console.log(nacimiento.until(fecha, { largestUnit: 'years' }).years); // 36
Comparar
const a = Temporal.PlainDate.from('2026-10-03');
const b = Temporal.PlainDate.from('2026-12-25');
console.log(a.equals(Temporal.PlainDate.from('2026-10-03'))); // true
console.log(Temporal.PlainDate.compare(a, b)); // -1 (a es anterior); 0 si iguales; 1 si posterior
// compare tiene la forma exacta que espera sort/toSorted
const festivos = ['2026-12-25', '2026-01-06', '2026-08-15'].map((t) => Temporal.PlainDate.from(t));
console.log(festivos.toSorted(Temporal.PlainDate.compare).map(String));
// ['2026-01-06', '2026-08-15', '2026-12-25']
⚠️ Importante: No compares objetos Temporal con
<o>: lanzan unTypeErrora propósito, para evitar comparaciones ambiguas. Usacompare()oequals().
Zonas horarias con ZonedDateTime
const enMadrid = Temporal.ZonedDateTime.from('2026-10-03T18:30[Europe/Madrid]');
console.log(enMadrid.toString()); // '2026-10-03T18:30:00+02:00[Europe/Madrid]'
console.log(enMadrid.offset); // '+02:00'
// El mismo instante en otras zonas
console.log(enMadrid.withTimeZone('America/New_York').toString());
// '2026-10-03T12:30:00-04:00[America/New_York]'
console.log(enMadrid.withTimeZone('Asia/Tokyo').toPlainDateTime().toString());
// '2026-10-04T01:30:00'
// De fecha "de pared" a fecha con zona
const cita = Temporal.PlainDateTime.from('2026-10-05T12:00').toZonedDateTime('America/New_York');
console.log(cita.withTimeZone('Europe/Madrid').toPlainTime().toString()); // '18:00:00'
Y lo mejor: Temporal entiende los cambios de hora:
// El 25 de octubre de 2026 España atrasa el reloj: ese día dura 25 horas
const sabado = Temporal.ZonedDateTime.from('2026-10-24T12:00[Europe/Madrid]');
console.log(sabado.add({ days: 1 }).toString()); // '2026-10-25T12:00:00+01:00[Europe/Madrid]'
console.log(sabado.add({ hours: 24 }).toString()); // '2026-10-25T11:00:00+01:00[Europe/Madrid]'
console.log(Temporal.ZonedDateTime.from('2026-10-25T00:00[Europe/Madrid]').hoursInDay); // 25
⚠️ Importante: Para calcular la diferencia en días entre dos
ZonedDateTime, ambos deben estar en la misma zona horaria; si no,until()lanzaRangeError: Mismatching TimeZones. Convierte antes uno de ellos conwithTimeZone().
Instant y la conversión con Date
const instante = Temporal.Instant.from('2026-10-03T16:30:00Z');
console.log(instante.epochMilliseconds); // 1791045000000
console.log(instante.toZonedDateTimeISO('Europe/Madrid').toString());
// '2026-10-03T18:30:00+02:00[Europe/Madrid]'
// De Date a Temporal (o fechaDate.toTemporalInstant() donde exista) y viceversa
const desdeDate = Temporal.Instant.fromEpochMilliseconds(new Date().getTime());
console.log(new Date(instante.epochMilliseconds).toISOString()); // '2026-10-03T16:30:00.000Z'
Duration
const duracion = Temporal.Duration.from({ hours: 2, minutes: 30 });
console.log(duracion.toString()); // 'PT2H30M' (formato ISO 8601 de duraciones)
console.log(duracion.total('minutes')); // 150
// Se pueden "equilibrar" las unidades
const rara = Temporal.Duration.from({ hours: 1, minutes: 90 });
console.log(rara.round({ largestUnit: 'hours' }).toString()); // 'PT2H30M'
console.log(Temporal.Duration.compare(rara, duracion)); // 0 (duran lo mismo)
console.log(duracion.toLocaleString('es-ES', { style: 'long' })); // '2 horas y 30 minutos'
| Necesito… | Tipo de Temporal |
|---|---|
| Guardar cuándo ocurrió algo (logs, pagos) | Instant |
| Un evento en un lugar concreto (vuelo, reunión internacional) | ZonedDateTime |
| Un cumpleaños, un festivo, una fecha de un formulario | PlainDate |
| Un horario (“abrimos a las 9:00”) | PlainTime |
| La duración de una película o una cuenta atrás | Duration |
5. Intl: internacionalización integrada
📘 Concepto: El objeto global
Intlcontiene formateadores que adaptan números, fechas, listas, plurales y textos al idioma y país del usuario. Usan los datos de CLDR (la base de datos de Unicode con las reglas de cientos de idiomas), así que no tienes que programar tú que en España se escribe1.234,50 €y en Estados Unidos€1,234.50.
Locales
Un locale es una etiqueta de idioma (BCP 47): 'es' (español), 'es-ES' (español de España), 'es-MX' (de México), 'en-US', 'ca' (catalán)…
El idioma preferido del usuario está en navigator.language (p. ej. 'es-ES') y la lista completa en navigator.languages. Si no pasas un locale a Intl, se usa el del sistema; en los ejemplos seremos explícitos con 'es-ES'.
💡 Consejo: Crear un formateador tiene un coste. Si vas a formatear muchos valores (una tabla, una lista), crea el formateador una vez y reutilízalo con
.format(), en lugar de llamar atoLocaleString()en cada elemento.
6. Intl.NumberFormat: números, monedas y unidades
const numero = new Intl.NumberFormat('es-ES');
console.log(numero.format(12345.678)); // '12.345,678'
console.log(numero.format(1234.5)); // '1234,5' ← ¡sin punto de miles!
⚠️ Importante: No es un error: según las normas de la RAE y de CLDR, en español los números de cuatro cifras no llevan separador de miles (
1234), pero a partir de cinco sí (12.345). Si tu diseño necesita siempre el separador, usa la opciónuseGrouping: 'always'.
Moneda
const euros = new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' });
console.log(euros.format(1234.5)); // '1234,50 €'
console.log(euros.format(99999.9)); // '99.999,90 €'
console.log(new Intl.NumberFormat('en-US', { style: 'currency', currency: 'EUR' }).format(1234.5));
// '€1,234.50'
console.log(new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'USD' }).format(99.9));
// '99,90 US$'
console.log(new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR', signDisplay: 'always' })
.format(15)); // '+15,00 €'
⚠️ Importante: Nunca construyas precios concatenando:
precio.toFixed(2) + ' €'da'1234.50 €'(con punto) y no sirve para otros idiomas. Y recuerda que los cálculos con decimales tienen errores de coma flotante (0.1 + 0.2); para dinero, trabaja en céntimos enteros y formatea al final.
Porcentajes, unidades y notación compacta
const pct = new Intl.NumberFormat('es-ES', { style: 'percent' });
console.log(pct.format(0.256)); // '26 %' (multiplica por 100 y redondea)
console.log(new Intl.NumberFormat('es-ES', { style: 'percent', maximumFractionDigits: 1 }).format(0.256));
// '25,6 %'
// Unidades
const nf = (opciones) => new Intl.NumberFormat('es-ES', opciones);
console.log(nf({ style: 'unit', unit: 'kilometer-per-hour' }).format(120)); // '120 km/h'
console.log(nf({ style: 'unit', unit: 'kilogram', unitDisplay: 'long' }).format(2.5)); // '2,5 kilogramos'
console.log(nf({ style: 'unit', unit: 'megabyte' }).format(512)); // '512 MB'
// Notación compacta (redes sociales, paneles)
console.log(nf({ notation: 'compact' }).format(1500)); // '1,5 mil'
console.log(nf({ notation: 'compact' }).format(2500000)); // '2,5 M'
console.log(nf({ notation: 'compact', compactDisplay: 'long' }).format(2500000)); // '2,5 millones'
console.log(new Intl.NumberFormat('en-US', { notation: 'compact' }).format(2500000)); // '2.5M'
// Controlar decimales
console.log(nf({ minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(3.1)); // '3,10'
| Opción | Valores habituales |
|---|---|
style | 'decimal' (por defecto), 'currency', 'percent', 'unit' |
currency | Código ISO 4217: 'EUR', 'USD', 'GBP', 'JPY'… |
unit | 'kilometer', 'celsius', 'liter', 'megabyte', 'kilometer-per-hour'… |
unitDisplay / currencyDisplay | 'short', 'long', 'narrow' / 'symbol', 'code', 'name' |
notation | 'standard', 'compact', 'scientific', 'engineering' |
minimumFractionDigits / maximumFractionDigits | Número de decimales |
signDisplay / useGrouping | 'auto', 'always', 'never', 'exceptZero' / 'auto', 'always', false |
💡 Consejo:
formatToParts()devuelve las piezas por separado (integer,decimal,fraction,currency…), útil para dar estilos distintos a cada parte, como el símbolo de moneda más pequeño.
7. Intl.DateTimeFormat: fechas y horas
Estilos predefinidos: dateStyle y timeStyle
const f = new Date(2026, 9, 3, 18, 30, 5);
const fmt = (opciones) => new Intl.DateTimeFormat('es-ES', opciones).format(f);
console.log(fmt()); // '3/10/2026'
console.log(fmt({ dateStyle: 'full' })); // 'sábado, 3 de octubre de 2026'
console.log(fmt({ dateStyle: 'long' })); // '3 de octubre de 2026'
console.log(fmt({ dateStyle: 'medium' })); // '3 oct 2026'
console.log(fmt({ dateStyle: 'short' })); // '3/10/26'
console.log(fmt({ timeStyle: 'short' })); // '18:30'
console.log(fmt({ timeStyle: 'long' })); // '18:30:05 CEST'
console.log(fmt({ dateStyle: 'full', timeStyle: 'short' }));
// 'sábado, 3 de octubre de 2026, 18:30'
Opciones a medida
Si los estilos no encajan, elige cada componente:
console.log(fmt({ weekday: 'long', day: 'numeric', month: 'long' }));
// 'sábado, 3 de octubre'
console.log(fmt({ weekday: 'short', day: '2-digit', month: 'short', year: 'numeric' }));
// 'sáb, 03 oct 2026'
console.log(fmt({ month: 'long', year: 'numeric' })); // 'octubre de 2026'
console.log(fmt({ hour: '2-digit', minute: '2-digit', hour12: true })); // '06:30 p. m.'
| Opción | Valores |
|---|---|
weekday | 'long' (sábado), 'short' (sáb), 'narrow' (S) |
year | 'numeric' (2026), '2-digit' (26) |
month | 'numeric', '2-digit', 'long' (octubre), 'short' (oct), 'narrow' |
day, hour, minute, second | 'numeric', '2-digit' |
hour12 | true / false |
timeZone | 'Europe/Madrid', 'America/New_York', 'UTC'… |
timeZoneName | 'short' (CEST, GMT+9), 'long', 'shortOffset'… |
⚠️ Importante: No puedes mezclar
dateStyle/timeStylecon opciones sueltas comoweekdayotimeZoneName: obtendrás unTypeError. Usa o los estilos o los componentes individuales (timeZonesí se puede combinar con ambos).
Zonas horarias
console.log(fmt({ timeStyle: 'short', timeZone: 'America/New_York' })); // '12:30' (18:30 de Madrid)
console.log(fmt({
day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit',
timeZone: 'Asia/Tokyo', timeZoneName: 'short',
})); // '4 oct, 01:30 GMT+9'
// Zona horaria del usuario
console.log(Intl.DateTimeFormat().resolvedOptions().timeZone); // 'Europe/Madrid'
Rangos de fechas
const inicio = new Date(2026, 9, 3);
const fin = new Date(2026, 9, 10);
console.log(new Intl.DateTimeFormat('es-ES', { dateStyle: 'medium' }).formatRange(inicio, fin));
// '3–10 oct 2026' ← no repite el mes ni el año
Con Temporal
Intl.DateTimeFormat#format() acepta objetos PlainDate, PlainDateTime, PlainTime e Instant, pero no ZonedDateTime (lanza TypeError). Para este último usa su propio toLocaleString(), que ya aplica su zona:
const cumple = Temporal.PlainDate.from('2026-10-03');
console.log(new Intl.DateTimeFormat('es-ES', { dateStyle: 'long' }).format(cumple));
// '3 de octubre de 2026'
const zdt = Temporal.ZonedDateTime.from('2026-10-03T18:30[Europe/Madrid]');
console.log(zdt.toLocaleString('es-ES', { dateStyle: 'long', timeStyle: 'short' }));
// '3 de octubre de 2026 a las 18:30'
8. Intl.RelativeTimeFormat: “hace 3 días”
const rtf = new Intl.RelativeTimeFormat('es-ES', { numeric: 'auto' });
console.log(rtf.format(-3, 'day'), '|', rtf.format(-1, 'day'), '|', rtf.format(0, 'day'));
// 'hace 3 días | ayer | hoy'
console.log(rtf.format(1, 'day'), '|', rtf.format(2, 'day'), '|', rtf.format(1, 'week'));
// 'mañana | pasado mañana | la próxima semana'
console.log(rtf.format(-2, 'hour'), '|', rtf.format(5, 'minute'), '|', rtf.format(-1, 'month'));
// 'hace 2 horas | dentro de 5 minutos | el mes pasado'
// Con numeric: 'always' (por defecto) siempre usa números
const rtfNum = new Intl.RelativeTimeFormat('es-ES');
console.log(rtfNum.format(-1, 'day')); // 'hace 1 día'
RelativeTimeFormat no calcula la diferencia: tú le das el número y la unidad. Esta función elige la unidad más adecuada a partir de dos Date:
const UNIDADES = [
['year', 365 * 24 * 3600],
['month', 30 * 24 * 3600],
['week', 7 * 24 * 3600],
['day', 24 * 3600],
['hour', 3600],
['minute', 60],
['second', 1],
];
function haceCuanto(fecha, ahora = new Date()) {
const segundos = Math.round((fecha - ahora) / 1000);
for (const [unidad, enSegundos] of UNIDADES) {
if (Math.abs(segundos) >= enSegundos || unidad === 'second') {
return rtf.format(Math.round(segundos / enSegundos), unidad);
}
}
}
const ahora = new Date('2026-10-03T18:30:00+02:00');
console.log(haceCuanto(new Date('2026-09-30T18:30:00+02:00'), ahora)); // 'hace 3 días'
console.log(haceCuanto(new Date('2026-10-03T16:00:00+02:00'), ahora)); // 'hace 2 horas'
console.log(haceCuanto(new Date('2026-10-03T18:29:20+02:00'), ahora)); // 'hace 40 segundos'
console.log(haceCuanto(new Date('2026-12-25T00:00:00+01:00'), ahora)); // 'dentro de 3 meses'
💡 Consejo: Esta versión usa meses de 30 días y años de 365, suficiente para “hace 3 días” en un comentario. En el ejemplo final verás una versión con Temporal que cuenta días de calendario (así, algo que ocurre pasado mañana a las 18:00 dirá “pasado mañana” y no “mañana”).
9. Plurales, listas, ordenación y más
Intl.PluralRules: elegir singular o plural
const pr = new Intl.PluralRules('es-ES');
console.log(pr.select(0), pr.select(1), pr.select(2)); // 'other' 'one' 'other'
const textos = { one: 'tarea pendiente', other: 'tareas pendientes' };
const pendientes = (n) => `${n} ${textos[pr.select(n)]}`;
console.log(pendientes(1)); // '1 tarea pendiente'
console.log(pendientes(5)); // '5 tareas pendientes'
console.log(pendientes(0)); // '0 tareas pendientes'
// Otros idiomas tienen más categorías: el ruso distingue 'one', 'few' y 'many'
console.log(new Intl.PluralRules('ru').select(3), new Intl.PluralRules('ru').select(5)); // 'few' 'many'
// Con { type: 'ordinal' } sirve para los ordinales ingleses: 1st, 2nd, 3rd, 4th, 21st...
Intl.ListFormat: “a, b y c”
const frutas = ['manzanas', 'peras', 'uvas'];
console.log(new Intl.ListFormat('es-ES').format(frutas));
// 'manzanas, peras y uvas'
console.log(new Intl.ListFormat('es-ES', { type: 'disjunction' }).format(frutas));
// 'manzanas, peras o uvas'
console.log(new Intl.ListFormat('en-US').format(frutas)); // 'manzanas, peras, and uvas'
// ¡Conoce la gramática! "y" pasa a "e" e "o" pasa a "u"
console.log(new Intl.ListFormat('es-ES').format(['Ana', 'Inés'])); // 'Ana e Inés'
console.log(new Intl.ListFormat('es-ES', { type: 'disjunction' }).format(['siete', 'ocho']));
// 'siete u ocho'
Intl.Collator: ordenar con acentos y ñ
sort() sin argumentos ordena por código Unicode: las mayúsculas van antes que las minúsculas, y las vocales con tilde y la ñ, al final.
const palabras = ['zorro', 'ñandú', 'Ávila', 'nube', 'árbol', 'oso', 'avión', 'Zamora', 'Oviedo'];
console.log(palabras.toSorted());
// ['Oviedo', 'Zamora', 'avión', 'nube', 'oso', 'zorro', 'Ávila', 'árbol', 'ñandú'] ❌
const collator = new Intl.Collator('es-ES');
console.log(palabras.toSorted(collator.compare));
// ['árbol', 'Ávila', 'avión', 'nube', 'ñandú', 'oso', 'Oviedo', 'Zamora', 'zorro'] ✅
// Números dentro del texto: 'archivo2' antes que 'archivo10'
const natural = new Intl.Collator('es-ES', { numeric: true });
console.log(['archivo10', 'archivo2', 'archivo1'].toSorted(natural.compare));
// ['archivo1', 'archivo2', 'archivo10']
// Comparar ignorando mayúsculas y tildes (búsquedas)
const base = new Intl.Collator('es-ES', { sensitivity: 'base' });
console.log(base.compare('canción', 'CANCION')); // 0 → se consideran iguales
// Atajo para comparaciones sueltas
console.log('ñandú'.localeCompare('oso', 'es-ES')); // -1 (ñ va antes que o)
Otras opciones útiles: sensitivity: 'accent' (a = A, pero a ≠ á) y caseFirst: 'upper' (mayúsculas primero en caso de empate).
Intl.Segmenter: dividir texto correctamente
length cuenta unidades UTF-16, no “caracteres que ve el usuario”. Con emojis compuestos o letras con diacríticos combinados, el resultado engaña:
const texto = '👨👩👧 ¡Hola!';
console.log(texto.length); // 15
console.log([...texto].length); // 12 (puntos de código)
const grafemas = new Intl.Segmenter('es-ES', { granularity: 'grapheme' });
console.log([...grafemas.segment(texto)].length); // 8 ← lo que ve el usuario
// Dividir en palabras (sin signos de puntuación)
const palabrasSeg = new Intl.Segmenter('es-ES', { granularity: 'word' });
const frase = '¡Hola, mundo! ¿Qué tal estás?';
console.log([...palabrasSeg.segment(frase)].filter((s) => s.isWordLike).map((s) => s.segment));
// ['Hola', 'mundo', 'Qué', 'tal', 'estás']
// Con granularity: 'sentence' divide en frases: ['Hola. ', '¿Qué tal? ', 'Bien, gracias.']
💡 Consejo: Usa
Segmenterpara contadores de caracteres (como el de una red social), para truncar textos sin partir un emoji por la mitad o para contar palabras en idiomas sin espacios, como el japonés o el chino.
Intl.DisplayNames: nombres de países, idiomas y monedas
const regiones = new Intl.DisplayNames(['es-ES'], { type: 'region' });
console.log(regiones.of('JP'), regiones.of('US')); // 'Japón' 'Estados Unidos'
const idiomas = new Intl.DisplayNames(['es-ES'], { type: 'language' });
console.log(idiomas.of('en-GB'), idiomas.of('ca')); // 'inglés británico' 'catalán'
const monedas = new Intl.DisplayNames(['es-ES'], { type: 'currency' });
console.log(monedas.of('JPY')); // 'yen japonés'
Muy útil para un selector de país o de idioma sin mantener listas traducidas a mano. Combinado con Intl.supportedValuesOf('currency') o Intl.supportedValuesOf('timeZone'), obtienes listas completas.
Intl.DurationFormat
El formateador más reciente (en todos los navegadores principales desde 2025) da formato a duraciones, ya sean objetos simples o Temporal.Duration:
const duracion = { hours: 2, minutes: 30 };
console.log(new Intl.DurationFormat('es-ES', { style: 'long' }).format(duracion)); // '2 horas y 30 minutos'
console.log(new Intl.DurationFormat('es-ES', { style: 'short' }).format(duracion)); // '2 h y 30 min'
console.log(new Intl.DurationFormat('es-ES', { style: 'digital' }).format({ ...duracion, seconds: 5 })); // '2:30:05'
toLocaleString: el atajo
Números y fechas tienen métodos que crean un formateador por debajo:
const f = new Date(2026, 9, 3, 18, 30, 5);
console.log((1234.5).toLocaleString('es-ES')); // '1234,5'
console.log((0.256).toLocaleString('es-ES', { style: 'percent' })); // '26 %'
console.log(f.toLocaleDateString('es-ES'), f.toLocaleTimeString('es-ES')); // '3/10/2026' '18:30:05'
console.log(f.toLocaleString('es-ES')); // '3/10/2026, 18:30:05'
console.log(f.toLocaleDateString('es-ES', { weekday: 'long', day: 'numeric', month: 'long' }));
// 'sábado, 3 de octubre'
10. Cuándo usar cada herramienta
| Necesidad | Herramienta |
|---|---|
| Timestamp para medir o guardar | Date.now() o Temporal.Now.instant() |
| Fecha sin hora (cumpleaños, festivo) | Temporal.PlainDate |
| Evento en una zona horaria concreta | Temporal.ZonedDateTime |
| Sumar días/meses, diferencias exactas | Temporal (add, until, since) |
Código antiguo o librerías que piden Date | Date (con cuidado) |
| Enviar o guardar fechas en una API | Cadena ISO 8601 (toISOString(), toString() de Temporal) |
| Mostrar una fecha u hora | Intl.DateTimeFormat / toLocaleString() |
| “Hace 3 días”, “mañana” | Intl.RelativeTimeFormat |
| Mostrar una duración | Intl.DurationFormat |
| Precios, porcentajes, unidades, “1,5 mil” | Intl.NumberFormat |
| “1 tarea” / “2 tareas” y “Ana, Luis y Marta” | Intl.PluralRules e Intl.ListFormat |
| Ordenar o comparar texto | Intl.Collator / localeCompare |
| Contar caracteres o palabras | Intl.Segmenter |
| Nombres de países, idiomas, monedas | Intl.DisplayNames |
11. Ejemplo completo: calendario de eventos
Vamos a construir una página que ordena los eventos por fecha, separa los próximos de los pasados, muestra cada fecha en la zona horaria del usuario (aunque el evento se definiera en otra), indica el precio en euros (“Gratis” si cuesta 0), añade una fecha relativa (“pasado mañana”) con una cuenta atrás que se actualiza cada segundo, y resume “3 eventos próximos: A, B y C” con plurales y listas correctos.
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Calendario de eventos</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 42rem; margin: 2rem auto; padding: 0 1rem; }
.evento { border: 1px solid #ccc; border-radius: .5rem; padding: 1rem; margin-block: .75rem; }
.pasado { opacity: .6; } .relativo { font-weight: bold; }
</style>
</head>
<body>
<h1>Próximos eventos</h1>
<p id="resumen"></p>
<div id="eventos"></div>
<h2>Eventos pasados</h2>
<div id="pasados"></div>
<script type="module">
// 1. Polyfill solo si el navegador no trae Temporal (p. ej., Safari)
if (!('Temporal' in globalThis)) {
await import('https://cdn.jsdelivr.net/npm/temporal-polyfill@1/global.esm.js/+esm');
}
const LOCALE = 'es-ES';
const MI_ZONA = Temporal.Now.timeZoneId(); // zona del usuario
// 2. Datos: cada evento se define en la zona horaria donde ocurre (en una app real, vendrían de una API)
const eventos = [
{ titulo: 'Concierto en Madrid', inicio: '2026-10-10T21:00', zona: 'Europe/Madrid', precio: 45 },
{ titulo: 'Webinar desde Nueva York', inicio: '2026-10-05T12:00', zona: 'America/New_York', precio: 0 },
{ titulo: 'Cena de Nochebuena', inicio: '2026-12-24T21:30', zona: 'Europe/Madrid', precio: 30 },
{ titulo: 'Taller de JavaScript', inicio: '2026-09-28T17:00', zona: 'Europe/Madrid', precio: 1250 },
];
// 3. Formateadores (se crean UNA vez y se reutilizan)
const fmtPrecio = new Intl.NumberFormat(LOCALE, { style: 'currency', currency: 'EUR' });
const fmtDuracion = new Intl.DurationFormat(LOCALE, { style: 'narrow' });
const rtf = new Intl.RelativeTimeFormat(LOCALE, { numeric: 'auto' });
const plural = new Intl.PluralRules(LOCALE);
const lista = new Intl.ListFormat(LOCALE);
// 4. Fecha relativa que cuenta días de calendario
function relativo(fecha, ahora) {
const segundos = ahora.until(fecha).total('seconds');
const abs = Math.abs(segundos);
if (abs < 60) return rtf.format(Math.round(segundos), 'second');
if (abs < 3600) return rtf.format(Math.round(segundos / 60), 'minute');
if (abs < 86_400) return rtf.format(Math.round(segundos / 3600), 'hour');
const hoy = ahora.toPlainDate();
const dia = fecha.toPlainDate();
const dias = hoy.until(dia).days;
if (Math.abs(dias) < 7) return rtf.format(dias, 'day');
if (Math.abs(dias) < 30) return rtf.format(Math.round(dias / 7), 'week');
const { years, months } = hoy.until(dia, { largestUnit: 'years' });
return years === 0 ? rtf.format(months, 'month') : rtf.format(years, 'year');
}
// 5. Prepara cada evento para mostrarlo
function preparar(evento, ahora) {
const inicio = Temporal.PlainDateTime.from(evento.inicio)
.toZonedDateTime(evento.zona) // la hora en el lugar del evento...
.withTimeZone(MI_ZONA); // ...convertida a la zona del usuario
return {
...evento,
inicio,
pasado: Temporal.ZonedDateTime.compare(inicio, ahora) < 0,
fechaTexto: inicio.toLocaleString(LOCALE, { dateStyle: 'full', timeStyle: 'short' }),
precioTexto: evento.precio === 0 ? 'Gratis' : fmtPrecio.format(evento.precio),
};
}
function tarjeta(evento, ahora) {
const articulo = document.createElement('article');
articulo.className = `evento${evento.pasado ? ' pasado' : ''}`;
articulo.innerHTML = `<h3></h3><p><time></time> · <span class="precio"></span></p>
<p><span class="relativo"></span> <span class="cuenta"></span></p>`;
// Los datos, siempre con textContent (nunca innerHTML con datos: XSS)
articulo.querySelector('h3').textContent = evento.titulo;
const time = articulo.querySelector('time');
time.textContent = evento.fechaTexto;
time.dateTime = evento.inicio.toInstant().toString();
articulo.querySelector('.precio').textContent = evento.precioTexto;
actualizarCuenta(articulo, evento, ahora);
return articulo;
}
function actualizarCuenta(articulo, evento, ahora) {
articulo.querySelector('.relativo').textContent = relativo(evento.inicio, ahora);
if (!evento.pasado) {
const falta = ahora.until(evento.inicio, { largestUnit: 'days', smallestUnit: 'seconds' });
articulo.querySelector('.cuenta').textContent = `(faltan ${fmtDuracion.format(falta)})`;
}
}
// 6. Pintar
const ahora = Temporal.Now.zonedDateTimeISO(MI_ZONA);
const preparados = eventos
.map((e) => preparar(e, ahora))
.toSorted((a, b) => Temporal.ZonedDateTime.compare(a.inicio, b.inicio));
const proximos = preparados.filter((e) => !e.pasado);
const pasados = preparados.filter((e) => e.pasado).toReversed();
const palabra = plural.select(proximos.length) === 'one' ? 'evento próximo' : 'eventos próximos';
document.querySelector('#resumen').textContent =
`${proximos.length} ${palabra}: ${lista.format(proximos.map((e) => e.titulo))}.`;
const tarjetasProximas = proximos.map((e) => [tarjeta(e, ahora), e]);
document.querySelector('#eventos').append(...tarjetasProximas.map(([t]) => t));
document.querySelector('#pasados').append(...pasados.map((e) => tarjeta(e, ahora)));
// 7. Cuenta atrás: actualizamos cada segundo
setInterval(() => {
const instante = Temporal.Now.zonedDateTimeISO(MI_ZONA);
for (const [articulo, evento] of tarjetasProximas) actualizarCuenta(articulo, evento, instante);
}, 1000);
</script>
</body>
</html>
Si abres la página el 3 de octubre de 2026 a las 18:30 en Madrid, verás algo así:
3 eventos próximos: Webinar desde Nueva York, Concierto en Madrid y Cena de Nochebuena.
Webinar desde Nueva York
lunes, 5 de octubre de 2026, 18:00 · Gratis
pasado mañana (faltan 1d 23h 30min)
Concierto en Madrid
sábado, 10 de octubre de 2026, 21:00 · 45,00 €
la próxima semana (faltan 7d 2h 30min)
Cena de Nochebuena
jueves, 24 de diciembre de 2026, 21:30 · 30,00 €
dentro de 2 meses (faltan 82d 3h)
Eventos pasados
Taller de JavaScript
lunes, 28 de septiembre de 2026, 17:00 · 1250,00 €
hace 5 días
Fíjate en que el webinar, definido a las 12:00 de Nueva York, aparece a las 18:00 para un usuario de Madrid, y en que 1250,00 € no lleva punto de miles (regla de las cuatro cifras).
✏️ Ejercicio: Cambia
LOCALEa'en-US'y la zona del usuario a'America/Mexico_City'. Sin tocar nada más, todos los textos (fechas, precios, “next week”, la lista con “and”) se adaptarán.
12. Ejercicios
Ejercicio 1: ¿Cuántos días faltan?
Escribe diasHasta(fechaISO) que devuelva cuántos días de calendario faltan desde hoy hasta una fecha AAAA-MM-DD, primero con Date (cuidado con el redondeo y los cambios de hora) y después con Temporal.PlainDate. Comprueba ambas con una fecha posterior al último domingo de octubre.
Ejercicio 2: Parsear fechas españolas
Crea parsearFechaES('03/10/2026') que devuelva un Temporal.PlainDate (o un Date local) y lance un RangeError claro si la fecha no existe (por ejemplo, 31/02/2026). Escribe al menos cinco casos de prueba.
Ejercicio 3: Calculadora de edad
Haz un formulario con un <input type="date"> que muestre la edad exacta en años, meses y días (“36 años, 4 meses y 13 días”) usando until con largestUnit: 'years' e Intl.DurationFormat. Valida que la fecha no sea futura.
Ejercicio 4: Reloj mundial
Muestra la hora actual en Madrid, Londres, Nueva York, Ciudad de México, Tokio y Sídney, actualizada cada segundo, con el nombre de la ciudad, la fecha si es distinta a la local y la diferencia horaria (+7 h). Usa Intl.DateTimeFormat con timeZone.
Ejercicio 5: Ticket de compra
Dado un array de productos { nombre, precioCentimos, cantidad }, genera un ticket con los precios en euros, el IVA del 21 % calculado en céntimos enteros, el total y una línea “3 productos: pan, leche y huevos” usando NumberFormat, PluralRules y ListFormat.
Ejercicio 6: Agenda de contactos ordenada
Ordena 20 nombres españoles con tildes, ñ y mayúsculas (Íñigo, Ángela, Óscar…) con Intl.Collator, agrúpalos por letra inicial (con la Ñ como letra propia) y añade un buscador que ignore mayúsculas y tildes.
Ejercicio 7: Contador de caracteres para redes sociales
Crea un <textarea> con un contador “quedan N caracteres” (máximo 280) que cuente grafemas con Intl.Segmenter (👨👩👧 cuenta como 1) y muestre el número de palabras.
Resumen
| Concepto | Sintaxis |
|---|---|
| Date: crear | new Date(), Date.now(), new Date(2026, 9, 3) (¡mes desde 0!) |
| ISO 8601 / fecha inválida | fecha.toISOString() / Number.isNaN(fecha.getTime()) |
| Date: días entre / sumar días | Math.round((b - a) / 86_400_000) / copia.setDate(copia.getDate() + n) |
| Polyfill de Temporal | import 'temporal-polyfill/global' |
| Hoy / ahora | Temporal.Now.plainDateISO(), Temporal.Now.zonedDateTimeISO() |
| Fecha sin hora / con zona | PlainDate.from('2026-10-03') / ZonedDateTime.from('...[Europe/Madrid]') |
| Aritmética y diferencias | .add({ days: 1 }), .with({ day: 1 }), a.until(b, { largestUnit: 'months' }) |
| Comparar / cambiar de zona | Temporal.PlainDate.compare(a, b) / zdt.withTimeZone('Asia/Tokyo') |
| Moneda | new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' }) |
| Fecha legible | new Intl.DateTimeFormat('es-ES', { dateStyle: 'full' }) |
| Relativo | new Intl.RelativeTimeFormat('es-ES', { numeric: 'auto' }).format(-1, 'day') |
| Plural | new Intl.PluralRules('es-ES').select(n) |
| Lista | new Intl.ListFormat('es-ES').format(arr) |
| Ordenar texto | arr.toSorted(new Intl.Collator('es-ES').compare) |
| Grafemas / palabras | new Intl.Segmenter('es-ES', { granularity: 'grapheme' }) |
| Nombres de países | new Intl.DisplayNames(['es-ES'], { type: 'region' }).of('JP') |
| Duraciones | new Intl.DurationFormat('es-ES', { style: 'long' }) |