Formatação de datas correta por fuso no JavaScript sem bibliotecas

O Intl.DateTimeFormat integrado do JavaScript trata a formatação de fusos IANA sem moment.js ou date-fns. Aprenda as opções timeZone explícitas, a exibição segura quanto ao horário de verão, formatToParts, os limites da conversão de relógio e quando o Temporal ou uma biblioteca de fusos ainda é útil.

Por que Date não tem propriedade de fuso

Um Date do JavaScript é sempre uma contagem de milissegundos UTC. Não há fuso armazenado dentro do objeto. Quando você chama .toString() ou .toLocaleString() sem opções, o JavaScript usa o fuso local do runtime a partir do sistema operacional. O mesmo código produz saídas diferentes em um servidor em Nova York e em um notebook em Tóquio, mesmo que o timestamp subjacente seja idêntico.

Formatar com Intl.DateTimeFormat

Intl.DateTimeFormat é a API integrada para formatação ciente de localidade e fuso. Ela aceita identificadores de fuso IANA, trata as transições de horário de verão automaticamente e está disponível em navegadores modernos e no Node.js. O segredo é passar a opção timeZone explicitamente em vez de confiar no padrão do runtime.

  • new Intl.DateTimeFormat('en-US', { timeZone: 'America/New_York', dateStyle: 'full', timeStyle: 'long' }).format(date)
  • date.toLocaleString('en-GB', { timeZone: 'Europe/London', hour12: false })
  • date.toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' })
  • new Intl.DateTimeFormat('en-US', { timeZone: 'UTC', hour12: false }).format(date)

Extrair partes individuais com formatToParts

Use formatToParts() para obter os componentes individuais da data como objetos {type, value} e montar strings de data personalizadas. Isso é melhor do que dividir uma string de data localizada porque pontuação, ordem e escritas diferem por localidade.

  • const parts = new Intl.DateTimeFormat('en-US', { timeZone: 'America/Chicago', year: 'numeric', month: '2-digit', day: '2-digit' }).formatToParts(date)
  • parts.find(p => p.type === 'year').value → '2023'
  • parts.find(p => p.type === 'month').value → '11'
  • Object.fromEntries(parts.map(p => [p.type, p.value])) → { year, month, day, hour, minute, second }

Converter hora de relógio em UTC (o sentido difícil)

Ir de uma hora de relógio em um fuso dado para UTC é mais difícil com a API Date clássica. Formatar um instante em America/New_York é fácil; construir o instante representado por 2026-03-08 02:30 em America/New_York não é, pois essa hora local pode ser pulada ou repetida durante as transições de horário de verão.

  • O Temporal chegou ao Estágio 4 em 2026, mas o suporte nativo dos navegadores ainda não é universal
  • Para uso em produção em todos os navegadores hoje, o polyfill do Temporal ou date-fns-tz toDate() ainda são escolhas práticas
  • Evite aritmética manual de offset UTC — as transições de horário de verão ocorrem em horas locais diferentes em anos diferentes
  • O zonedToEpochMs() deste site usa correção de offset de uma iteração — veja src/timeUtils.ts

Escolher o identificador de fuso correto

Use nomes de fuso IANA como America/New_York, Europe/London, Asia/Tokyo ou UTC. Evite offsets fixos como UTC-5 para a hora voltada ao usuário porque os offsets mudam com o horário de verão. America/New_York pode ser UTC-5 em janeiro e UTC-4 em julho; o nome IANA permite à plataforma aplicar a regra histórica correta para a data selecionada.

  • Bom: America/Los_Angeles — inclui regras de horário de verão históricas e futuras
  • Bom: Europe/Berlin — trata o horário de verão da Europa Central automaticamente
  • Bom: Asia/Shanghai — exibição estável UTC+8 sem horário de verão
  • Use UTC para logs, armazenamento de API, timestamps de banco e comparação entre regiões
  • Use o fuso IANA do usuário para a exibição final na interface do produto

FAQ sobre formatação de fusos

O JavaScript pode formatar uma data em outro fuso sem uma biblioteca?
Sim. Use Intl.DateTimeFormat ou toLocaleString com uma opção timeZone, como America/New_York ou Asia/Tokyo.
O Intl.DateTimeFormat trata o horário de verão?
Sim. Quando você usa um identificador de fuso IANA, o runtime aplica o offset correto para aquele fuso e data.
Devo armazenar offsets de fuso ou nomes de fuso?
Armazene UTC para os instantes dos eventos. Se você precisa do contexto local do usuário, armazene um nome de fuso IANA como America/New_York, não apenas um offset numérico.
Como obtenho o fuso do usuário no JavaScript?
Use Intl.DateTimeFormat().resolvedOptions().timeZone, que retorna um nome IANA como America/New_York. Armazene esse nome, não um offset numérico, quando precisar reconstruir depois a hora local do usuário.