Cómo formatear tiempo relativo en React Router v7

Formatear marcas de tiempo como frases de 'hace 2 días'

Problema

Mostrar marcas de tiempo como frases de tiempo relativo como "hace 2 días" o "en 3 horas" hace que el contenido se sienta inmediato y contextual. Sin embargo, estas frases siguen reglas gramaticales complejas que varían dramáticamente entre idiomas. El inglés coloca "ago" después de las cantidades pasadas y "in" antes de las futuras, pero otros idiomas pueden flexionar la unidad de tiempo en sí, reordenar palabras o usar estructuras gramaticales completamente diferentes. Construir manualmente estas frases con concatenación de cadenas produce resultados incorrectos en todos los idiomas excepto en el que codificaste, rompiendo la experiencia de usuario para audiencias internacionales.

Solución

Utiliza el formato de tiempo relativo adaptado al idioma para convertir las diferencias de marca de tiempo en frases gramaticalmente correctas. Calcula la diferencia de tiempo entre una marca de tiempo dada y el momento actual, luego formatea esa diferencia utilizando las reglas de idioma del usuario. Esto asegura que las expresiones de tiempo pasado y futuro sigan la gramática correcta, el orden de palabras y los patrones de inflexión para cada idioma sin manipulación manual de cadenas.

Pasos

1. Crear un helper para calcular valores de tiempo relativo

El componente FormattedRelativeTime requiere un value numérico y una prop unit. Construye una función helper que calcule la diferencia de tiempo y seleccione una unidad apropiada.

export function getRelativeTimeValue(date: Date | number) {
  const now = Date.now();
  const timestamp = typeof date === "number" ? date : date.getTime();
  const diffInSeconds = Math.round((timestamp - now) / 1000);

  const minute = 60;
  const hour = minute * 60;
  const day = hour * 24;
  const week = day * 7;
  const month = day * 30;
  const year = day * 365;

  const absDiff = Math.abs(diffInSeconds);

  if (absDiff < minute) {
    return { value: diffInSeconds, unit: "second" as const };
  } else if (absDiff < hour) {
    return {
      value: Math.round(diffInSeconds / minute),
      unit: "minute" as const,
    };
  } else if (absDiff < day) {
    return { value: Math.round(diffInSeconds / hour), unit: "hour" as const };
  } else if (absDiff < week) {
    return { value: Math.round(diffInSeconds / day), unit: "day" as const };
  } else if (absDiff < month) {
    return { value: Math.round(diffInSeconds / week), unit: "week" as const };
  } else if (absDiff < year) {
    return { value: Math.round(diffInSeconds / month), unit: "month" as const };
  } else {
    return { value: Math.round(diffInSeconds / year), unit: "year" as const };
  }
}

Esta función convierte una marca de tiempo en un valor numérico con signo y la unidad de tiempo más apropiada basada en la magnitud de la diferencia.

2. Crear un componente de tiempo relativo utilizando FormattedRelativeTime

Utiliza el componente FormattedRelativeTime de react-intl, que renderiza el tiempo relativo formateado y puede actualizarse en intervalos opcionalmente.

import { FormattedRelativeTime } from "react-intl";
import { getRelativeTimeValue } from "./getRelativeTimeValue";

interface RelativeTimeProps {
  date: Date | number;
  updateIntervalInSeconds?: number;
}

export function RelativeTime({
  date,
  updateIntervalInSeconds,
}: RelativeTimeProps) {
  const { value, unit } = getRelativeTimeValue(date);

  return (
    <FormattedRelativeTime
      value={value}
      unit={unit}
      numeric="auto"
      updateIntervalInSeconds={updateIntervalInSeconds}
    />
  );
}

La opción numeric="auto" permite al formateador utilizar frases como "ayer" en lugar de "hace 1 día" cuando sea apropiado. La prop opcional updateIntervalInSeconds controla con qué frecuencia se vuelve a renderizar el componente para mantener actualizado el tiempo relativo.

3. Usar el componente en una ruta de React Router

Renderiza el componente de tiempo relativo en cualquier componente de ruta donde necesites mostrar marcas de tiempo.

import { RelativeTime } from "./RelativeTime";

interface Post {
  id: string;
  title: string;
  content: string;
  createdAt: number;
}

export function PostDetail({ post }: { post: Post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <time dateTime={new Date(post.createdAt).toISOString()}>
        <RelativeTime date={post.createdAt} />
      </time>
      <p>{post.content}</p>
    </article>
  );
}

El componente formatea automáticamente la marca de tiempo según la configuración regional del usuario, produciendo frases como "hace 2 días" en español, "2 days ago" en inglés o "il y a 2 jours" en francés.

4. Formatear tiempo relativo imperativamente con useIntl

Para casos donde necesitas la cadena formateada directamente (como establecer atributos de elementos), utiliza la función formatRelativeTime del hook useIntl.

import { useIntl } from "react-intl";
import { getRelativeTimeValue } from "./getRelativeTimeValue";

interface CommentProps {
  author: string;
  text: string;
  timestamp: number;
}

export function Comment({ author, text, timestamp }: CommentProps) {
  const intl = useIntl();
  const { value, unit } = getRelativeTimeValue(timestamp);
  const relativeTime = intl.formatRelativeTime(value, unit, {
    numeric: "auto",
  });

  return (
    <div aria-label={`Comentario de ${author}, publicado ${relativeTime}`}>
      <strong>{author}</strong>
      <p>{text}</p>
    </div>
  );
}

Este enfoque te proporciona una cadena formateada que puedes usar en atributos, concatenar con otro texto o pasar a APIs que no son de React mientras mantienes soporte completo para diferentes configuraciones regionales.