Intl, dates and time zones
editSet the default locale and time zone
DefaultLocale is the locale of Intl and of the toLocaleString methods when a script does not give one, and TimeZone is the local time zone of Date, Intl.DateTimeFormat and Temporal:
using var engine = new JsEngine(new JsEngineOptions { DefaultLocale = "de-DE", TimeZone = "Europe/Berlin" });
var realm = engine.CreateRealm();
// The default locale and time zone of the scripts
Assert.Equal("1.234,5", realm.Execute("(1234.5).toLocaleString()").AsString());
Assert.Equal("29.10.2026, 13:00:00", realm.Execute("new Date(Date.UTC(2026, 9, 29, 12)).toLocaleString()").AsString());
Assert.Equal("de-DE|Europe/Berlin", realm.Execute("const options = new Intl.DateTimeFormat().resolvedOptions(); `${options.locale}|${options.timeZone}`").AsString());
// The scripts can ask for another locale or time zone
Assert.Equal("1,234.5", realm.Execute("new Intl.NumberFormat('en-US').format(1234.5)").AsString());
Assert.Equal("29 octobre 2026", realm.Execute("new Date(Date.UTC(2026, 9, 29, 12)).toLocaleDateString('fr-FR', { dateStyle: 'long', timeZone: 'UTC' })").AsString());- DefaultLocale is a BCP 47 language tag,
en-USby default. A tag that is not valid, or that matches no locale of the data, is replaced byen-US. - TimeZone is an IANA identifier, such as
Europe/Paris. When it is null, the engine uses the local time zone of the TimeProvider, and maps a Windows identifier to its IANA identifier; a time zone that is not in the database uses its own rules (see Use a custom time zone). An identifier that is not a time zone of the database, such as an offset, throws anInvalidOperationExceptionwhen a script first uses the local time zone. - Set both explicitly on a server: otherwise, the scripts depend on the settings of the machine they run on.
Control the clock
TimeProvider is the clock of Date.now(), new Date() and Temporal.Now, and gives the local time zone when TimeZone is not set. Give a fixed clock to make the results of the scripts reproducible, for instance in tests:
/// <summary>
/// A clock that always gives the same time: <c>Date.now()</c>, <c>new Date()</c> and <c>Temporal.Now</c> use it. The local
/// time zone is the one of the machine, unless one is given.
/// </summary>
public sealed class FixedTimeProvider(DateTimeOffset utcNow, TimeZoneInfo? localTimeZone = null) : TimeProvider
{
public override DateTimeOffset GetUtcNow() => utcNow;
public override TimeZoneInfo LocalTimeZone => localTimeZone ?? base.LocalTimeZone;
}using var engine = new JsEngine(new JsEngineOptions
{
TimeProvider = new FixedTimeProvider(new DateTimeOffset(2026, 10, 29, 23, 30, 0, TimeSpan.Zero)),
TimeZone = "Asia/Tokyo",
});
var realm = engine.CreateRealm();
Assert.Equal("2026-10-29T23:30:00.000Z", realm.Execute("new Date().toISOString()").AsString());
Assert.Equal("2026-10-30", realm.Execute("Temporal.Now.plainDateISO().toString()").AsString());
Assert.Equal(8, realm.Execute("new Date().getHours()").AsNumber());The same TimeProvider measures the delays of the timeout jobs of the engine (Atomics.waitAsync).
Use a custom time zone
When TimeZone is not set and the local time zone of the TimeProvider is not in the IANA database, nor a Windows identifier of one of its zones, the engine uses the rules of the TimeZoneInfo. This is the case of a zone created by TimeZoneInfo.CreateCustomTimeZone, for instance to test the scripts in a given time zone:
// A time zone that is not in the IANA database: +05:00, and +06:00 from the last Sunday of March to the last Sunday of October
var rule = TimeZoneInfo.AdjustmentRule.CreateAdjustmentRule(
DateTime.MinValue.Date,
DateTime.MaxValue.Date,
TimeSpan.FromHours(1),
TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 2, 0, 0), month: 3, week: 5, DayOfWeek.Sunday),
TimeZoneInfo.TransitionTime.CreateFloatingDateRule(new DateTime(1, 1, 1, 3, 0, 0), month: 10, week: 5, DayOfWeek.Sunday));
var timeZone = TimeZoneInfo.CreateCustomTimeZone("Test/Zone", TimeSpan.FromHours(5), "Test zone", "Test standard time", "Test daylight time", [rule]);
using var engine = new JsEngine(new JsEngineOptions
{
TimeProvider = new FixedTimeProvider(new DateTimeOffset(2026, 7, 1, 12, 0, 0, TimeSpan.Zero), timeZone),
});
var realm = engine.CreateRealm();
Assert.Equal(18, realm.Execute("new Date().getHours()").AsNumber());
Assert.Equal(-300, realm.Execute("new Date(2026, 0, 1).getTimezoneOffset()").AsNumber());
Assert.Equal("Test/Zone", realm.Execute("Temporal.Now.timeZoneId()").AsString());- A zone without daylight saving time is an offset time zone: its identifier is its offset, such as
+05:00. - A zone with daylight saving time keeps the identifier of the
TimeZoneInfo. The engine reads the transitions of its adjustment rules, and its last rule repeats every year when it never ends.Temporal.Now.timeZoneId()andresolvedOptions().timeZonereturn this identifier, but the scripts cannot pass it toTemporalorIntl: it is not a time zone of the database. - Set TimeZone to
UTCto use UTC instead of such a zone.
Parse dates
Date.parse and new Date(string) read the date time string format of ECMAScript (2026-10-29T13:00:00.000+01:00), then the formats of toString and toUTCString, then the legacy formats that V8 reads (Oct 29 2026 13:00 GMT+0100, 10/29/2026, 2026-10-29 13:00:00...). Like Node.js, the date time string format also accepts:
- a lowercase
tseparator orzdesignator:2026-10-29t13:00:00z; - an offset without colon:
2026-10-29T13:00:00.000+0100.
A string without offset is in the local time zone, except a date alone (2026-10-29), which is UTC. The parsing does not depend on the culture of .NET: a string that Node.js does not read, such as 29/10/2026, is NaN.
The same results on every machine
The engine does not use the globalization of .NET or of the operating system: it embeds its own copy of the data.
- Locales: the CLDR data of the locales of the
modernandmoderatecoverage levels, with their parents. It coversIntl.Collator,DateTimeFormat,DisplayNames,DurationFormat,ListFormat,Locale,NumberFormat,PluralRules,RelativeTimeFormatandSegmenter, and thetoLocaleStringmethods. - Time zones: the IANA time zone database, with the history of each zone, for
Date,IntlandTemporal. - Unicode: the character data of the regular expressions, of
normalizeand of the case conversions.Intl.Segmentersplits the words of Chinese, Japanese, Thai, Lao, Khmer and Burmese with the word lists of ICU.
So the results are the same on Windows, Linux and macOS, and in an application that runs in invariant globalization mode. They change only when the data of the library is updated. See Data versions and output differences for the versions of the data.
The cost of the data
The engine reads the data when a script first needs it, and only the parts it uses: a script that does not use Intl or the locale-sensitive methods pays nothing. What it reads is kept for the life of the process and shared by every engine, so only the first use of a locale in the process pays for it.
- The first collator of a locale costs the most:
Intl.CollatorandlocaleComparebuild the collation of the locale from the rules of CLDR. The Chinese collations are the largest, and the first one takes on the order of 100 ms. - Using the formatters, the plural rules, the display names and the collator in about 80 locales keeps about 25 MB.
To keep this cost out of the first request of a server, create the collators and the formatters of the locales you expect when the application starts:
// At startup: load the data of the locales that the scripts will use, for every engine of the process
using (var warmUp = new JsEngine())
{
warmUp.CreateRealm().Execute("""
for (const locale of ['en', 'fr', 'zh']) {
new Intl.Collator(locale).compare('a', 'b');
new Intl.NumberFormat(locale).format(1);
new Intl.DateTimeFormat(locale).format(0);
}
""");
}
// Later: the engines of the requests reuse the data
using var engine = new JsEngine();
Assert.Equal("a,b,c", engine.CreateRealm().Execute("['c', 'a', 'b'].sort(new Intl.Collator('fr').compare).join()").AsString());