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.DateTimeFormat y escribir “hace 3 días” con Intl.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/2026 es 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 Date representa 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 es 0 y diciembre es 11. 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: Date es 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. La Z final 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 a new Date(): descompón la cadena tú mismo y usa new 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 un TypeError a propósito, para evitar comparaciones ambiguas. Usa compare() o equals().

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() lanza RangeError: Mismatching TimeZones. Convierte antes uno de ellos con withTimeZone().

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 Intl contiene 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 escribe 1.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 a toLocaleString() 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ón useGrouping: '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/timeStyle con opciones sueltas como weekday o timeZoneName: obtendrás un TypeError. Usa o los estilos o los componentes individuales (timeZone sí 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 Segmenter para 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 LOCALE a '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' })