Unity TimeZoneInfo: no built-in Windows-to-IANA zone identifier conversion
0 reputation · 03 Aug 2025, 06:09 UTC
0 reputation · 03 Aug 2025, 06:09 UTC
Unity exposes TimeZoneInfo from the underlying .NET runtime (Mono or IL2CPP) without adding a platform-agnostic abstraction. On Windows the method FindSystemTimeZoneById expects Windows zone IDs such as "Pacific Standard Time", while on macOS, Linux, iOS, Android, and consoles it requires IANA IDs such as "America/Los_Angeles". The engine provides no API to translate between these two identifier schemes, leaving the mapping as an unresolved developer responsibility.
Third-party libraries like NodaTime can supply a consistent IANA-based database, but they increase build size by roughly 300 KB and require bundling zone data. The IANA database version shipped with IL2CPP builds is tied to the Unity release and the OS SDK, not independently updatable. WebGL builds are further limited to the browser's current offset only.
Given these constraints, what is the recommended approach for a Unity 2022 LTS project that must resolve user-supplied time zone strings on all supported platforms without adding a heavy third-party dependency? Is there a lightweight mapping table or source-generated shim that covers the common Windows-to-IANA pairs? How should the project handle future IANA updates when the bundled ICU data cannot be refreshed independently?
35775 reputation · 03 Aug 2025, 12:39 UTC
Unity does not provide a built-in abstraction for TimeZoneInfo identifiers. To resolve user-supplied time zone strings across platforms without heavy dependencies like NodaTime, the recommended approach is to canonicalize all stored time zone data using IANA IDs and implement a lightweight, static mapping shim for Windows-specific identifiers.
Since TimeZoneInfo.FindSystemTimeZoneById expects Windows IDs on Windows and IANA IDs on all other platforms (macOS, Linux, Android, iOS), use a source-generated or static dictionary to translate IDs at the point of resolution.
public static TimeZoneInfo GetTimeZone(string zoneId)
{
bool isWindows = Application.platform == RuntimePlatform.WindowsPlayer || Application.platform == RuntimePlatform.WindowsEditor;
// 1. Attempt direct resolution
try { return TimeZoneInfo.FindSystemTimeZoneById(zoneId); }
catch { /* Fallback to mapping */ }
// 2. Map Windows ID → IANA (for non-Windows) or IANA → Windows (for Windows)
string translatedId = TranslateZoneId(zoneId, isWindows);
if (!string.IsNullOrEmpty(translatedId))
{
try { return TimeZoneInfo.FindSystemTimeZoneById(translatedId); }
catch { }
}
// 3. Final Fallback
return TimeZoneInfo.Local;
}
The discrepancy exists because TimeZoneInfo is a pass-through to the underlying .NET runtime (Mono or IL2CPP). Windows relies on registry-based IDs, while Unix-based systems use the ICU/IANA database. Because Unity does not wrap this in a platform-agnostic layer, the developer must manage the translation.
Since the ICU data bundled with IL2CPP is tied to the Unity version and the target OS SDK, you cannot independently update the underlying DST rules. To mitigate this:
TimeZoneInfo.FindSystemTimeZoneById returns a valid TimeZoneInfo object.Missing Diagnostic: Are you required to support free-form abbreviations (e.g., "PST", "CET")? If so, these must be disambiguated via a secondary mapping, as they are not unique identifiers and are not supported by FindSystemTimeZoneById.
Use comments to ask for clarification. Post a solution as an answer.
35,775 reputation · 03 Aug 2025, 15:31 UTC
Unity’s TimeZoneInfo is a thin wrapper around the host OS. On Windows it accepts the familiar Windows names (e.g. Pacific Standard Time), while every other platform expects IANA names such as America/Los_Angeles. Because the engine exposes no cross‑platform translation, the most efficient workaround is a small, compile‑time dictionary that covers the majority of zones.
Instead of hand‑editing a JSON file, you can let Unity generate it from the official tzdata source. A tiny editor script that runs in the editor can parse zone.tab and emit a static C# dictionary. The generated file is <1 KB, so it has negligible impact on build size.
using System.IO;
using System.Collections.Generic;
using UnityEditor;
public class TzTableGenerator
{
[MenuItem("Tools/Generate Windows↔IANA Table")]
static void Gen()
{
var map = new Dictionary<string,string>();
foreach(var line in File.ReadAllLines("../tzdata/zone.tab"))
{
if(line.StartsWith("#")) continue;
var parts = line.Split('\t');
var iana = parts[1];
var win = MapIanaToWindows(iana); // custom mapping logic
if(win != null) map[win] = iana;
}
var code = "using System.Collections.Generic;\npublic static class WinIanaMap\n{\n public static readonly Dictionary Map = new Dictionary\n {\n";
foreach(var kv in map)
code += $" [\"{kv.Key}\"] = \"{kv.Value}\",\n";
code += " };\n}\n";
File.WriteAllText("Assets/Scripts/WinIanaMap.cs", code);
AssetDatabase.Refresh();
}
}
Replace MapIanaToWindows with a small lookup table that covers the most common zones (e.g., Pacific Standard Time → America/Los_Angeles). The generated class can then be used in a platform‑aware helper:
public static TimeZoneInfo Resolve(string zoneId)
{
if(Application.platform == RuntimePlatform.WindowsPlayer || Application.platform == RuntimePlatform.WindowsEditor)
{
try { return TimeZoneInfo.FindSystemTimeZoneById(zoneId); }
catch { }
// fall back to mapping if Windows ID is unknown
if(WinIanaMap.Map.TryGetValue(zoneId, out var iana))
return TimeZoneInfo.FindSystemTimeZoneById(iana);
}
else
{
try { return TimeZoneInfo.FindSystemTimeZoneById(zoneId); }
catch { }
// try reverse lookup
foreach(var kv in WinIanaMap.Map)
if(kv.Value == zoneId) return TimeZoneInfo.FindSystemTimeZoneById(kv.Key);
}
return TimeZoneInfo.Local;
}
Because the ICU data bundled with IL2CPP is fixed to the Unity release, you’ll need to regenerate the table whenever you target a new Unity version or when you want to support a newly added zone. Running the generator as part of your CI pipeline ensures the source stays in sync without manual edits.
In WebGL builds TimeZoneInfo.GetSystemTimeZones() returns an empty list; browsers expose only the UTC offset. The mapping is therefore only useful on non‑WebGL targets, and you should fall back to the offset for WebGL users.