ICU: alias de zona horaria y la TZ que informas
Pregunta a ICU qué identidad de zona horaria lee de verdad una página: cómo colapsan los alias, cuándo gana TZ y por qué un desfase fijo iguala una zona con horario de verano.
BotBrowser Team
Quieres la documentación estructurada de Huella digital?
Este artículo forma parte de la biblioteca editorial. Para pasos de configuración, material de referencia y actualizaciones continuas, entra en la sección de docs.
BotBrowser soporta repetir un fixture autorizado del mismo origen bajo un perfil declarado y conservar observaciones acotadas sobre lo que reporta la página, pero no puede hacer que ICU devuelva un nombre con el que no fue compilado, cambiar qué grafía trata como canónica una versión concreta de ICU, ni hacer que una zona de desfase fijo se comporte como una zona con horario de verano solo porque ambas leen el mismo reloj en enero. Esta revisión ejecuta el ICU que viene con el runtime de Node sobre una lista fija de nombres de zona, reporta qué responde Intl.DateTimeFormat().resolvedOptions().timeZone para cada uno, y separa tres cosas que suelen confundirse: el nombre que lee la página, el desfase que renderiza y la variable de entorno que aportó el valor por defecto.
TL;DR
- Casi todas las grafías históricas siguen funcionando, pero ICU responde con su propio nombre canónico:
Asia/KolkatainformaAsia/Calcutta,Europe/KyivinformaEurope/Kiev. UTC,Etc/UTC,Etc/GMT,GMTyZulucolapsan todos aUTC, así que la cadena informada no puede distinguirlos.- Definir
process.env.TZcambia el valor por defecto que informa la siguiente llamada aIntl; una opcióntimeZoneexplícita gana en esa llamada y deja el valor por defecto intacto. Etc/GMT+5yAmerica/New_Yorkrenderizan el mismo reloj en enero y difieren en una hora en julio, así que una sola muestra de invierno no separa un desfase fijo de una zona con horario de verano.- Los desfases no son solo horas enteras:
Asia/KatmanduinformaGMT+5:45yPacific/ChathaminformaGMT+13:45. - Todo esto es propiedad de un par ICU y tzdata concretos; no dice nada sobre cómo puntúa una sesión un sitio.
Contents
- Qué ICU usó esta ejecución
- Pide a ICU el nombre canónico, no la entrada
- Cuenta cuántas entradas caen en un mismo grupo
- Dónde gana de verdad la variable TZ
- Lee el desfase, no solo el nombre
- Dónde un desfase fijo imita una zona con horario de verano
- Leer una identidad de zona sin exagerar
Qué ICU usó esta ejecución
La sonda imprime las versiones de ICU y de tzdata antes que nada, porque cada afirmación de abajo está acotada a ese par. La ejecución informó icu=78.3, tz=2026a, sobre node=v26.9.0. Otra compilación de ICU puede traer otra tabla canónica: una grafía que hoy es alias puede volverse canónica después, y eso ha ocurrido al retirarse nombres obsoletos.
Esa línea de versión es la forma más barata de mantener honesta una revisión de zonas horarias. Un informe que dice que una página "informa Europe/Kiev" sin decir qué ICU y qué tzdata lo produjeron no se puede recomprabar, porque la respuesta es una búsqueda en una tabla que viene con el runtime, no un cálculo del reloj. Registrar ambas versiones convierte una anécdota en una observación reproducible, y además explica por qué dos máquinas con la misma versión de navegador pueden discrepar.
Pide a ICU el nombre canónico, no la entrada
La sonda pasa 21 nombres por new Intl.DateTimeFormat('en-US', { timeZone: zone }).resolvedOptions().timeZone. La propiedad interesante es que la respuesta a menudo no es la entrada:
Asia/Calcutta -> Asia/Calcutta
Asia/Kolkata -> Asia/Calcutta
Europe/Kiev -> Europe/Kiev
Europe/Kyiv -> Europe/Kiev
Asia/Rangoon -> Asia/Rangoon
Asia/Yangon -> Asia/Rangoon
Asia/Saigon -> Asia/Saigon
Asia/Ho_Chi_Minh -> Asia/Saigon
Pacific/Ponape -> Pacific/Ponape
Pacific/Pohnpei -> Pacific/Ponape
Cada par es una zona con una grafía antigua y otra nueva, y ICU resuelve ambas a un único nombre canónico. La dirección del colapso importa: la grafía antigua gana en esta compilación, así que una página que informa Asia/Calcutta bien pudo haberse configurado con Asia/Kolkata, y una que informa Europe/Kiev pudo configurarse con Europe/Kyiv. La cadena informada es evidencia de a qué canoniza este ICU, no de qué pasó el llamador.
El colapso no se limita a nombres obsoletos. UTC, Etc/UTC, Etc/GMT, GMT y Zulu devuelven todos UTC, y US/Eastern devuelve America/New_York. Cinco entradas distintas se vuelven una salida, así que una comparación que solo lee el nombre resuelto no puede distinguirlas, y un registro que guarda solo ese campo ha perdido información que no puede recuperar.
Diez de los 21 nombres probados vuelven exactamente como se escribieron, más de lo que sugieren los pares de arriba: UTC, Etc/GMT+5, Etc/GMT-5, Asia/Calcutta, Europe/Kiev, America/Buenos_Aires, America/New_York, Asia/Rangoon, Pacific/Ponape y Asia/Saigon. El grupo literal no significa «ya canónico»; significa «ya es la grafía que prefiere esta tabla». Las dos variantes GMT están en él y aun así llevan desfases opuestos. Son zonas distintas con desfases opuestos, y la convención POSIX invierte el signo respecto a la costumbre. En la prueba por pares de abajo, Etc/GMT+5 renderiza 07:00 cuando UTC es 12:00, mientras Etc/GMT-5 renderiza 17:00.
Un nombre desconocido falla en voz alta en lugar de normalizarse: Mars/Olympus lanza un RangeError, y también los errores más cercanos Asia/Calcuttta, America/New_Yrok, Europe/Kievv y Europe/Berlin1. Solo varían las mayúsculas, así que asia/kolkata y ASIA/KOLKATA responden ambos Asia/Calcutta; un nombre con espacios extra se rechaza como cualquier otra cadena desconocida. La consecuencia es más estrecha de lo que parece: una zona mal escrita se rechaza en lugar de mapearse en silencio, pero un alias bien escrito se remapea en silencio, y la página no puede distinguir ambos desenlaces solo con la cadena informada. Una página que informa una zona plausible ha pasado una comprobación, que el nombre existe, y no ha fallado ninguna, porque el nombre que informa puede seguir difiriendo del configurado.
Cuenta cuántas entradas caen en un mismo grupo
La estructura de alias se razona mejor contada que leída nombre a nombre. Una segunda pasada agrupa nueve entradas candidatas por la respuesta resuelta:
bucket Asia/Calcutta <- Asia/Kolkata, Asia/Calcutta
bucket REJECTED <- Asia/Bombay
bucket Asia/Dhaka <- Asia/Dacca, Asia/Dhaka
bucket Europe/Kiev <- Europe/Kyiv, Europe/Kiev, Europe/Uzhgorod, Europe/Zaporozhye
De esas cuatro líneas salen cuatro hallazgos. La zona de la India absorbe dos grafías; el nombre largo de Asia/Calcutta es India Standard Time y su desfase GMT+5:30, así que el nombre antiguo no describe otro país. El nombre antiguo de Mumbai, Asia/Bombay, no está en esta tabla y se rechaza de plano, lo que muestra que la tabla no es una historia completa de todos los nombres usados. Daca absorbe la grafía antigua Asia/Dacca. Y la zona de Kiev absorbe cuatro entradas, incluidas dos ciudades del oeste de Ucrania que geográficamente son distintas pero resuelven a la misma entrada canónica.
Ese último grupo es el que rompe el razonamiento ingenuo. Quien asuma que el nombre canónico describe una ubicación leerá Europe/Kiev como "la sesión dice estar en Kiev". En esta compilación también cubre una sesión configurada como Europe/Uzhgorod, que queda a unos 600 kilómetros. La cadena canónica es una entrada de tabla, no una afirmación sobre dónde está la máquina.
El caso rechazado es el contraejemplo útil por otra razón. Si la tabla aceptara todos los alias históricos, un rechazo sería evidencia fuerte de configuración errónea. Como no es así, un rechazo prueba solo que esa grafía concreta falta en esta compilación de ICU. La afirmación segura es estrecha: esta entrada no fue aceptada aquí, así que la zona informada por la página vino por otro camino.
Dónde gana de verdad la variable TZ
La segunda sonda separa el valor por defecto de una opción explícita, que es donde nace la mayor parte de la confusión sobre "¿en qué zona está este navegador?".
TZ as inherited: resolved=America/Toronto toTimeString=07:00:00 GMT-0500 offset=GMT-5
TZ set at runtime: resolved=Asia/Tokyo toTimeString=21:00:00 GMT+0900 offset=GMT+9
explicit option wins over env: Europe/Berlin
default after explicit call still: Asia/Tokyo
invalid zone rejected: RangeError
long name part: Eastern Standard Time
En esas seis líneas se ven tres comportamientos. Mutar process.env.TZ en tiempo de ejecución cambia lo que informa la siguiente llamada a Intl sin opción, tanto en resolvedOptions() como en toTimeString(), así que un valor de entorno no queda congelado al arrancar el proceso en este runtime. Una opción timeZone explícita gana sobre el entorno en esa llamada y deja el valor por defecto intacto para llamadas posteriores, que siguen informando Asia/Tokyo. Y el nombre largo de America/New_York en enero es Eastern Standard Time, un nombre que cambia con la fecha.
La consecuencia para un fixture es que "la página informa la zona X" es ambiguo salvo que el registro diga cómo se aportó X. Un proceso arrancado con TZ definida y sin opción por llamada informa un valor de entorno; una página que pasa una opción explícita informa una decisión tomada en código. Ambas producen la misma cadena en resolvedOptions().timeZone cuando coinciden, y solo la entrada registrada las distingue. Por eso un informe de soporte que cita solo la zona no se puede reproducir: la misma cadena puede venir de cualquiera de los dos caminos, así que la primera pregunta es dónde entró el valor al proceso.
Hay una trampa de segundo orden en la misma zona. Como TZ la lee el runtime y no la página, un valor heredado de un perfil de shell puede sobrevivir a una prueba que creía estar ejercitando un valor derivado del perfil. La sonda muestra la zona heredada como America/Toronto, que vino del entorno del host y no de ninguna opción de página. Un fixture que registra solo la zona resuelta atribuiría eso al perfil bajo prueba. Comprobar el entorno antes de la ejecución, y anotar si la variable se definió al lanzar o se mutó después, es la diferencia entre medir el perfil y medir el shell.
Una opción explícita tampoco reescribe el valor por defecto de forma permanente. La sonda define un valor por defecto con TZ, formatea con una zona explícita, y luego pide a un formateador nuevo el valor por defecto y sigue recibiendo el de entorno. Dos lecturas consecutivas en un mismo script pueden discrepar si una pasó una opción, y una comparación que intercala ambas formas de llamada sin etiquetarlas parecerá que la zona cambió entre lecturas.
Lee el desfase, no solo el nombre
Una identidad de zona suele registrarse como una cadena, pero la misma zona puede renderizar seis formas de nombre según cómo se pregunte:
name shortOffset: GMT-5
name longOffset: GMT-05:00
name short: EST
name long: Eastern Standard Time
name shortGeneric: ET
name longGeneric: Eastern Time
No son evidencia intercambiable. GMT-5 y GMT-05:00 llevan un desfase y ninguna estación, así que se mantienen estables todo el año. EST incrusta la designación de invierno y en julio leería EDT. ET y Eastern Time son agnósticos a la estación y coinciden con ambas. Un informe que registra "la página dice EST" y luego lo compara con una muestra de verano ha comparado dos etiquetas distintas y puede reportar un cambio falso.
Los desfases tampoco son solo horas enteras, una suposición común que falla en zonas reales:
zone Asia/Kathmandu -> Asia/Katmandu GMT+5:45
zone Australia/Eucla -> Australia/Eucla GMT+8:45
zone Pacific/Chatham -> Pacific/Chatham GMT+13:45
zone Asia/Kolkata -> Asia/Calcutta GMT+5:30
Katmandú además canoniza a otra grafía, Asia/Katmandu, y renderiza un desfase de 45 minutos. Chatham renderiza GMT+13:45 en enero porque sus reglas de horario de verano son del hemisferio sur. Un validador que espere desfases en horas enteras, o que espere un signo positivo solo por debajo de cierto tamaño, leerá mal las cuatro filas de la sonda sin reportar ningún error.
La dimensión de idioma añade una tercera variable. El mismo instante y la misma zona se renderizan distinto según el locale, y cambian tanto el orden como la convención de reloj:
locale en-US: hourCycle=h12 hour12=true -> Jan 15, 2026, 7:00 AM
locale en-GB: hourCycle=h23 hour12=false -> 15 Jan 2026, 07:00
locale de-DE: hourCycle=h23 hour12=false -> 15.01.2026, 07:00
locale fr-FR: hourCycle=h23 hour12=false -> 15 janv. 2026, 07:00
locale ja-JP: hourCycle=h23 hour12=false -> 2026/01/15 7:00
en-US es la única fila en ciclo de 12 horas, y ja-JP imprime 7:00 sin cero inicial mientras las otras filas h23 imprimen 07:00. Una comparación que normaliza fechas como cadenas entre locales marcará cuatro diferencias que son todas formato, no estado de sesión.
Dónde un desfase fijo imita una zona con horario de verano
La última sección muestrea dos fechas separadas un año, la forma más barata de separar un desfase fijo de una zona que observa horario de verano:
dst America/New_York: 07:00 / 08:00
dst Etc/GMT+5: 07:00 / 07:00
dst Europe/Berlin: 13:00 / 14:00
America/New_York y Etc/GMT+5 renderizan ambos 07:00 para un instante de enero, así que una sola muestra de invierno no los separa. En julio difieren en una hora, y Europe/Berlin muestra el mismo salto de 13:00 a 14:00. Un informe que captura un instante y declara la zona consistente ha medido coincidencia en una fecha, no identidad de la zona.
La prueba de pares de desfase dice lo mismo desde el otro lado:
pair Etc/GMT+5 vs America/New_York: 07:00 / 07:00
pair Etc/GMT-5 vs Asia/Karachi: 17:00 / 17:00
pair UTC vs Etc/UTC: 12:00 / 12:00
Cada par coincide en el instante muestreado mientras describe zonas distintas. Dos de los tres pares coincidirían también en julio; solo el primero discreparía, porque solo un lado observa horario de verano. Por eso la comparación solo-invierno es la trampa: produce un resultado coincidente para cada par de esta tabla, incluido el que discrepará seis meses después.
Si un fixture solo puede permitirse una muestra, la afirmación honesta es "estas dos coincidieron en esta fecha". Una segunda muestra en la estación opuesta cuesta casi nada y convierte esa afirmación en "estas dos describen las mismas reglas de desfase", que es la que la mayoría de las revisiones de zona realmente quiere hacer.
El hemisferio sur hace la regla de muestreo más estricta que "elige verano e invierno". Pacific/Chatham renderiza GMT+13:45 en enero, que es su verano, así que quien lea ese valor como lectura de invierno tiene la estación al revés. Australia/Eucla mantiene un GMT+8:45 constante porque no cambia nunca. Un par de muestras separadas seis meses las distingue igual, pero solo si el registro guarda el instante junto al valor renderizado en lugar de etiquetar una muestra como "invierno" por suposición.
Elegir los instantes también importa. Una muestra tomada a las 12:00 UTC mantiene las dos fechas limpiamente comparables, mientras una muestra cerca de medianoche local puede mover tanto la fecha renderizada como la hora, y una comparación que solo guarda la hora leerá el cambio de fecha como un día faltante. Guardar el instante ISO exacto elimina ambas ambigüedades sin coste extra.
La zona horaria es un campo junto a dos vecinos que siguen reglas distintas, así que un fixture que registra solo este queda incompleto. Zona horaria, locale e idioma cubre cómo interactúan los tres, y navigator language y localización de aplicaciones muestra que la lista de idiomas tiene su propio problema de canonización, donde un array ordenado se compara contra una sola cabecera negociada.
Leer una identidad de zona sin exagerar
Una lectura de zona soporta una afirmación estrecha: esta compilación de ICU, con esta entrada, informó este nombre canónico y este desfase renderizado para este instante. No afirma qué alias se configuró, si la zona observa horario de verano, qué locale produjo la cadena ni cómo trata un sitio una sesión que la informa.
Guarda cinco campos en el registro y la lectura sigue siendo comprobable. Las versiones de ICU y tzdata, porque la tabla canónica y las reglas de horario de verano vienen con ellas. El nombre de entrada tal como se aportó, porque la canonización lo descarta. El instante muestreado, porque un desfase fijo y una zona con horario de verano coinciden en invierno. La forma de llamada, porque tanto un valor de entorno como una opción explícita rellenan el mismo campo de salida. Y el locale con su hourCycle, porque la cadena renderizada depende de ambos.
La página de docs correspondiente cubre cómo BotBrowser mantiene zona horaria, locale e idioma alineados con el perfil cargado. Esta revisión añade el lado de la lectura: qué hace ICU con los nombres que esos ajustes producen, qué grafías colapsan en una respuesta, cuántas entradas comparten grupo y dónde una sola muestra no distingue dos zonas distintas. Configuración consistente e interpretación correcta de la cadena informada son preguntas separadas, y un fixture que solo registra el nombre resuelto no responde del todo a ninguna.
Para cualquier decisión sobre una sesión real, usa un resultado de integración autorizado del sistema que decide en lugar de extrapolar desde esta sonda. Un nombre canónico registrado en un fixture y un veredicto de producción son tipos distintos de evidencia, y la brecha entre ambos es donde ocurre la exageración.
Practical conclusion
Registra las versiones de ICU y tzdata, el nombre de entrada, el instante muestreado, la forma de llamada y el locale con su ciclo horario. En esta compilación las grafías antiguas ganan la canonización en cuatro grupos, Asia/Bombay se rechaza de plano, cinco entradas tipo UTC colapsan a UTC, TZ define un valor por defecto que una opción explícita anula sin cambiar, los desfases incluyen valores de 45 minutos, y un desfase fijo coincide con una zona con horario de verano en enero y discrepa en julio.
Sources
- MDN: Intl.DateTimeFormat
- MDN: Intl.DateTimeFormat resolvedOptions
- Base de datos de zonas horarias IANA
- Datos de alias de zona horaria de CLDR
- Documentación de zona horaria, locale e idioma de BotBrowser
Next reading
Usa zona horaria, locale e idioma para cómo interactúan los tres campos, y navigator language y localización de aplicaciones para el lado del idioma del mismo perfil.
Este artículo forma parte de la guía: ¿Qué es la huella digital del navegador?
Artículos Relacionados
Zona horaria, locale e idioma en las API del navegador
Huella digitalua-parser-js: Client Hints, bots y un tope de 500
Huella digitalFingerprinting MIME y códecs: señales de soporte multimedia
Huella digitalCómo leer las señales de huella del navegador
PlataformaFormato de fechas y portabilidad de zonas horarias
Huella digitalBotD: qué detectores se ejecutan de verdad
Lleva BotBrowser de la investigación a producción
Usa estas guías para entender el modelo y después avanzar hacia validación multiplataforma, contextos aislados y despliegue de navegador preparado para escalar.