Cómo formatear tiempo relativo en TanStack Start v1

Formatea 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 la información temporal sea más intuitiva para los usuarios. Sin embargo, estas frases siguen reglas gramaticales complejas que varían según el idioma. El inglés coloca "ago" después de cantidades pasadas e "in" antes de las futuras, pero otros idiomas pueden usar diferentes órdenes de palabras, flexionar unidades de tiempo o emplear 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.

La API de formateo subyacente requiere un valor numérico y una unidad de tiempo, pero las marcas de tiempo llegan como objetos Date o valores en milisegundos. Convertir una marca de tiempo en la unidad y el valor apropiados requiere calcular la diferencia desde el momento actual y seleccionar la unidad más natural para expresar esa diferencia.

Solución

Calcula la diferencia de tiempo entre una marca de tiempo y el momento actual, selecciona la unidad más apropiada para esa diferencia y luego formatea el resultado usando formateo de tiempo relativo específico del idioma. Esto transforma marcas de tiempo sin procesar en frases gramaticalmente correctas que se adaptan al idioma del usuario.

Usa el formateo de tiempo relativo de react-intl con una función auxiliar que determina la mejor unidad. La función auxiliar compara la diferencia de tiempo con umbrales para cada unidad y devuelve tanto el valor numérico como el nombre de la unidad. El formateador luego aplica las reglas gramaticales correctas para el idioma activo.

Pasos

1. Crea una función auxiliar de selección de unidad

Construye una función que calcule la diferencia de tiempo y seleccione la unidad más natural para expresarla.

type RelativeTimeUnit =
  | "second"
  | "minute"
  | "hour"
  | "day"
  | "week"
  | "month"
  | "year";

interface RelativeTimeValue {
  value: number;
  unit: RelativeTimeUnit;
}

export function selectRelativeTimeUnit(
  timestamp: Date | number,
  baseTime: Date | number = Date.now(),
): RelativeTimeValue {
  const date = typeof timestamp === "number" ? timestamp : timestamp.getTime();
  const base = typeof baseTime === "number" ? baseTime : baseTime.getTime();
  const diffMs = date - base;
  const absDiff = Math.abs(diffMs);

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

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

Esta función convierte una marca de tiempo en un valor relativo y una unidad comparando la diferencia de tiempo con umbrales fijos. Devuelve valores negativos para tiempos pasados y valores positivos para tiempos futuros, que el formateador interpreta correctamente.

2. Crear un componente de tiempo relativo

Construye un componente que combine el helper de selección de unidad con el formato de react-intl.

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

interface RelativeTimeProps {
  date: Date | number;
  numeric?: "always" | "auto";
  style?: "long" | "short" | "narrow";
}

export function RelativeTime({
  date,
  numeric = "auto",
  style = "long",
}: RelativeTimeProps) {
  const { value, unit } = selectRelativeTimeUnit(date);

  return (
    <FormattedRelativeTime
      value={value}
      unit={unit}
      numeric={numeric}
      style={style}
    />
  );
}

Este componente acepta una marca de tiempo y opciones de formato, calcula el valor relativo y la unidad, y luego delega en el componente de react-intl para el renderizado adaptado a la configuración regional. La opción numeric="auto" produce frases como "ayer" en lugar de "hace 1 día" cuando es apropiado.

3. Usar el componente en tus rutas

Importa y renderiza el componente donde necesites mostrar marcas de tiempo relativas.

import { createFileRoute } from "@tanstack/react-router";
import { RelativeTime } from "../components/RelativeTime";

export const Route = createFileRoute("/posts/$postId")({
  component: PostPage,
});

function PostPage() {
  const post = {
    title: "Understanding Relative Time",
    publishedAt: new Date("2024-11-15T10:30:00Z"),
    updatedAt: new Date(Date.now() - 2 * 60 * 60 * 1000),
  };

  return (
    <article>
      <h1>{post.title}</h1>
      <p>
        Published <RelativeTime date={post.publishedAt} />
      </p>
      <p>
        Updated <RelativeTime date={post.updatedAt} style="short" />
      </p>
    </article>
  );
}

El componente funciona tanto en contextos renderizados en el servidor como en el cliente. En el servidor produce la frase de tiempo relativo inicial, y en el cliente muestra la misma frase usando la configuración regional del usuario del IntlProvider.

4. Añadir una opción de formato imperativo

Para casos donde necesites la cadena formateada directamente, crea un helper basado en hooks.

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

export function useRelativeTime() {
  const intl = useIntl();

  return (
    date: Date | number,
    options?: {
      numeric?: "always" | "auto";
      style?: "long" | "short" | "narrow";
    },
  ) => {
    const { value, unit } = selectRelativeTimeUnit(date);
    return intl.formatRelativeTime(value, unit, options);
  };
}

Este hook devuelve una función que formatea marcas de tiempo de forma imperativa, útil para establecer atributos de texto o calcular valores fuera de JSX.

5. Usar el hook para contextos sin componentes

Llama al hook en componentes donde necesites cadenas formateadas para atributos o lógica.

import { createFileRoute } from "@tanstack/react-router";
import { useRelativeTime } from "../hooks/useRelativeTime";

export const Route = createFileRoute("/events/$eventId")({
  component: EventPage,
});

function EventPage() {
  const formatRelativeTime = useRelativeTime();
  const event = {
    name: "Product Launch",
    startTime: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000),
  };

  const timeUntil = formatRelativeTime(event.startTime);

  return (
    <div>
      <h1>{event.name}</h1>
      <time dateTime={event.startTime.toISOString()} title={timeUntil}>
        {timeUntil}
      </time>
    </div>
  );
}

El hook proporciona la misma lógica de formato en forma funcional, permitiéndote usar cadenas de tiempo relativo en atributos, valores calculados o en cualquier lugar donde no se pueda renderizar un componente.