Por que Datas e Números Precisam de Localização
TABLE OF CONTENTS
Se você envia globalmente, a mesma string pode significar coisas diferentes para diferentes usuários: 12/01/2025, 1.234, 1,234, 12:00, 00:00. Essas pequenas diferenças não são cosméticas — elas afetam a confiança, compreensão e até mesmo a conformidade legal. Este guia explica por que datas e números devem ser localizados, o que realmente muda por localidade e como implementá-lo corretamente em todo o seu produto.
O que Muda por Localidade
- Datas: ordem (MDY vs DMY vs YMD), separadores, nomes de meses/dias; alguns mercados usam calendários não gregorianos.
- Horários: relógios de 12 horas vs 24 horas, marcadores AM/PM, fusos horários, regras de horário de verão, início da semana (domingo vs segunda-feira).
- Números: separadores decimais (
.vs,), estilos de agrupamento (1,234vs1.234vs1 234), agrupamento indiano (1,23,456), espaços não separáveis, estilos de menos/mais (hífen vs verdadeiro menos−). - Moeda: símbolo vs código ISO, colocação do símbolo (prefixo/sufixo), espaçamento estreito vs amplo, precisão decimal (0, 2 ou 3+), negativos contábeis (por exemplo,
(1 234,56 €)). - Percentuais e unidades: espaçamento e colocação do sinal (
50%vs50 %), nomes locais para unidades, métrico vs imperial.
Impacto nos Negócios
- Confiança e conversão: Preços, taxas e datas que “parecem erradas” reduzem compras e aumentam a rotatividade.
- Risco operacional: Datas mal interpretadas podem alterar reservas ou prazos; erros de análise quebram faturas, exportações e análises.
- Conformidade: Documentos financeiros com formatos errados podem violar regras de faturamento, impostos ou relatórios.
- Carga de suporte: Usuários abrem tickets para esclarecer horários, moedas e entradas de números que não correspondem às expectativas.
Incidente do Mundo Real
Uma plataforma europeia de reservas de viagens exibiu datas de partida como 01/03/2024 sem contexto de localidade. Clientes dos EUA interpretaram isso como “3 de janeiro”, enquanto clientes do Reino Unido e europeus leram como “1 de março”.
Impacto:
- 12% das reservas internacionais foram feitas para datas erradas
- O serviço de atendimento ao cliente recebeu mais de 3.400 tickets de esclarecimento em uma semana
- $2,3M em reembolsos e taxas de remarcação
- 8% dos clientes afetados migraram para concorrentes
Causa raiz: Formato DD/MM/YYYY codificado em confirmações de e-mail enquanto o site usava MM/DD/YYYY para usuários dos EUA. A correção exigiu apenas 3 horas de tempo de engenharia — a ambiguidade custou 800 vezes mais do que a prevenção teria custado.
Lição: Datas sem contexto são uma bomba-relógio para qualquer produto global.
Armadilhas Comuns
- Formatos codificados:
MM/DD/YYYYou1,234.56embutidos na UI, e-mails, PDFs ou exportações CSV. - Armazenamento de strings localizadas: Salvar “Dec 1, 2025” em vez de tempo ISO causa erros de análise e desvio de fuso horário.
- Análise ingênua: Usar padrões de local do servidor; assumir separadores ou relógios de 12/24 horas.
- Concatenação de strings: Construir “quantidade + moeda” como
"$" + 1234.56em vez de formatação sensível ao local. - Ambiguidade de fuso horário: Mostrar horários locais sem zonas explícitas para ações sensíveis ao tempo.
- Inconsistência do navegador: Safari, Firefox e Chrome podem formatar datas/números de maneira diferente; sempre teste em todos os navegadores-alvo.
- Lacunas de renderização no lado do servidor: Node.js sem dados completos do ICU (
node --with-intl=full-icu) produz formatação incompleta ou incorreta. - Armadilhas de desempenho: Criar novas instâncias
Intl.*em loops ou ciclos de renderização em vez de armazenar formatadores em cache.
Melhores Práticas
- Armazenar valores canônicos: Datas/horas como ISO-8601 em UTC; dinheiro como unidades menores (centavos) ou decimais de alta precisão.
- Renderizar por localidade do usuário: Use APIs sensíveis à localidade (por exemplo,
Intl.DateTimeFormat,Intl.NumberFormat) para exibição. - Validar e analisar por localidade: Aceitar entrada de localidade quando apropriado; mostrar espaços reservados/exemplos para guiar os usuários.
- Ser explícito com o tempo: Mostrar abreviações de fuso horário ou deslocamentos; permitir seleção do usuário onde os resultados dependem do tempo.
- Clareza de moeda: Preferir códigos ISO onde existe ambiguidade; respeitar a colocação de símbolos e espaçamento por localidade.
- Centralizar formatação: Uma camada de utilidade para todas as superfícies (UI, e-mails, PDFs, exportações) para garantir consistência.
- Cache de formatadores: Chamadas de construtor
Intl.*são caras; criar uma vez e reutilizar instâncias por localidade.
Quando NÃO Localizar
Nem tudo deve ser localizado. Aqui estão cenários onde formatos consistentes e legíveis por máquina são melhores:
- Exportações legíveis por máquina: Arquivos CSV consumidos por pipelines de dados ou ferramentas de análise devem usar um formato fixo (por exemplo, datas ISO-8601, ponto como separador decimal). Documente este formato claramente nos cabeçalhos de exportação ou arquivos README.
- Respostas de API: APIs REST/GraphQL devem retornar datas como strings ISO-8601 e números no formato padrão JSON. Deixe os clientes lidarem com a localização com base nas preferências de seus usuários.
- Logs internos e métricas: Logs, painéis de monitoramento e consultas de banco de dados se beneficiam de formatos consistentes para análise, agregação e alerta. Use ISO-8601 e formatos numéricos padrão.
- Identificadores canônicos: IDs de transação, números de pedido ou referências que os usuários podem precisar comunicar entre localidades devem evitar formatação específica de localidade.
Regra prática: Localize para humanos que leem conteúdo; use formatos padrão para máquinas que processam dados.
Plano de Implementação
- Superfícies de auditoria: Inventário de datas, horários, números, moeda na UI, e-mails, PDFs, exportações CSV/Excel, análises, logs.
- Definir locais: Listar locais suportados e padrões de fallback; especificar política de 12/24 horas por local.
- Construir utilitários: Envolver APIs
Intl.*(ou uma biblioteca) com ajudantes e testes compartilhados; implementar cache de formatadores. - Estratégia de entrada: Atualizar formulários para aceitar e validar formatos de locais; armazenar valores canônicos juntamente com a entrada bruta.
- Regras de conteúdo: Documentar estilo para datas curtas/longas, datas relativas, exibição de moeda e formatação de porcentagem.
- Implementação: Converter primeiro as superfícies de maior tráfego e maior risco; lançar por trás de um sinalizador de recurso, se necessário.
Estratégia de Teste
Além de QA manual, implementar testes automatizados para evitar regressões:
- Testes de snapshot: Congelar saídas formatadas para cada local suportado. Mudanças desencadeiam revisão para garantir que são intencionais.
- Testes de ida e volta: Verificar que
format → parse → formatproduz resultados estáveis. Detecta perda de precisão ou formatos ambíguos. - Casos extremos: Testar números negativos, zero, números muito grandes (bilhões/trilhões), transições de DST, anos bissextos e datas de limite (1 de janeiro, 31 de dezembro).
- Validação entre navegadores: Testes automatizados no Chrome, Firefox, Safari e Edge para detectar diferenças de renderização.
- Fallback de local: Verificar degradação graciosa quando locais não suportados são solicitados.
- Benchmarks de desempenho: Medir custo de criação de formatador; garantir que o cache está funcionando em produção.
Dicas de Desempenho
APIs Intl.* são poderosas, mas podem ser lentas se mal utilizadas. Siga estas diretrizes:
// ❌ RUIM: Cria novo formatador em cada chamada
function formatPrice(amount, locale) {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD'
}).format(amount);
}
// ✅ BOM: Cache formatadores por localidade const formatters = new Map(); function getFormatter(locale) { if (!formatters.has(locale)) { formatters.set(locale, new Intl.NumberFormat(locale, { style: ‘currency’, currency: ‘USD’ })); } return formatters.get(locale); }
function formatPrice(amount, locale) { return getFormatter(locale).format(amount); }
- **Cache de instâncias de formatadores**: A construção de `Intl.*` é cara (~1–5ms); formatar com uma instância existente é rápido (~0.01ms).
- **Evite loops**: Nunca crie formatadores dentro de `map()`, `forEach()`, ou loops de renderização.
- **Carregamento preguiçoso de dados de localidade**: Para aplicativos web, considere a divisão de código para carregar apenas localidades necessárias, reduzindo o tamanho do pacote.
- **Servidor**: Use um único formatador por ciclo de requisição/resposta; evite criar por item em grandes conjuntos de dados.
## Lista de Verificação de QA
- Datas são inequívocas em `en-US`, `en-GB`, `de-DE`, `fr-FR`, `hi-IN`, `ja-JP`, `ar-EG`.
- O tempo usa a convenção correta de 12/24 horas; marcadores AM/PM aparecem onde esperado; fusos horários mostrados para fluxos críticos de tempo.
- Números usam separadores decimais e de agrupamento corretos; espaços não quebráveis onde padrão (por exemplo, `fr-FR`).
- A moeda mostra o símbolo/código correto, posicionamento e precisão decimal para cada localidade; negativos exibidos corretamente.
- Entradas aceitam e analisam dados de localidade do usuário (ou impõem claramente um formato suportado); mensagens de validação são sensíveis à localidade.
- Exportações (CSV/PDF) correspondem às expectativas de localidade ou documentam claramente formatos fixos para consumo por máquina.
- Cross-browser: Datas, horários e números são renderizados de forma consistente no Chrome, Firefox, Safari e Edge.
- Desempenho: Nenhuma criação de formatador em loops; instâncias em cache reutilizadas em renderizações.
## Exemplos de Código
### JavaScript
```js
// Datas
const date = new Date('2025-01-12T00:00:00Z');
console.log(new Intl.DateTimeFormat('en-US', { dateStyle: 'short', timeZone: 'UTC' }).format(date));
// → 1/12/25
console.log(new Intl.DateTimeFormat('en-GB', { dateStyle: 'short', timeZone: 'UTC' }).format(date));
// → 12/01/2025 (day/month/year)
console.log(new Intl.DateTimeFormat('ja-JP', { dateStyle: 'medium', timeZone: 'UTC' }).format(date));
// → 2025/01/12
// Times with timezone display
const time = new Date('2025-01-12T18:05:00Z');
console.log(new Intl.DateTimeFormat('en-US', {
dateStyle: 'short',
timeStyle: 'short',
timeZone: 'America/New_York',
timeZoneName: 'short'
}).format(time));
// → 1/12/25, 1:05 PM EST
console.log(new Intl.DateTimeFormat('de-DE', {
timeStyle: 'short',
timeZone: 'Europe/Berlin',
hour12: false
}).format(time));
// → 19:05
// Numbers
const n = 1234.56;
console.log(new Intl.NumberFormat('en-US').format(n)); // → 1,234.56
console.log(new Intl.NumberFormat('de-DE').format(n)); // → 1.234,56
console.log(new Intl.NumberFormat('fr-FR').format(n)); // → 1 234,56 (NBSP as group separator)
// Indian numbering
console.log(new Intl.NumberFormat('hi-IN').format(1234567.89)); // → 12,34,567.89
// Currency
console.log(new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(n));
// → $1,234.56
console.log(new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(n));
// → 1.234,56 €
console.log(new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' }).format(1234));
// → ¥1,234 (no decimals)
// Percent and units
console.log(new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 0 }).format(0.5));
// → 50%
console.log(new Intl.NumberFormat('fr-FR', { style: 'percent', maximumFractionDigits: 0 }).format(0.5));
// → 50 %
// Formatter caching example
class LocaleFormatter {
constructor() {
this.cache = new Map();
}
getDateFormatter(locale, options) {
const key = `date:${locale}:${JSON.stringify(options)}`;
if (!this.cache.has(key)) {
this.cache.set(key, new Intl.DateTimeFormat(locale, options));
}
return this.cache.get(key);
}
formatDate(date, locale, options = { dateStyle: 'medium' }) {
return this.getDateFormatter(locale, options).format(date);
}
}
const formatter = new LocaleFormatter();
console.log(formatter.formatDate(new Date(), 'en-US')); // → Jan 12, 2025
console.log(formatter.formatDate(new Date(), 'fr-FR')); // → 12 janv. 2025
Python
from babel.dates import format_date, format_time, format_datetime
from babel.numbers import format_number, format_currency, format_percent
from datetime import datetime
import pytz
# Datas
date = datetime(2025, 1, 12)
print(format_date(date, format='short', locale='en_US')) # → 1/12/25
print(format_date(date, format='short', locale='en_GB')) # → 12/01/2025
print(format_date(date, format='medium', locale='ja_JP')) # → 2025/01/12
# Horários com fuso horário
tz_ny = pytz.timezone('America/New_York')
tz_berlin = pytz.timezone('Europe/Berlin')
time = datetime(2025, 1, 12, 18, 5, tzinfo=pytz.UTC)
print(format_datetime(time.astimezone(tz_ny), 'short', tzinfo=tz_ny, locale='en_US'))
# → 1/12/25, 1:05 PM
print(format_time(time.astimezone(tz_berlin), format='short', tzinfo=tz_berlin, locale='de_DE'))
# → 19:05
# Números
n = 1234.56
print(format_number(n, locale='en_US')) # → 1,234.56
print(format_number(n, locale='de_DE')) # → 1.234,56
print(format_number(n, locale='fr_FR')) # → 1 234,56
# Numeração indiana
print(format_number(1234567.89, locale='hi_IN')) # → 12,34,567.89
# Moeda
print(format_currency(n, 'USD', locale='en_US')) # → $1,234.56
print(format_currency(n, 'EUR', locale='de_DE')) # → 1.234,56 €
print(format_currency(1234, 'JPY', locale='ja_JP')) # → ¥1,234
# Percentual
print(format_percent(0.5, locale='en_US')) # → 50%
print(format_percent(0.5, locale='fr_FR')) # → 50 %
Java
import java.text.NumberFormat;
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;
import java.util.Currency;
// Datas
ZonedDateTime date = ZonedDateTime.of(2025, 1, 12, 0, 0, 0, 0, ZoneId.of("UTC"));
DateTimeFormatter usFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
.withLocale(Locale.US);
System.out.println(usFormatter.format(date)); // → 1/12/25
DateTimeFormatter gbFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
.withLocale(Locale.UK);
System.out.println(gbFormatter.format(date)); // → 12/01/2025
DateTimeFormatter jpFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(Locale.JAPAN);
System.out.println(jpFormatter.format(date)); // → 2025/01/12
// Horários
ZonedDateTime time = ZonedDateTime.of(2025, 1, 12, 18, 5, 0, 0, ZoneId.of("UTC"));
ZonedDateTime timeNY = time.withZoneSameInstant(ZoneId.of("America/New_York"));
DateTimeFormatter usTimeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
.withLocale(Locale.US);
System.out.println(usTimeFormatter.format(timeNY)); // → 1:05 PM
// Números
double n = 1234.56;
NumberFormat usFormat = NumberFormat.getInstance(Locale.US);
System.out.println(usFormat.format(n)); // → 1,234.56
NumberFormat deFormat = NumberFormat.getInstance(Locale.GERMANY);
System.out.println(deFormat.format(n)); // → 1.234,56
NumberFormat frFormat = NumberFormat.getInstance(Locale.FRANCE);
System.out.println(frFormat.format(n)); // → 1 234,56
// Moeda
NumberFormat usCurrency = NumberFormat.getCurrencyInstance(Locale.US);
usCurrency.setCurrency(Currency.getInstance("USD"));
System.out.println(usCurrency.format(n)); // → $1,234.56
NumberFormat deCurrency = NumberFormat.getCurrencyInstance(Locale.GERMANY);
deCurrency.setCurrency(Currency.getInstance("EUR"));
System.out.println(deCurrency.format(n)); // → 1.234,56 €
NumberFormat jpCurrency = NumberFormat.getCurrencyInstance(Locale.JAPAN); jpCurrency.setCurrency(Currency.getInstance(“JPY”)); System.out.println(jpCurrency.format(1234)); // → ¥1,234
### Go
```go
package main
import (
"fmt"
"time"
"golang.org/x/text/language"
"golang.org/x/text/message"
"golang.org/x/text/number"
)
func main() {
// Números
n := 1234.56
pUS := message.NewPrinter(language.AmericanEnglish)
fmt.Println(pUS.Sprintf("%.2f", n)) // → 1,234.56
pDE := message.NewPrinter(language.German)
fmt.Println(pDE.Sprintf("%.2f", n)) // → 1.234,56
pFR := message.NewPrinter(language.French)
fmt.Println(pFR.Sprintf("%.2f", n)) // → 1 234,56
// Moeda (usando pacote number)
fmt.Println(pUS.Sprint(number.Decimal(n, number.Scale(2)))) // → 1,234.56
// Datas - O pacote time do Go usa layouts em vez de formatação de localidade
// Para formatação completa de datas i18n, use github.com/goodsign/monday ou similar
date := time.Date(2025, 1, 12, 0, 0, 0, 0, time.UTC)
fmt.Println(date.Format("01/02/2006")) // US: 01/12/2025
fmt.Println(date.Format("02/01/2006")) // EU: 12/01/2025
fmt.Println(date.Format("2006/01/02")) // ISO: 2025/01/12
}
Nota: A biblioteca padrão do Go tem suporte limitado a localidades. Para uso em produção, considere:
golang.org/x/textpara formatação de númerosgithub.com/goodsign/mondaypara formatação de data/hora localizadagithub.com/bojanz/currencypara manipulação de moeda
Recursos Úteis
- Normas: Unicode CLDR (padrão de dados de localidade), IETF BCP 47 (identificadores de localidade)
- Documentação: MDN Intl Reference
- Bibliotecas:
- Luxon (biblioteca moderna de data/tempo com i18n)
- date-fns com módulos de localidade
- Globalize.js (i18n abrangente baseado em CLDR)
- Format.js (i18n focado em React com sintaxe de mensagens ICU)
- Testes: Intl polyfill para navegadores mais antigos
Fechar
Acertar datas e números é uma vitória de baixo esforço e alto impacto: maior confiança, menos erros e uma experiência global mais suave. Centralize a formatação, armazene valores canônicos, use APIs sensíveis à localidade em todos os lugares onde seus usuários leem ou digitam tempo, dinheiro ou números — e saiba quando pular a localização para dados legíveis por máquina. Com cache e testes adequados, você construirá um sistema robusto que se escala entre localidades sem sacrificar o desempenho.


