-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow TimeZoneInfo display names to use any of the installed Windows languages #52992
Changes from 5 commits
dd58776
02d0bf9
95f21f2
5582d75
7eb9d6d
0099d71
b6006cf
a0405cf
77cfea3
1618aa9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -36,6 +36,8 @@ public sealed partial class TimeZoneInfo | |
private const int MaxKeyLength = 255; | ||
private const string InvariantUtcStandardDisplayName = "Coordinated Universal Time"; | ||
|
||
private static readonly Dictionary<string, string> s_FileMuiPathCache = new(); | ||
|
||
private sealed partial class CachedData | ||
{ | ||
private static TimeZoneInfo GetCurrentOneYearLocal() | ||
|
@@ -736,6 +738,102 @@ private static bool TryCompareTimeZoneInformationToRegistry(in TIME_ZONE_INFORMA | |
} | ||
} | ||
|
||
/// <summary> | ||
/// Helper function for getting the MUI path for a given resource and culture, using a cache to prevent duplicating work. | ||
/// Searches the installed OS languages for either an exact matching culture, or one that has the same parent. | ||
/// If not found, uses the preferred default OS UI language, to align with prior behavior. | ||
/// </summary> | ||
private static string GetCachedFileMuiPath(string filePath, CultureInfo cultureInfo) | ||
{ | ||
string? result; | ||
string cacheKey = $"{cultureInfo.Name};{filePath}"; | ||
|
||
lock (s_FileMuiPathCache) | ||
{ | ||
if (s_FileMuiPathCache.TryGetValue(cacheKey, out result)) | ||
{ | ||
return result; | ||
} | ||
} | ||
|
||
result = GetFileMuiPath(filePath, cultureInfo); | ||
|
||
lock (s_FileMuiPathCache) | ||
{ | ||
s_FileMuiPathCache.TryAdd(cacheKey, result); | ||
} | ||
|
||
return result; | ||
} | ||
|
||
/// <summary> | ||
/// Helper function for getting the MUI path for a given resource and culture. | ||
/// Searches the installed OS languages for either an exact matching culture, or one that has the same parent. | ||
/// If not found, uses the preferred default OS UI language, to align with prior behavior. | ||
/// </summary> | ||
private static unsafe string GetFileMuiPath(string filePath, CultureInfo cultureInfo) | ||
{ | ||
char* fileMuiPath = stackalloc char[Interop.Kernel32.MAX_PATH]; | ||
char* language = stackalloc char[Interop.Kernel32.LOCALE_NAME_MAX_LENGTH]; | ||
uint fileMuiPathLength = Interop.Kernel32.MAX_PATH; | ||
uint languageLength = Interop.Kernel32.LOCALE_NAME_MAX_LENGTH; | ||
ulong enumerator = 0; | ||
|
||
while (true) | ||
{ | ||
// Search all installed languages. The enumerator is re-used between loop iterations. | ||
bool succeeded = Interop.Kernel32.GetFileMUIPath( | ||
Interop.Kernel32.MUI_USE_INSTALLED_LANGUAGES, | ||
filePath, language, ref languageLength, | ||
fileMuiPath, ref fileMuiPathLength, ref enumerator); | ||
|
||
if (!succeeded) | ||
{ | ||
// Recurse to search using the parent of the desired culture. | ||
if (cultureInfo.Parent.Name != string.Empty) | ||
{ | ||
return GetFileMuiPath(filePath, cultureInfo.Parent); | ||
} | ||
|
||
// Final fallback, using the preferred installed UI language. | ||
enumerator = 0; | ||
succeeded = Interop.Kernel32.GetFileMUIPath( | ||
Interop.Kernel32.MUI_USER_PREFERRED_UI_LANGUAGES, | ||
filePath, language, ref languageLength, | ||
fileMuiPath, ref fileMuiPathLength, ref enumerator); | ||
|
||
if (succeeded) | ||
{ | ||
return new string(fileMuiPath); | ||
} | ||
|
||
// Shouldn't get here, as there's always at least one language installed. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would ideally be a Debug.Fail. |
||
return string.Empty; | ||
} | ||
|
||
// Lookup succeeded. Check for exact match to the desired culture. | ||
var lang = new string(language); | ||
if (string.Equals(lang, cultureInfo.Name, StringComparison.OrdinalIgnoreCase)) | ||
Comment on lines
+818
to
+819
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't need to allocate the string just to do this comparison. Presumably exact match is common and we'd want to delay the creation of lang until after this if block? This can instead be: ReadOnlySpan<char> lang = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(language);
if (lang.Equals(cultureInfo.Name, StringComparison.OrdinalIgnoreCase) Then below if we need lang for the GetCultureInfo call, that can just be |
||
{ | ||
return new string(fileMuiPath); | ||
} | ||
|
||
// Check for match of any parent of the language returned to the desired culture. | ||
var ci = CultureInfo.GetCultureInfo(lang); | ||
while (ci.Parent.Name != string.Empty) | ||
{ | ||
if (ci.Parent.Name.Equals(cultureInfo.Name, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
return new string(fileMuiPath); | ||
} | ||
|
||
ci = ci.Parent; | ||
} | ||
|
||
// Not found yet. Continue with next iteration. | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Helper function for retrieving a localized string resource via MUI. | ||
/// The function expects a string in the form: "@resource.dll, -123" | ||
|
@@ -746,13 +844,16 @@ private static bool TryCompareTimeZoneInformationToRegistry(in TIME_ZONE_INFORMA | |
/// If a localized resource file exists, we LoadString resource ID "123" and | ||
/// return it to our caller. | ||
/// </summary> | ||
private static string TryGetLocalizedNameByMuiNativeResource(string resource) | ||
private static string GetLocalizedNameByMuiNativeResource(string resource, CultureInfo? cultureInfo = null) | ||
{ | ||
if (string.IsNullOrEmpty(resource)) | ||
{ | ||
return string.Empty; | ||
} | ||
|
||
// Use the current UI culture when culture not specified | ||
cultureInfo ??= CultureInfo.CurrentUICulture; | ||
|
||
// parse "@tzres.dll, -100" | ||
// | ||
// filePath = "C:\Windows\System32\tzres.dll" | ||
|
@@ -782,34 +883,23 @@ private static string TryGetLocalizedNameByMuiNativeResource(string resource) | |
return string.Empty; | ||
} | ||
|
||
if (!int.TryParse(resources[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int resourceId)) | ||
// Get the MUI File Path | ||
string fileMuiPath = GetCachedFileMuiPath(filePath, cultureInfo); | ||
if (fileMuiPath == string.Empty) | ||
{ | ||
// not likely, but we could not resolve a MUI path | ||
return string.Empty; | ||
} | ||
resourceId = -resourceId; | ||
|
||
try | ||
{ | ||
unsafe | ||
{ | ||
char* fileMuiPath = stackalloc char[Interop.Kernel32.MAX_PATH]; | ||
int fileMuiPathLength = Interop.Kernel32.MAX_PATH; | ||
int languageLength = 0; | ||
long enumerator = 0; | ||
|
||
bool succeeded = Interop.Kernel32.GetFileMUIPath( | ||
Interop.Kernel32.MUI_PREFERRED_UI_LANGUAGES, | ||
filePath, null /* language */, ref languageLength, | ||
fileMuiPath, ref fileMuiPathLength, ref enumerator); | ||
return succeeded ? | ||
TryGetLocalizedNameByNativeResource(new string(fileMuiPath, 0, fileMuiPathLength), resourceId) : | ||
string.Empty; | ||
} | ||
} | ||
catch (EntryPointNotFoundException) | ||
// Get the resource ID | ||
if (!int.TryParse(resources[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int resourceId)) | ||
{ | ||
return string.Empty; | ||
} | ||
resourceId = -resourceId; | ||
|
||
// Finally, get the resource from the resource path | ||
return GetLocalizedNameByNativeResource(fileMuiPath, resourceId); | ||
} | ||
|
||
/// <summary> | ||
|
@@ -819,7 +909,7 @@ private static string TryGetLocalizedNameByMuiNativeResource(string resource) | |
/// "resource.dll" is a language-specific resource DLL. | ||
/// If the localized resource DLL exists, LoadString(resource) is returned. | ||
/// </summary> | ||
private static unsafe string TryGetLocalizedNameByNativeResource(string filePath, int resource) | ||
private static unsafe string GetLocalizedNameByNativeResource(string filePath, int resource) | ||
{ | ||
IntPtr handle = IntPtr.Zero; | ||
try | ||
|
@@ -869,17 +959,17 @@ private static void GetLocalizedNamesByRegistryKey(RegistryKey key, out string? | |
// try to load the strings from the native resource DLL(s) | ||
if (!string.IsNullOrEmpty(displayNameMuiResource)) | ||
{ | ||
displayName = TryGetLocalizedNameByMuiNativeResource(displayNameMuiResource); | ||
displayName = GetLocalizedNameByMuiNativeResource(displayNameMuiResource); | ||
} | ||
|
||
if (!string.IsNullOrEmpty(standardNameMuiResource)) | ||
{ | ||
standardName = TryGetLocalizedNameByMuiNativeResource(standardNameMuiResource); | ||
standardName = GetLocalizedNameByMuiNativeResource(standardNameMuiResource); | ||
} | ||
|
||
if (!string.IsNullOrEmpty(daylightNameMuiResource)) | ||
{ | ||
daylightName = TryGetLocalizedNameByMuiNativeResource(daylightNameMuiResource); | ||
daylightName = GetLocalizedNameByMuiNativeResource(daylightNameMuiResource); | ||
} | ||
|
||
// fallback to using the standard registry keys | ||
|
@@ -901,7 +991,7 @@ private static void GetLocalizedNamesByRegistryKey(RegistryKey key, out string? | |
/// Helper function that takes a string representing a time_zone_name registry key name | ||
/// and returns a TimeZoneInfo instance. | ||
/// </summary> | ||
private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out TimeZoneInfo? value, out Exception? e) | ||
private static TimeZoneInfoResult GetTimeZoneFromLocalMachine(string id, out TimeZoneInfo? value, out Exception? e) | ||
{ | ||
e = null; | ||
|
||
|
@@ -1003,7 +1093,7 @@ private static string GetUtcStandardDisplayName() | |
// try to load the string from the native resource DLL(s) | ||
if (!string.IsNullOrEmpty(standardNameMuiResource)) | ||
{ | ||
standardDisplayName = TryGetLocalizedNameByMuiNativeResource(standardNameMuiResource); | ||
standardDisplayName = GetLocalizedNameByMuiNativeResource(standardNameMuiResource); | ||
} | ||
|
||
// fallback to using the standard registry key | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: s_fileMuiPathCache