Formatando intervalos de tempo com Intl.RelativeTimeFormat em um projeto Angular

Expressar o tempo de forma mais natural, como "há 5 minutos" ou "em 2 dias", torna a informação mais compreensível e é comum em diversas aplicações, desde redes sociais até extratos bancários. Antigamente, era necessário escrever várias linhas de código para implementar essa funcionalidade, mas atualmente, com o Intl.RelativeTimeFormat, disponível nativamente no JavaScript, podemos facilmente formatar intervalos de tempo de forma clara, relativa e sem se preocupar com as traduções, simplificando o processo.

Neste pequeno texto, vamos falar sobre como utilizar essa funcionalidade em um projeto frontend utilizando o framework Angular.

Introdução

Para quem está começando, vale lembrar que o JavaScript está em constante evolução. Em projetos frontend, quem interpreta o código JavaScript é um "motor" dentro do navegador (Chrome, Safari, Edge, entre outros). Já no backend, o código JavaScript é interpretado pelo Node.js (que usa o mesmo "motor" do Chrome, o V8) ou Deno, por exemplo.

Esses "motores" têm diferentes implementações. Por isso, novas funções JavaScript podem levar um tempo para serem suportadas em todos os lugares, portanto, algumas funcionalidades podem funcionar em um ambiente, mas não em outro.

Por exemplo, a função fetch, que já foi comentada por aqui e é usada para buscar dados de um servidor, funciona a partir da versão 18 do Node.js. Por isso, é importante sempre verificar se os recursos JavaScript que você está usando no seu código são suportados pelo seu ambiente de execução, garantindo que tudo funcione como esperado. No caso do  Intl.RelativeTimeFormat, que é o nosso alvo de estudo, ele é suportado a partir das seguintes versões:

Navegador Plataforma Versão
Chrome (Desktop) 71
Edge (Desktop) 79
Firefox (Desktop) 65
Opera (Desktop) 58
Safari (Desktop) 14
Chrome (Android) 71
Firefox (Android) 65
Opera (Android) 50
Safari (iOS) 14
Node.js (-) 12.0.0

Os dados acima foram extraídos deste link em setembro de 2024.

O básico sobre o Intl.RelativeTimeFormat

O Intl.RelativeTimeFormat permite formatar textos que descrevem intervalos de tempo de maneira simples em relação ao momento atual, por exemplo: "ontem", "há uma hora", "amanhã", "em 2 dias", entre outros.

Exemplo - Formatando intervalo de tempo

Abaixo há um exemplo bem simples de como utilizar o Intl.RelativeTimeFormat:

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

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

console.log(rtf.format(1, 'day'));
// "amanhã"

console.log(rtf.format(-3, 'day'));
// "há 3 dias"

console.log(rtf.format(2, 'week'));
// "em 2 semanas"
💡
Para executar o trecho de código acima, selecione o código e copie. Na sequência abra o DevTools do seu navegador (ctrl + shift + i) e na aba "console" cole o código e pressione enter para ver o resultado da execução.

Explicação do código acima

Antes de tudo é necessário criar uma instância do Intl.RelativeTimeFormat. Para isso, passamos o idioma desejado e um objeto com opções de configuração. Esse objeto de configuração pode receber os seguintes parâmetros:

  1. locale (idioma): Por exemplo 'en-US' para inglês (Estados Unidos) ou 'pt-BR' para português (Brasil).
  2. options (opções): Um objeto opcional que pode conter propriedades como:
    • numeric: Pode ser 'always' (sempre usar números) ou 'auto' (usar palavras como "ontem" ou "amanhã" quando aplicável).
    • style: Pode ser 'long''short' ou 'narrow', determinando o comprimento da string de saída.

Depois de criar a instância, podemos utilizar o método format, que recebe dois parâmetros, para formatar o intervalo de tempo:

  1. primeiro parâmetro: diferença de tempo em relação a data corrente, podendo ser positivo ou negativo para indicar tempos futuros ou passados.
  2. segundo parâmetro: unidade de tempo (ver lista abaixo)
type RelativeTimeFormatUnit =
  | "year"
  | "years"
  | "quarter"
  | "quarters"
  | "month"
  | "months"
  | "week"
  | "weeks"
  | "day"
  | "days"
  | "hour"
  | "hours"
  | "minute"
  | "minutes"
  | "second"
  | "seconds";

Unidades de tempo

Exemplo:

(() => {
  const opcoes = [
    { numeric: "always", style: "long" },
    { numeric: "auto", style: "long" },

    { numeric: "always", style: "short" },
    { numeric: "auto", style: "short" },

    { numeric: "always", style: "narrow" },
    { numeric: "auto", style: "narrow" },
  ];

  for (const { numeric, style } of opcoes) {
    const rtf = new Intl.RelativeTimeFormat(
      "pt-BR",
      { numeric, style }
    );

    console.group(`${numeric}-${style}`);
    console.log(
      rtf.format(-1, "minute"),
      rtf.format(1, "day"),
      rtf.format(-2, "week"),
      rtf.format(2, "month")
    );
    console.groupEnd();
  }
})();

Resultado:

always-long
há 1 minuto – "em 1 dia" – "há 2 semanas" – "em 2 meses"

auto-long
há 1 minuto – "amanhã" – "há 2 semanas" – "em 2 meses"

always-short
há 1 min. – "em 1 dia" – "há 2 sem." – "em 2 meses"

auto-short
há 1 min. – "amanhã" – "há 2 sem." – "em 2 meses"

always-narrow
há 1 min. – "em 1 dia" – "há 2 sem." – "em 2 meses"

auto-narrow
há 1 min. – "amanhã" – "há 2 sem." – "em 2 meses"

Resultado da execução do código acima

Construindo um Angular Pipe para formatar o intervalo de tempo com Intl.RelativeTimeFormat

Partindo para uma aplicação mais próxima da realidade, criei um projeto em Angular 18 utilizando o StackBlitz. Em seguida, desenvolvi um Pipe que recebe uma data de referência e retorna o intervalo de tempo formatado usando o Intl.RelativeTimeFormat.

A implementação do Pipe ficou as seguinte forma:

import { Pipe, PipeTransform } from '@angular/core';
import { LOCALE_ID, inject } from '@angular/core';

@Pipe({
  standalone: true,
  // -------------------------------------------------------
  // Define o Pipe como puro, ou seja, só será recalculado
  // quando as entradas mudarem.
  // Para mais detalhes:
  // https://consolelog.com.br/pipes-nativos-do-angular/
  pure: true,
  name: 'formatRelativeDate',
})
export class FormatRelativeDatePipe
  implements PipeTransform
{
  // Injeta a localização padrão da aplicação (LOCALE_ID)
  defaultLocale: string = inject(LOCALE_ID);

  // Função que será chamada pelo Angular ao usar o Pipe
  transform(date: Date, locale?: string) {
    return this.format(
      date, locale || this.defaultLocale
    );
  }

  format(date: Date, locale: string) {
    // Mapeia as constantes de milissegundos para cada
    // unidade de tempo
    const MILLISECONDS_PER = {
      YEAR: 31536000000,
      MONTH: 2592000000,
      WEEK: 604800000,
      DAY: 86400000,
      HOUR: 3600000,
      MINUTE: 60000,
      SECOND: 1000,
    };

    const now = new Date();
    // Calcula a diferença entre a data fornecida
    // e a data atual em milissegundos
    const differenceInMilliseconds =
      date.getTime() - now.getTime();

    // Calcula o valor absoluto da diferença,
    // ignorando se é no passado ou futuro
    const absoluteDifference = Math.abs(
      differenceInMilliseconds
    );

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

    // Faz o mapeamento entre as unidades de tempo
    // suportadas pelo Intl.RelativeTimeFormat e o
    // intervalo de tempo em milissegundos por ano, mês,
    // semana, dia, hora, minuto e segundo.
    const timeUnits: Array<{
      unit: Intl.RelativeTimeFormatUnit;
      threshold: number;
    }> = [
      {
        unit: 'year',
        threshold: MILLISECONDS_PER.YEAR
      }, {
        unit: 'month',
        threshold: MILLISECONDS_PER.MONTH
      }, {
        unit: 'week',
        threshold: MILLISECONDS_PER.WEEK
      }, {
        unit: 'day',
        threshold: MILLISECONDS_PER.DAY
      }, {
        unit: 'hour',
        threshold: MILLISECONDS_PER.HOUR
      }, {
        unit: 'minute',
        threshold: MILLISECONDS_PER.MINUTE
      }, {
        unit: 'second',
        threshold: MILLISECONDS_PER.SECOND
      },
    ];

    // Verifica qual unidade de tempo é a mais apropriada
    // para a diferença entre as datas
    for (const { unit, threshold } of timeUnits) {
      if (absoluteDifference >= threshold) {
        return formatter.format(
          Math.round(
            differenceInMilliseconds / threshold
          ),
          unit
        );
      }
    }

    // Se a diferença for menor que 1 segundo:
    return formatter.format(0, 'second');
  }
}

format-relative-date.pipe.ts

O arquivo main.ts, que define o componente principal da aplicação, ficou assim:

import { Component, LOCALE_ID } from '@angular/core';
import { CommonModule } from '@angular/common';
import { bootstrapApplication }
  from '@angular/platform-browser';
import { FormatRelativeDatePipe }
  from './format-relative-date.pipe';

import { registerLocaleData } from '@angular/common';
import localePT from '@angular/common/locales/pt';
import localeES from '@angular/common/locales/es';

registerLocaleData(localePT, 'pt-br');
registerLocaleData(localeES, 'es-es');

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [FormatRelativeDatePipe, CommonModule],
  template: `
    <h1>Formatação relativa de tempo</h1>
    <p>Data atual: {{ currentDate | date: 'full' }}</p>

    @for (date of dates; track $index) {
      <h2>({{ date | date : 'full' }})</h2>
      <div class="grid">
        <div>
          {{date | formatRelativeDate}}
        </div>
        <div>
          date | formatRelativeDate
        </div>
      </div>
      <div class="grid">
        <div>
          {{date | formatRelativeDate : 'en-us'}}
        </div>
        <div>
          date | formatRelativeDate : 'en-us'
        </div>
      </div>
      <div class="grid">
        <div>
          {{date | formatRelativeDate : 'es-es'}}
        </div>
        <div>
          date | formatRelativeDate : 'es-es
        </div>
      </div>
    }`,
})
export class App {
  currentDate = new Date();
  dates: Date[] = [
    new Date(this.currentDate.getTime() - 60_000),
    new Date(this.currentDate.getTime() + 60_000),
    new Date('2024-01-10T12:00:00.000Z'),
    new Date('2024-02-10T12:00:00.000-0300')
  ];
}

bootstrapApplication(App, {
  providers: [{ provide: LOCALE_ID, useValue: 'pt-br' }],
});

main.ts

Resultado da execução do projeto:

Resultado final

Para acessar o projeto acima, utilize este link do StackBlitz.

Considerações

Neste texto, exploramos o uso do Intl.RelativeTimeFormat para formatação de intervalos de tempo, aplicando-o em um projeto Angular, mas destacando que o mesmo conceito pode ser facilmente reutilizado em outras plataformas como Node.js, NestJS, Deno, React, Vue e entre outros.

Links interessantes: