Por qué las fechas y los números necesitan localización
TABLE OF CONTENTS
Si envías a nivel mundial, la misma cadena puede significar cosas diferentes para distintos usuarios: 12/01/2025, 1.234, 1,234, 12:00, 00:00. Esas pequeñas diferencias no son cosméticas: afectan la confianza, la comprensión e incluso el cumplimiento legal. Esta guía explica por qué las fechas y los números deben ser localizados, qué cambia realmente según la localidad y cómo implementarlo correctamente en todo tu producto.
Qué Cambia Según la Localidad
- Fechas: orden (MDY vs DMY vs YMD), separadores, nombres de meses/días; algunos mercados usan calendarios no gregorianos.
- Horas: relojes de 12 horas vs 24 horas, marcadores AM/PM, zonas horarias, reglas de horario de verano, inicio de la semana (domingo vs lunes).
- Números: separadores decimales (
.vs,), estilos de agrupación (1,234vs1.234vs1 234), agrupación india (1,23,456), espacios de no separación, estilos de menos/más (guion vs verdadero menos−). - Moneda: símbolo vs código ISO, colocación del símbolo (prefijo/sufijo), espaciado estrecho vs amplio, precisión decimal (0, 2 o 3+), negativos contables (por ejemplo,
(1 234,56 €)). - Porcentaje y unidades: espaciado y colocación del signo (
50%vs50 %), nombres locales para unidades, métrico vs imperial.
Impacto en el Negocio
- Confianza y conversión: Precios, tarifas y fechas que “parecen incorrectos” reducen las compras y aumentan la rotación.
- Riesgo operativo: Las fechas mal interpretadas pueden cambiar reservas o plazos; los errores de análisis rompen facturas, exportaciones y análisis.
- Cumplimiento: Los documentos financieros con formatos incorrectos pueden violar las reglas de facturación, impuestos o informes.
- Carga de soporte: Los usuarios abren tickets para aclarar horas, monedas y entradas de números que no coinciden con las expectativas.
Incidente en el Mundo Real
Una plataforma europea de reservas de viajes mostró las fechas de salida como 01/03/2024 sin contexto de localidad. Los clientes de EE. UU. interpretaron esto como “3 de enero” mientras que los clientes del Reino Unido y Europa lo leyeron como “1 de marzo”.
Impacto:
- El 12% de las reservas internacionales se realizaron para las fechas incorrectas
- El servicio al cliente recibió más de 3,400 tickets de aclaración en una semana
- $2.3M en reembolsos y tarifas de reprogramación
- El 8% de los clientes afectados se cambiaron a la competencia
Causa raíz: Formato DD/MM/YYYY codificado en las confirmaciones por correo electrónico mientras que el sitio web utilizaba MM/DD/YYYY para los usuarios de EE. UU. La solución requirió solo 3 horas de tiempo de ingeniería: la ambigüedad costó 800 veces más que la prevención.
Lección: Las fechas sin contexto son una bomba de tiempo para cualquier producto global.
Errores Comunes
- Formatos codificados:
MM/DD/YYYYo1,234.56integrados en la interfaz de usuario, correos electrónicos, PDFs o exportaciones CSV. - Almacenamiento de cadenas localizadas: Guardar “Dec 1, 2025” en lugar de tiempo ISO causa errores de análisis y desajuste de zona horaria.
- Análisis ingenuo: Usar configuraciones regionales del servidor por defecto; asumir separadores o relojes de 12/24 horas.
- Concatenación de cadenas: Construir “monto + moneda” como
"$" + 1234.56en lugar de formateo consciente de la localidad. - Ambigüedad de zona horaria: Mostrar horas locales sin zonas explícitas para acciones sensibles al tiempo.
- Inconsistencia del navegador: Safari, Firefox y Chrome pueden formatear fechas/números de manera diferente; siempre probar en todos los navegadores objetivo.
- Brechas en el renderizado del lado del servidor: Node.js sin datos completos de ICU (
node --with-intl=full-icu) produce formateo incompleto o incorrecto. - Trampas de rendimiento: Crear nuevas instancias de
Intl.*en bucles o ciclos de renderizado en lugar de almacenar en caché los formateadores.
Mejores Prácticas
- Almacenar valores canónicos: Fechas/horas como ISO‑8601 en UTC; dinero en unidades menores (centavos) o decimales de alta precisión.
- Renderizar por configuración regional del usuario: Utilizar APIs conscientes de la configuración regional (por ejemplo,
Intl.DateTimeFormat,Intl.NumberFormat) para la visualización. - Validar y analizar por configuración regional: Aceptar la entrada de configuración regional cuando sea apropiado; mostrar marcadores de posición/ejemplos para guiar a los usuarios.
- Ser explícito con el tiempo: Mostrar abreviaturas de zona horaria o compensaciones; permitir la selección del usuario donde los resultados dependen del tiempo.
- Claridad de moneda: Preferir códigos ISO donde exista ambigüedad; respetar la colocación de símbolos y el espaciado por configuración regional.
- Centralizar el formato: Una capa de utilidad para todas las superficies (UI, correos electrónicos, PDFs, exportaciones) para asegurar la consistencia.
- Cachear formateadores: Las llamadas al constructor
Intl.*son costosas; crear una vez y reutilizar instancias por configuración regional.
Cuándo NO Localizar
No todo debe ser localizado. Aquí hay escenarios donde los formatos consistentes y legibles por máquina son mejores:
- Exportaciones legibles por máquina: Los archivos CSV consumidos por tuberías de datos o herramientas de análisis deben usar un formato fijo (por ejemplo, fechas ISO-8601, punto como separador decimal). Documentar este formato claramente en los encabezados de exportación o archivos README.
- Respuestas de API: Las APIs REST/GraphQL deben devolver fechas como cadenas ISO-8601 y números en formato JSON estándar. Permitir que los clientes manejen la localización según las preferencias de sus usuarios.
- Registros internos y métricas: Los registros, paneles de monitoreo y consultas de bases de datos se benefician de formatos consistentes para el análisis, la agregación y las alertas. Usar ISO-8601 y formatos numéricos estándar.
- Identificadores canónicos: Los IDs de transacción, números de pedido o referencias que los usuarios puedan necesitar comunicar a través de configuraciones regionales deben evitar el formato específico de la configuración regional.
Regla general: Localizar para humanos que leen contenido; usar formatos estándar para máquinas que procesan datos.
Plan de Implementación
- Superficies de auditoría: Fechas de inventario, horas, números, moneda en la interfaz de usuario, correos electrónicos, PDFs, exportaciones CSV/Excel, analíticas, registros.
- Definir locales: Listar los locales soportados y sus valores predeterminados; especificar la política de 12/24 horas por local.
- Construir utilidades: Envolver las APIs
Intl.*(o una biblioteca) con ayudantes compartidos y pruebas; implementar almacenamiento en caché del formateador. - Estrategia de entrada: Actualizar formularios para aceptar y validar formatos locales; almacenar valores canónicos junto con la entrada en bruto.
- Reglas de contenido: Documentar el estilo para fechas cortas/largas, fechas relativas, visualización de moneda y formato de porcentaje.
- Despliegue: Convertir primero las superficies de mayor tráfico y mayor riesgo; implementar detrás de un flag de característica si es necesario.
Estrategia de Pruebas
Más allá del control de calidad manual, implementar pruebas automatizadas para prevenir regresiones:
- Pruebas de instantáneas: Congelar salidas formateadas para cada local soportado. Los cambios desencadenan una revisión para asegurar que son intencionales.
- Pruebas de ida y vuelta: Verificar que
format → parse → formatproduce resultados estables. Detecta pérdida de precisión o formatos ambiguos. - Casos límite: Probar números negativos, cero, números muy grandes (miles de millones/billones), transiciones de horario de verano, años bisiestos y fechas límite (1 de enero, 31 de diciembre).
- Validación entre navegadores: Pruebas automatizadas en Chrome, Firefox, Safari y Edge para detectar diferencias de renderizado.
- Degradación de locales: Verificar degradación elegante cuando se solicitan locales no soportados.
- Puntos de referencia de rendimiento: Medir el costo de creación del formateador; asegurar que el almacenamiento en caché funcione en producción.
Consejos de Rendimiento
Las APIs Intl.* son poderosas pero pueden ser lentas si se usan incorrectamente. Siga estas pautas:
// ❌ MALO: Crea un nuevo formateador en cada llamada
function formatPrice(amount, locale) {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'USD'
}).format(amount);
}
```js
// ✅ BUENO: Cachear formateadores por localización
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);
}
- Cachear instancias de formateadores: La construcción de
Intl.*es costosa (~1–5ms); formatear con una instancia existente es rápido (~0.01ms). - Evitar bucles: Nunca crear formateadores dentro de
map(),forEach(), o bucles de renderizado. - Carga diferida de datos de localización: Para aplicaciones web, considera la división de código para cargar solo las localizaciones requeridas, reduciendo el tamaño del paquete.
- Lado del servidor: Usar un único formateador por ciclo de solicitud/respuesta; evitar crear por ítem en grandes conjuntos de datos.
Lista de Verificación de QA
- Las fechas son inequívocas en
en-US,en-GB,de-DE,fr-FR,hi-IN,ja-JP,ar-EG. - El tiempo utiliza la convención correcta de 12/24 horas; los marcadores AM/PM aparecen donde se espera; las zonas horarias se muestran para flujos críticos de tiempo.
- Los números usan los separadores decimales y de agrupación correctos; espacios no separables donde es estándar (por ejemplo,
fr-FR). - La moneda muestra el símbolo/código correcto, la colocación y la precisión decimal para cada localización; los negativos se muestran correctamente.
- Las entradas aceptan y analizan datos de localización del usuario (o imponen claramente un formato compatible); los mensajes de validación son conscientes de la localización.
- Las exportaciones (CSV/PDF) coinciden con las expectativas de localización o documentan claramente formatos fijos para el consumo por máquina.
- Compatibilidad entre navegadores: Las fechas, horas y números se renderizan de manera consistente en Chrome, Firefox, Safari y Edge.
- Rendimiento: No se crean formateadores en bucles; las instancias en caché se reutilizan en los renderizados.
Ejemplos de Código
JavaScript
// Fechas
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 (día/mes/año)
console.log(new Intl.DateTimeFormat('ja-JP', { dateStyle: 'medium', timeZone: 'UTC' }).format(date));
// → 2025/01/12
// Tiempos con visualización de zona horaria
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
// Números
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 como separador de grupo)
// Numeración india
console.log(new Intl.NumberFormat('hi-IN').format(1234567.89)); // → 12,34,567.89
// Moneda
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 (sin decimales)
// Porcentaje y unidades
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 %
// Ejemplo de almacenamiento en caché de formateadores
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
```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
# Fechas
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
# Horas con zona horaria
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
# Numeración india
print(format_number(1234567.89, locale='hi_IN')) # → 12,34,567.89
# Moneda
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
# Porcentaje
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;
// Fechas
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
// Horas
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
// Moneda
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
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
// Moneda (usando el paquete number)
fmt.Println(pUS.Sprint(number.Decimal(n, number.Scale(2)))) // → 1,234.56
// Fechas - El paquete time de Go utiliza layouts en lugar de formato de locales
// Para un formato de fecha completo i18n, use github.com/goodsign/monday o 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: La biblioteca estándar de Go tiene soporte limitado para locales. Para uso en producción, considere:
golang.org/x/textpara formateo de númerosgithub.com/goodsign/mondaypara formateo de fecha/hora localizadogithub.com/bojanz/currencypara manejo de moneda
Recursos Útiles
- Estándares: Unicode CLDR (estándar de datos de configuración regional), IETF BCP 47 (identificadores de configuración regional)
- Documentación: Referencia MDN Intl
- Bibliotecas:
- Luxon (biblioteca moderna de fecha/hora con i18n)
- date-fns con módulos de configuración regional
- Globalize.js (i18n integral basado en CLDR)
- Format.js (i18n enfocado en React con sintaxis de mensajes ICU)
- Pruebas: Polyfill Intl para navegadores antiguos
Cierre
Obtener las fechas y números correctamente es una victoria de bajo esfuerzo y alto impacto: mayor confianza, menos errores y una experiencia global más fluida. Centraliza el formato, almacena valores canónicos, usa APIs conscientes de la configuración regional en todos los lugares donde tus usuarios lean o escriban tiempo, dinero o números, y sabe cuándo omitir la localización para datos legibles por máquina. Con un almacenamiento en caché y pruebas adecuados, construirás un sistema robusto que se escala a través de configuraciones regionales sin sacrificar el rendimiento.


