From 2cc2d5c9eb4ac6087a76c2e5f06ea046cfc1b01e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Tue, 2 Jun 2026 09:28:13 +0200 Subject: [PATCH 1/2] feat: add icu::set_default_time_zone / get_default_time_zone Expose ICU's default time zone so embedders can install a specific IANA zone (e.g. from the TZ environment variable) instead of relying solely on host detection via DateTimeConfigurationChangeNotification(Redetect). On Windows, ICU's host detection reads the time zone from the OS and ignores TZ, so Redetect cannot honor TZ there. set_default_time_zone wraps icu::TimeZone::adoptDefault(createTimeZone(id)) so the requested zone is resolved on every platform. get_default_time_zone returns the current default's IANA id. WIP: not yet built/tested against a full V8 build locally. --- src/binding.cc | 20 ++++++++++++++++++++ src/icu.rs | 28 ++++++++++++++++++++++++++++ tests/test_api.rs | 25 +++++++++++++++++++++++++ 3 files changed, 73 insertions(+) diff --git a/src/binding.cc b/src/binding.cc index 3bc6e87985..5364ee83e6 100644 --- a/src/binding.cc +++ b/src/binding.cc @@ -14,6 +14,8 @@ #include "libplatform/libplatform.h" #include "support.h" #include "unicode/locid.h" +#include "unicode/timezone.h" +#include "unicode/unistr.h" #include "v8-callbacks.h" #include "v8-cppgc.h" #include "v8-fast-api-calls.h" @@ -4286,6 +4288,24 @@ void icu_set_default_locale(const char* locale) { icu::Locale::setDefault(icu::Locale(locale), status); } +size_t icu_get_default_timezone(char* output, size_t output_len) { + icu::UnicodeString id; + std::unique_ptr tz(icu::TimeZone::createDefault()); + tz->getID(id); + icu_77::CheckedArrayByteSink sink(output, static_cast(output_len)); + id.toUTF8(sink); + assert(!sink.Overflowed()); + return sink.NumberOfBytesAppended(); +} + +void icu_set_default_timezone(const char* timezone) { + // Takes ownership of the created TimeZone and installs it as the process + // wide default, so ICU (and therefore V8's Date implementation) resolves + // the given IANA id regardless of how the host reports its time zone. + icu::TimeZone::adoptDefault( + icu::TimeZone::createTimeZone(icu::UnicodeString::fromUTF8(timezone))); +} + } // extern "C" // v8::PropertyDescriptor diff --git a/src/icu.rs b/src/icu.rs index 7737f1aaca..99328996d8 100644 --- a/src/icu.rs +++ b/src/icu.rs @@ -5,6 +5,8 @@ use std::ffi::CString; unsafe extern "C" { fn icu_get_default_locale(output: *mut char, output_len: usize) -> usize; fn icu_set_default_locale(locale: *const char); + fn icu_get_default_timezone(output: *mut char, output_len: usize) -> usize; + fn icu_set_default_timezone(timezone: *const char); fn udata_setCommonData_77(this: *const u8, error_code: *mut i32); } @@ -69,3 +71,29 @@ pub fn set_default_locale(locale: &str) { icu_set_default_locale(c_str.as_ptr()); } } + +/// Returns the IANA id of ICU's current default time zone (e.g. +/// `"America/New_York"`). +pub fn get_default_time_zone() -> String { + let mut output = [0u8; 1024]; + let len = unsafe { + icu_get_default_timezone(output.as_mut_ptr() as *mut char, output.len()) + }; + std::str::from_utf8(&output[..len]).unwrap().to_owned() +} + +/// Sets ICU's default time zone from an IANA id (e.g. `"UTC"`, +/// `"Asia/Manila"`). This makes `Date` resolve the given zone on every +/// platform, including Windows, where ICU otherwise reads the host time +/// zone from the OS and ignores the `TZ` environment variable. +/// +/// After calling this, notify each isolate via +/// [`crate::Isolate::date_time_configuration_change_notification`] with +/// [`crate::TimeZoneDetection::Skip`] so cached values are refreshed +/// without re-detecting (and overwriting) the zone just set here. +pub fn set_default_time_zone(timezone: &str) { + unsafe { + let c_str = CString::new(timezone).expect("Invalid timezone"); + icu_set_default_timezone(c_str.as_ptr()); + } +} diff --git a/tests/test_api.rs b/tests/test_api.rs index bb3ae8d3ea..a796b55a4c 100644 --- a/tests/test_api.rs +++ b/tests/test_api.rs @@ -9812,6 +9812,31 @@ fn icu_set_common_data_fail() { ); } +#[test] +fn icu_default_time_zone() { + let _setup_guard = setup::parallel_test(); + v8::icu::set_default_time_zone("America/New_York"); + assert_eq!(v8::icu::get_default_time_zone(), "America/New_York"); + + let isolate = &mut v8::Isolate::new(Default::default()); + isolate + .date_time_configuration_change_notification(v8::TimeZoneDetection::Skip); + { + v8::scope!(let scope, isolate); + let context = v8::Context::new(scope, Default::default()); + let scope = &mut v8::ContextScope::new(scope, context); + // 2020-06-26T07:00:00Z is 03:00 in America/New_York (EDT, UTC-4). + let source = r#" + new Date(Date.UTC(2020, 5, 26, 7, 0, 0)).getHours(); + "#; + let value = eval(scope, source).unwrap(); + assert_eq!(value.number_value(scope).unwrap(), 3.0); + } + + // Restore the default so other tests aren't affected by ordering. + v8::icu::set_default_time_zone("UTC"); +} + #[test] fn icu_format() { let _setup_guard = setup::parallel_test(); From 05bbab2b48fcc67aa1fa5e65d1ec54ee2f84d565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Tue, 2 Jun 2026 09:39:32 +0200 Subject: [PATCH 2/2] test: run icu_default_time_zone sequentially and restore original zone The test mutates the process-wide ICU default time zone, which affects how other tests render Date values, so it must hold the exclusive lock (sequential_test) rather than the shared one. Also restore the original zone afterwards so test ordering doesn't matter. --- tests/test_api.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_api.rs b/tests/test_api.rs index a796b55a4c..8e9b63c19c 100644 --- a/tests/test_api.rs +++ b/tests/test_api.rs @@ -9814,7 +9814,11 @@ fn icu_set_common_data_fail() { #[test] fn icu_default_time_zone() { - let _setup_guard = setup::parallel_test(); + // Mutates the process-wide ICU default time zone, which also affects how + // other tests' `Date`s render, so take the exclusive lock. + let _setup_guard = setup::sequential_test(); + let original = v8::icu::get_default_time_zone(); + v8::icu::set_default_time_zone("America/New_York"); assert_eq!(v8::icu::get_default_time_zone(), "America/New_York"); @@ -9833,8 +9837,8 @@ fn icu_default_time_zone() { assert_eq!(value.number_value(scope).unwrap(), 3.0); } - // Restore the default so other tests aren't affected by ordering. - v8::icu::set_default_time_zone("UTC"); + // Restore the original default so test ordering doesn't matter. + v8::icu::set_default_time_zone(&original); } #[test]