API Intl.RelativeTimeFormat

Formatea cadenas de tiempo relativo en JavaScript con soporte completo de internacionalización

Introducción

Mostrar marcas de tiempo relativas como "hace 3 horas" o "en 2 días" es un requisito común en aplicaciones web. Los feeds de redes sociales, secciones de comentarios, sistemas de notificaciones y registros de actividad necesitan mostrar tiempos legibles para humanos que se actualicen a medida que el contenido envejece.

Construir esta funcionalidad desde cero presenta desafíos. Necesitas calcular diferencias de tiempo, seleccionar unidades apropiadas, manejar reglas de pluralización en diferentes idiomas y mantener traducciones para cada configuración regional compatible. Esta complejidad explica por qué los desarrolladores tradicionalmente recurrían a bibliotecas como Moment.js, añadiendo un tamaño de paquete significativo para lo que parece un formateo simple.

La API Intl.RelativeTimeFormat proporciona una solución nativa. Formatea cadenas de tiempo relativo con soporte completo de internacionalización, manejando reglas de pluralización y convenciones culturales automáticamente. La API funciona en todos los navegadores principales con una cobertura global del 95 %, eliminando la necesidad de dependencias externas mientras produce resultados de sonido natural en docenas de idiomas.

Uso básico

El constructor Intl.RelativeTimeFormat crea una instancia de formateador que convierte valores numéricos y unidades de tiempo en cadenas localizadas.

const rtf = new Intl.RelativeTimeFormat('en');

console.log(rtf.format(-1, 'day'));
// "1 day ago"

console.log(rtf.format(2, 'hour'));
// "in 2 hours"

console.log(rtf.format(-3, 'month'));
// "3 months ago"

El método format() toma dos parámetros:

  • value: Un número que indica la cantidad de tiempo
  • unit: Una cadena que especifica la unidad de tiempo

Los valores negativos indican tiempos pasados, los valores positivos indican tiempos futuros. La API maneja automáticamente la pluralización, produciendo "hace 1 día" o "hace 2 días" según el valor.

Unidades de tiempo compatibles

La API admite ocho unidades de tiempo, cada una aceptando formas tanto singulares como plurales:

const rtf = new Intl.RelativeTimeFormat('en');

// These produce identical output
console.log(rtf.format(-5, 'second'));
// "5 seconds ago"

console.log(rtf.format(-5, 'seconds'));
// "5 seconds ago"

Unidades disponibles de menor a mayor:

  • second o seconds
  • minute o minutes
  • hour o hours
  • day o days
  • week o weeks
  • month o months
  • quarter o quarters
  • year o years

La unidad de trimestre resulta útil en aplicaciones empresariales que rastrean períodos fiscales, mientras que las otras cubren las necesidades típicas de formateo de tiempo relativo.

Salida en lenguaje natural

La opción numeric controla si el formateador utiliza valores numéricos o alternativas en lenguaje natural.

const rtfNumeric = new Intl.RelativeTimeFormat('en', {
  numeric: 'always'
});

console.log(rtfNumeric.format(-1, 'day'));
// "1 day ago"

console.log(rtfNumeric.format(0, 'day'));
// "in 0 days"

console.log(rtfNumeric.format(1, 'day'));
// "in 1 day"

Establecer numeric en auto produce frases más idiomáticas para valores comunes:

const rtfAuto = new Intl.RelativeTimeFormat('en', {
  numeric: 'auto'
});

console.log(rtfAuto.format(-1, 'day'));
// "yesterday"

console.log(rtfAuto.format(0, 'day'));
// "today"

console.log(rtfAuto.format(1, 'day'));
// "tomorrow"

Esta salida en lenguaje natural crea interfaces más conversacionales. La opción auto funciona en todas las unidades de tiempo, aunque el efecto es más notable con los días. Otros idiomas tienen sus propias alternativas idiomáticas que la API maneja automáticamente.

Estilos de formateo

La opción style ajusta la verbosidad de la salida para diferentes contextos de interfaz:

const rtfLong = new Intl.RelativeTimeFormat('en', {
  style: 'long'
});

console.log(rtfLong.format(-2, 'hour'));
// "2 hours ago"

const rtfShort = new Intl.RelativeTimeFormat('en', {
  style: 'short'
});

console.log(rtfShort.format(-2, 'hour'));
// "2 hr. ago"

const rtfNarrow = new Intl.RelativeTimeFormat('en', {
  style: 'narrow'
});

console.log(rtfNarrow.format(-2, 'hour'));
// "2h ago"

Utiliza el estilo long (el predeterminado) para interfaces estándar donde la legibilidad es lo más importante. Utiliza el estilo short para diseños con espacio limitado como interfaces móviles o tablas de datos. Utiliza el estilo narrow para pantallas extremadamente compactas donde cada carácter cuenta.

Cálculo de diferencias de tiempo

La API Intl.RelativeTimeFormat formatea valores pero no los calcula. Debes calcular las diferencias de tiempo y seleccionar las unidades apropiadas tú mismo. Esta separación de responsabilidades te da control sobre la lógica de cálculo mientras delegas la complejidad del formateo a la API.

Cálculo básico de diferencia de tiempo

Para una unidad de tiempo específica, calcula la diferencia entre dos fechas:

const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });

function formatDaysAgo(date) {
  const now = new Date();
  const diffInMs = date - now;
  const diffInDays = Math.round(diffInMs / (1000 * 60 * 60 * 24));

  return rtf.format(diffInDays, 'day');
}

const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);

console.log(formatDaysAgo(yesterday));
// "yesterday"

const nextWeek = new Date();
nextWeek.setDate(nextWeek.getDate() + 7);

console.log(formatDaysAgo(nextWeek));
// "in 7 days"

Este enfoque funciona cuando sabes qué unidad tiene sentido para tu caso de uso. Las marcas de tiempo de comentarios podrían usar siempre horas o días, mientras que la programación de eventos podría centrarse en días o semanas.

Selección automática de unidad

Para formateo de tiempo relativo de propósito general, selecciona la unidad más apropiada según la magnitud de la diferencia de tiempo:

const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });

const units = {
  year: 24 * 60 * 60 * 1000 * 365,
  month: 24 * 60 * 60 * 1000 * 365 / 12,
  week: 24 * 60 * 60 * 1000 * 7,
  day: 24 * 60 * 60 * 1000,
  hour: 60 * 60 * 1000,
  minute: 60 * 1000,
  second: 1000
};

function formatRelativeTime(date) {
  const now = new Date();
  const diffInMs = date - now;
  const absDiff = Math.abs(diffInMs);

  for (const [unit, msValue] of Object.entries(units)) {
    if (absDiff >= msValue || unit === 'second') {
      const value = Math.round(diffInMs / msValue);
      return rtf.format(value, unit);
    }
  }
}

const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
console.log(formatRelativeTime(fiveMinutesAgo));
// "5 minutes ago"

const threeDaysAgo = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000);
console.log(formatRelativeTime(threeDaysAgo));
// "3 days ago"

Esta implementación itera a través de las unidades de mayor a menor, seleccionando la primera unidad donde la diferencia de tiempo supera el valor en milisegundos de la unidad. El respaldo a segundos asegura que la función siempre devuelva un resultado.

Los umbrales de unidad utilizan valores aproximados. Los meses se calculan como 1/12 de un año en lugar de tener en cuenta las longitudes variables de los meses. Esta aproximación funciona bien para visualizaciones de tiempo relativo donde la precisión importa menos que la legibilidad.

Soporte de internacionalización

El formateador respeta las convenciones específicas de cada configuración regional para la visualización de tiempo relativo. Diferentes idiomas tienen diferentes reglas de pluralización, diferentes órdenes de palabras y diferentes expresiones idiomáticas.

const rtfEnglish = new Intl.RelativeTimeFormat('en', {
  numeric: 'auto'
});

console.log(rtfEnglish.format(-1, 'day'));
// "yesterday"

const rtfSpanish = new Intl.RelativeTimeFormat('es', {
  numeric: 'auto'
});

console.log(rtfSpanish.format(-1, 'day'));
// "ayer"

const rtfJapanese = new Intl.RelativeTimeFormat('ja', {
  numeric: 'auto'
});

console.log(rtfJapanese.format(-1, 'day'));
// "昨日"

Las reglas de pluralización varían significativamente entre idiomas. El inglés distingue entre uno y muchos (1 day vs 2 days). El árabe tiene seis formas plurales dependiendo del recuento. El japonés usa la misma forma independientemente de la cantidad. La API maneja estas complejidades automáticamente.

const rtfArabic = new Intl.RelativeTimeFormat('ar');

console.log(rtfArabic.format(-1, 'day'));
// "قبل يوم واحد"

console.log(rtfArabic.format(-2, 'day'));
// "قبل يومين"

console.log(rtfArabic.format(-3, 'day'));
// "قبل 3 أيام"

console.log(rtfArabic.format(-11, 'day'));
// "قبل 11 يومًا"

El formateador también maneja la dirección del texto para idiomas de derecha a izquierda y aplica convenciones de formato culturalmente apropiadas. Esta localización automática elimina la necesidad de mantener archivos de traducción o implementar lógica de pluralización personalizada.

Formato avanzado con formatToParts

El método formatToParts() devuelve la cadena formateada como un array de objetos, permitiendo el estilo personalizado o la manipulación de componentes individuales.

const rtf = new Intl.RelativeTimeFormat('en');

const parts = rtf.formatToParts(-5, 'second');

console.log(parts);
// [
//   { type: 'integer', value: '5', unit: 'second' },
//   { type: 'literal', value: ' seconds ago' }
// ]

Cada objeto de parte contiene:

  • type: Ya sea integer para valores numéricos o literal para texto
  • value: El contenido de cadena de esta parte
  • unit: La unidad de tiempo (presente en partes enteras)

Esta estructura permite un renderizado personalizado donde es posible que desees aplicar estilos diferentes a los números que al texto, o extraer componentes específicos para su visualización:

const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });

function formatWithStyledNumber(value, unit) {
  const parts = rtf.formatToParts(value, unit);

  return parts.map(part => {
    if (part.type === 'integer') {
      return `<strong>${part.value}</strong>`;
    }
    return part.value;
  }).join('');
}

console.log(formatWithStyledNumber(-5, 'hour'));
// "<strong>5</strong> hours ago"

Al usar numeric: 'auto' con valores que tienen alternativas en lenguaje natural, formatToParts() devuelve una única parte literal:

const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });

const parts = rtf.formatToParts(-1, 'day');

console.log(parts);
// [
//   { type: 'literal', value: 'yesterday' }
// ]

Este comportamiento te permite detectar cuándo se usa lenguaje natural versus formato numérico, permitiéndote aplicar diferentes estilos o comportamientos según el tipo de salida.

Optimización del rendimiento

Crear instancias de Intl.RelativeTimeFormat implica cargar datos de configuración regional e inicializar reglas de formato. Esta operación es lo suficientemente costosa como para evitar repetirla innecesariamente.

Almacenar en caché las instancias de formateador

Crea los formateadores una vez y reutilízalos:

const formatterCache = new Map();

function getFormatter(locale, options = {}) {
  const cacheKey = `${locale}-${JSON.stringify(options)}`;

  if (!formatterCache.has(cacheKey)) {
    formatterCache.set(
      cacheKey,
      new Intl.RelativeTimeFormat(locale, options)
    );
  }

  return formatterCache.get(cacheKey);
}

// Reuse cached formatters
const rtf = getFormatter('en', { numeric: 'auto' });
console.log(rtf.format(-1, 'day'));
// "yesterday"

Esta estrategia de almacenamiento en caché se vuelve importante al formatear muchas marcas de tiempo, como al renderizar feeds de actividad o hilos de comentarios.

Minimizar la sobrecarga de cálculo

Almacena las marcas de tiempo en lugar de calcular los tiempos relativos repetidamente:

// Store the creation date
const comment = {
  text: "Great article!",
  createdAt: new Date('2025-10-14T10:30:00Z')
};

// Calculate relative time only when rendering
function renderComment(comment, locale) {
  const rtf = getFormatter(locale, { numeric: 'auto' });
  const units = {
    day: 24 * 60 * 60 * 1000,
    hour: 60 * 60 * 1000,
    minute: 60 * 1000,
    second: 1000
  };

  const diffInMs = comment.createdAt - new Date();
  const absDiff = Math.abs(diffInMs);

  for (const [unit, msValue] of Object.entries(units)) {
    if (absDiff >= msValue || unit === 'second') {
      const value = Math.round(diffInMs / msValue);
      return rtf.format(value, unit);
    }
  }
}

Este enfoque separa el almacenamiento de datos de la presentación, lo que te permite recalcular los tiempos relativos cuando cambia la configuración regional del usuario o cuando se actualizan las renderizaciones sin modificar los datos subyacentes.

Implementación práctica

Combinar la lógica de cálculo con el formato produce una función de utilidad reutilizable adecuada para aplicaciones en producción:

class RelativeTimeFormatter {
  constructor(locale = 'en', options = { numeric: 'auto' }) {
    this.formatter = new Intl.RelativeTimeFormat(locale, options);

    this.units = [
      { name: 'year', ms: 24 * 60 * 60 * 1000 * 365 },
      { name: 'month', ms: 24 * 60 * 60 * 1000 * 365 / 12 },
      { name: 'week', ms: 24 * 60 * 60 * 1000 * 7 },
      { name: 'day', ms: 24 * 60 * 60 * 1000 },
      { name: 'hour', ms: 60 * 60 * 1000 },
      { name: 'minute', ms: 60 * 1000 },
      { name: 'second', ms: 1000 }
    ];
  }

  format(date) {
    const now = new Date();
    const diffInMs = date - now;
    const absDiff = Math.abs(diffInMs);

    for (const unit of this.units) {
      if (absDiff >= unit.ms || unit.name === 'second') {
        const value = Math.round(diffInMs / unit.ms);
        return this.formatter.format(value, unit.name);
      }
    }
  }
}

// Usage
const formatter = new RelativeTimeFormatter('en');

const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
console.log(formatter.format(fiveMinutesAgo));
// "5 minutes ago"

const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);
console.log(formatter.format(tomorrow));
// "tomorrow"

Esta clase encapsula tanto el formateador como la lógica de selección de unidades, proporcionando una interfaz limpia que acepta objetos Date y devuelve cadenas formateadas.

Integración con frameworks

En aplicaciones React, crea el formateador una vez y pásalo a través del contexto o props:

import { createContext, useContext } from 'react';

const RelativeTimeContext = createContext(null);

export function RelativeTimeProvider({ locale, children }) {
  const formatter = new RelativeTimeFormatter(locale);

  return (
    <RelativeTimeContext.Provider value={formatter}>
      {children}
    </RelativeTimeContext.Provider>
  );
}

export function useRelativeTime() {
  const formatter = useContext(RelativeTimeContext);
  if (!formatter) {
    throw new Error('useRelativeTime must be used within RelativeTimeProvider');
  }
  return formatter;
}

// Component usage
function CommentTimestamp({ date }) {
  const formatter = useRelativeTime();
  return <time>{formatter.format(date)}</time>;
}

Este patrón garantiza que los formateadores se creen una vez por configuración regional y se compartan entre todos los componentes que necesiten formato de tiempo relativo.

Compatibilidad con navegadores

Intl.RelativeTimeFormat funciona en todos los navegadores modernos con un 95% de cobertura global:

  • Chrome 71+
  • Firefox 65+
  • Safari 14+
  • Edge 79+

Internet Explorer no admite esta API. Para aplicaciones que requieran compatibilidad con IE, hay polyfills disponibles, aunque la implementación nativa proporciona mejor rendimiento y tamaños de paquete más pequeños.

Cuándo usar esta API

Intl.RelativeTimeFormat funciona mejor para:

  • Mostrar la antigüedad del contenido en feeds y líneas de tiempo
  • Mostrar marcas de tiempo de comentarios o publicaciones
  • Formatear la programación de eventos en relación con la hora actual
  • Crear sistemas de notificaciones con marcas de tiempo relativas
  • Crear registros de actividad con tiempos legibles para humanos

La API no es adecuada para:

  • Formato de fecha y hora absoluta (usar Intl.DateTimeFormat)
  • Seguimiento preciso del tiempo que requiere exactitud de milisegundos
  • Temporizadores de cuenta regresiva que se actualizan cada segundo
  • Aritmética de fechas o cálculos de calendario

Para aplicaciones que requieren visualizaciones de tiempo tanto relativas como absolutas, combinar Intl.RelativeTimeFormat con Intl.DateTimeFormat. Mostrar tiempos relativos para contenido reciente y cambiar a fechas absolutas para contenido más antiguo:

function formatTimestamp(date, locale = 'en') {
  const now = new Date();
  const diffInMs = Math.abs(date - now);
  const sevenDaysInMs = 7 * 24 * 60 * 60 * 1000;

  if (diffInMs < sevenDaysInMs) {
    const rtf = new RelativeTimeFormatter(locale);
    return rtf.format(date);
  } else {
    const dtf = new Intl.DateTimeFormat(locale, {
      year: 'numeric',
      month: 'short',
      day: 'numeric'
    });
    return dtf.format(date);
  }
}

const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
console.log(formatTimestamp(yesterday));
// "yesterday"

const lastMonth = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
console.log(formatTimestamp(lastMonth));
// "Sep 14, 2025"

Este enfoque híbrido proporciona los beneficios del lenguaje natural de los tiempos relativos para contenido reciente mientras mantiene la claridad para marcas de tiempo más antiguas.