Estado: Aceptado
Fecha: 2026-06-02
Contexto: Plataforma Evolith — Estándar Raíz (aplica a todos los sistemas hijos: UMS y productos futuros)
Reemplaza: Ninguno
Relacionado: ADR-0052 (Inmutabilidad del Audit Trail), ADR-0056 (Clean Architecture Frontend)
Los sistemas distribuidos que abarcan múltiples países y zonas horarias deben manejar fechas, horas y preferencias de idioma de manera consistente en todas las capas. Sin un estándar explícito, los equipos toman decisiones incompatibles: algunos almacenan hora local, otros usan formateo por defecto del browser, otros codifican identificadores de idioma en duro. Cuando los datos cruzan límites de sistemas o se muestran a usuarios en zonas horarias distintas, estas inconsistencias generan errores de visualización, discrepancias en auditorías y fallos de cumplimiento.
Tres preocupaciones independientes deben abordarse juntas porque interactúan en la inicialización de la sesión:
Todos los sistemas hijos de Evolith (actual: UMS; futuros: facturación, logística, operaciones portuarias, integraciones ERP) deben seguir el mismo estándar para que los trails de auditoría, delegaciones y eventos entre sistemas sean interpretables sin ambigüedad temporal.
Regla: Toda fecha o timestamp almacenado en cualquier tabla de base de datos, evento de dominio, mensaje outbox o cuerpo de respuesta API debe ser UTC.
DateTime.UtcNow o DateTimeOffset.UtcNow. Nunca usar DateTime.Now.UtcNow al nivel de dominio y Utc al nivel de persistencia (ej: CreatedAtUtc, DeletedAtUtc).ValueConverter<DateTime, DateTime> en todas las propiedades DateTime que fuerce DateTimeKind.Utc en lectura, previniendo el almacenamiento silencioso en hora local en SQLite (que no almacena información de zona horaria).Z (ej: "2026-06-02T15:30:00Z") para hacer UTC explícito para los consumidores.new Date(isoString) — JavaScript siempre interpreta cadenas con sufijo Z como UTC internamente. Nunca aplicar offsets manualmente a valores UTC antes de almacenar.Justificación: UTC es el único ancla sin ambigüedad para sistemas distribuidos. Las horas locales introducen brechas por horario de verano, ambigüedad de reloj de pared e inconsistencia entre datacenters. Almacenar UTC y convertir en el momento de visualización es el estándar de la industria (IANA, RFC 3339, ISO 8601).
Regla: Al hacer login, el frontend detecta el timezone IANA del browser usando Intl.DateTimeFormat().resolvedOptions().timeZone y lo almacena en el estado de sesión autenticado. Se envía al backend como header X-Timezone en cada llamada API.
Intl.DateTimeFormat().resolvedOptions().timeZone retorna un identificador IANA (ej: "America/Lima", "Europe/Madrid"). Soportado en todos los browsers modernos.UI_TIMEZONE_DEFAULT (configurado por el admin del tenant, ej: "America/Lima")"UTC" (último recurso únicamente)undefined.X-Timezone se valida contra la base de datos de zonas horarias IANA y se almacena en el contexto del request. Se usa para formateo de fechas del lado del servidor (ej: generación de reportes, timestamps en emails).Intl.DateTimeFormat con una opción timeZone explícita.Justificación: El browser conoce la zona horaria real del usuario sin requerir configuración manual. El parámetro del tenant cubre casos borde (terminales kiosko, sesiones SSO desde browsers externos) mientras sigue siendo el default sensato para la mayoría de usuarios en la misma región que el tenant.
Regla: El idioma de la UI se resuelve en la inicialización de la sesión en este orden:
| Prioridad | Fuente | Mecanismo |
|---|---|---|
| 1 (más alta) | Header HTTP Accept-Language del browser |
Leído por CultureMiddleware del backend, validado contra locales soportados |
| 2 | Parámetro de tenant UI_LANGUAGE_DEFAULT |
Leído desde el caché de configuración en memoria al hacer login |
| 3 (más baja) | Default hardcodeado de la plataforma "es" |
Fallback final |
CultureMiddleware del backend lee Accept-Language, extrae el código de idioma primario (primeros 2 caracteres, en minúsculas: "es-PE" → "es"), lo valida contra la lista de locales soportados y establece CultureInfo.CurrentCulture para el request.LoginSuccessResponse.Language Y en SessionParameters.DefaultLanguage.sessionParameters.defaultLanguage de la respuesta y llama useI18nStore.setLanguage(lang) para inicializar el idioma activo de la UI para toda la sesión.["es", "en"].formatDate, formatDateTime y formatRelativeTime deben recibir el locale activo del store i18n. Las funciones no deben usar un locale hardcodeado.Justificación: La preferencia del browser refleja lo que el usuario configuró a nivel de SO. Respetarla no requiere configuración manual y cubre la mayoría de casos. El admin del tenant puede controlar el fallback para entornos donde la configuración del browser no es representativa (kioscos compartidos, dispositivos gestionados).
Intl (disponible en todos los browsers modernos; no es una restricción real).X-Timezone añade un overhead menor a cada request (una cadena, negligible).DateTime en entidades de dominio usan DateTime.UtcNow.ValueConverter UTC para propiedades DateTime.Z.CultureMiddleware lee Accept-Language y valida contra locales soportados.Language (resuelto) y SessionParameters.DefaultTimezone.Intl.DateTimeFormat().resolvedOptions().timeZone al hacer login.X-Timezone.sessionParameters.defaultLanguage al hacer login.formatDate/formatDateTime pasan el locale activo y el timezone de la sesión.