Skip to content

Commit 0c30a1b

Browse files
[browser] Revert to full NativeName by interop with JS (#99956)
* Fix NativeName and DisplayName for browser. * Nit - revert over-renaming. * Update src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Browser.cs Co-authored-by: Meri Khamoyan <96171496+mkhamoyan@users.noreply.github.com> * Enable more tests and fix them - initialize english and native names on CultureInfo constructio * Ubnblock fixed test. * MT does not work with HG well, revet MT to original way of working. * Expect fixed version of NativeName in tests with single treaded runtime. * Assert.Contains does not know that `\u00F1` is same as `ñ` (it does not evaluate the string before comparison) - fix it with string interpolation. * Windows has problems with comparing utf8 by xunit. --------- Co-authored-by: Meri Khamoyan <96171496+mkhamoyan@users.noreply.github.com>
1 parent 9038180 commit 0c30a1b

File tree

15 files changed

+224
-42
lines changed

15 files changed

+224
-42
lines changed

src/libraries/Common/src/Interop/Browser/Interop.Locale.cs

+2
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,7 @@ internal static unsafe partial class JsGlobalization
1313
internal static extern unsafe int GetFirstDayOfWeek(in string culture, out int exceptionalResult, out object result);
1414
[MethodImplAttribute(MethodImplOptions.InternalCall)]
1515
internal static extern unsafe int GetFirstWeekOfYear(in string culture, out int exceptionalResult, out object result);
16+
[MethodImplAttribute(MethodImplOptions.InternalCall)]
17+
internal static extern unsafe int GetLocaleInfo(in string locale, in string culture, char* buffer, int bufferLength, out int exceptionalResult, out object result);
1618
}
1719
}

src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Browser.cs

+52-3
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,62 @@ namespace System.Globalization
1010
internal sealed partial class CultureData
1111
{
1212
private const int CULTURE_INFO_BUFFER_LEN = 50;
13+
private const int LOCALE_INFO_BUFFER_LEN = 80;
14+
15+
private void JSInitLocaleInfo()
16+
{
17+
string? localeName = _sName;
18+
if (string.IsNullOrEmpty(localeName))
19+
{
20+
_sEnglishLanguage = "Invariant Language";
21+
_sNativeLanguage = _sEnglishLanguage;
22+
_sEnglishCountry = "Invariant Country";
23+
_sNativeCountry = _sEnglishCountry;
24+
_sEnglishDisplayName = $"{_sEnglishLanguage} ({_sEnglishCountry})";
25+
_sNativeDisplayName = _sEnglishDisplayName;
26+
}
27+
else
28+
{
29+
// English locale info
30+
(_sEnglishLanguage, _sEnglishCountry) = JSGetLocaleInfo("en-US", localeName);
31+
_sEnglishDisplayName = string.IsNullOrEmpty(_sEnglishCountry) ?
32+
_sEnglishLanguage :
33+
$"{_sEnglishLanguage} ({_sEnglishCountry})";
34+
// Native locale info
35+
(_sNativeLanguage, _sNativeCountry) = JSGetLocaleInfo(localeName, localeName);
36+
_sNativeDisplayName = string.IsNullOrEmpty(_sNativeCountry) ?
37+
_sNativeLanguage :
38+
$"{_sNativeLanguage} ({_sNativeCountry})";
39+
}
40+
}
41+
42+
private unsafe (string, string) JSGetLocaleInfo(string cultureName, string localeName)
43+
{
44+
char* buffer = stackalloc char[LOCALE_INFO_BUFFER_LEN];
45+
int resultLength = Interop.JsGlobalization.GetLocaleInfo(cultureName, localeName, buffer, LOCALE_INFO_BUFFER_LEN, out int exception, out object exResult);
46+
if (exception != 0)
47+
throw new Exception((string)exResult);
48+
string result = new string(buffer, 0, resultLength);
49+
string[] subresults = result.Split("##");
50+
if (subresults.Length == 0)
51+
throw new Exception("LocaleInfo recieved from the Browser is in incorrect format.");
52+
if (subresults.Length == 1)
53+
return (subresults[0], ""); // Neutral culture
54+
return (subresults[0], subresults[1]);
55+
}
56+
57+
private string JSGetNativeDisplayName(string localeName, string cultureName)
58+
{
59+
(string languageName, string countryName) = JSGetLocaleInfo(localeName, cultureName);
60+
return string.IsNullOrEmpty(countryName) ?
61+
languageName :
62+
$"{languageName} ({countryName})";
63+
}
1364

1465
private static unsafe CultureData JSLoadCultureInfoFromBrowser(string localeName, CultureData culture)
1566
{
1667
char* buffer = stackalloc char[CULTURE_INFO_BUFFER_LEN];
17-
int exception;
18-
object exResult;
19-
int resultLength = Interop.JsGlobalization.GetCultureInfo(localeName, buffer, CULTURE_INFO_BUFFER_LEN, out exception, out exResult);
68+
int resultLength = Interop.JsGlobalization.GetCultureInfo(localeName, buffer, CULTURE_INFO_BUFFER_LEN, out int exception, out object exResult);
2069
if (exception != 0)
2170
throw new Exception((string)exResult);
2271
string result = new string(buffer, 0, resultLength);

src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Icu.cs

+14-1
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,12 @@ private string IcuGetLocaleInfo(LocaleStringData type, string? uiCultureName = n
203203
Debug.Assert(!GlobalizationMode.Invariant);
204204
Debug.Assert(!GlobalizationMode.UseNls);
205205
Debug.Assert(_sWindowsName != null, "[CultureData.IcuGetLocaleInfo] Expected _sWindowsName to be populated already");
206+
#if TARGET_BROWSER && !FEATURE_WASM_MANAGED_THREADS
207+
if (type == LocaleStringData.NativeDisplayName)
208+
{
209+
return JSGetNativeDisplayName(_sWindowsName, uiCultureName ?? _sWindowsName);
210+
}
211+
#endif
206212
return IcuGetLocaleInfo(_sWindowsName, type, uiCultureName);
207213
}
208214

@@ -302,7 +308,14 @@ private unsafe string IcuGetTimeFormatString(bool shortFormat)
302308
// no support to lookup by region name, other than the hard-coded list in CultureData
303309
private static CultureData? IcuGetCultureDataFromRegionName() => null;
304310

305-
private string IcuGetLanguageDisplayName(string cultureName) => IcuGetLocaleInfo(cultureName, LocaleStringData.LocalizedDisplayName, CultureInfo.CurrentUICulture.Name);
311+
private string IcuGetLanguageDisplayName(string cultureName)
312+
{
313+
#if TARGET_BROWSER && !FEATURE_WASM_MANAGED_THREADS
314+
return JSGetNativeDisplayName(CultureInfo.CurrentUICulture.Name, cultureName);
315+
#else
316+
return IcuGetLocaleInfo(cultureName, LocaleStringData.LocalizedDisplayName, CultureInfo.CurrentUICulture.Name);
317+
#endif
318+
}
306319

307320
// use the fallback which is to return NativeName
308321
private static string? IcuGetRegionDisplayName() => null;

src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -816,12 +816,13 @@ private static string NormalizeCultureName(string name, out bool isNeutralName)
816816
{
817817
return null;
818818
}
819-
#if TARGET_BROWSER
819+
#if TARGET_BROWSER && !FEATURE_WASM_MANAGED_THREADS
820820
// populate fields for which ICU does not provide data in Hybrid mode
821821
if (GlobalizationMode.Hybrid && !string.IsNullOrEmpty(culture._sName))
822822
{
823823
culture = JSLoadCultureInfoFromBrowser(culture._sName, culture);
824824
}
825+
culture.JSInitLocaleInfo();
825826
#endif
826827

827828
// We need _sWindowsName to be initialized to know if we're using overrides.

src/libraries/System.Runtime/tests/System.Globalization.Tests/CultureInfo/CultureInfoEnglishName.cs

+8-7
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,22 @@ namespace System.Globalization.Tests
99
public class CultureInfoEnglishName
1010
{
1111
// Android has its own ICU, which doesn't 100% map to UsingLimitedCultures
12-
public static bool SupportFullGlobalizationData => PlatformDetection.IsNotUsingLimitedCultures || PlatformDetection.IsAndroid;
12+
// Browser uses JS to get the NativeName that is missing in ICU (in the singlethreaded runtime only)
13+
public static bool SupportFullGlobalizationData =>
14+
(!PlatformDetection.IsWasi || PlatformDetection.IsHybridGlobalizationOnApplePlatform) && !PlatformDetection.IsWasmThreadingSupported;
1315

1416
public static IEnumerable<object[]> EnglishName_TestData()
1517
{
1618
yield return new object[] { CultureInfo.CurrentCulture.Name, CultureInfo.CurrentCulture.EnglishName };
1719

18-
if (SupportFullGlobalizationData || PlatformDetection.IsHybridGlobalizationOnApplePlatform)
20+
if (SupportFullGlobalizationData)
1921
{
2022
yield return new object[] { "en-US", "English (United States)" };
2123
yield return new object[] { "fr-FR", "French (France)" };
2224
yield return new object[] { "uz-Cyrl", "Uzbek (Cyrillic)" };
2325
}
2426
else
2527
{
26-
// Mobile / Browser ICU doesn't contain CultureInfo.EnglishName
2728
yield return new object[] { "en-US", "en (US)" };
2829
yield return new object[] { "fr-FR", "fr (FR)" };
2930
}
@@ -41,12 +42,12 @@ public void EnglishName(string name, string expected)
4142
public void ChineseNeutralEnglishName()
4243
{
4344
CultureInfo ci = new CultureInfo("zh-Hans");
44-
Assert.True(ci.EnglishName == "Chinese (Simplified)" || ci.EnglishName == "Chinese, Simplified",
45-
$"'{ci.EnglishName}' not equal to `Chinese (Simplified)` nor `Chinese, Simplified`");
45+
Assert.True(ci.EnglishName == "Chinese (Simplified)" || ci.EnglishName == "Chinese, Simplified" || ci.EnglishName == "Simplified Chinese",
46+
$"'{ci.EnglishName}' not equal to `Chinese (Simplified)` nor `Chinese, Simplified` nor `Simplified Chinese`");
4647

4748
ci = new CultureInfo("zh-HanT");
48-
Assert.True(ci.EnglishName == "Chinese (Traditional)" || ci.EnglishName == "Chinese, Traditional",
49-
$"'{ci.EnglishName}' not equal to `Chinese (Traditional)` nor `Chinese, Traditional`");
49+
Assert.True(ci.EnglishName == "Chinese (Traditional)" || ci.EnglishName == "Chinese, Traditional" || ci.EnglishName == "Traditional Chinese",
50+
$"'{ci.EnglishName}' not equal to `Chinese (Traditional)` nor `Chinese, Traditional` nor `Traditional Chinese`");
5051
}
5152
}
5253
}

src/libraries/System.Runtime/tests/System.Globalization.Tests/CultureInfo/CultureInfoNames.cs

+23-11
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,31 @@ namespace System.Globalization.Tests
1010
{
1111
public class CultureInfoNames
1212
{
13-
private static bool SupportFullIcuResources => (PlatformDetection.IsNotMobile && PlatformDetection.IsIcuGlobalization) || PlatformDetection.IsHybridGlobalizationOnApplePlatform;
13+
// Android has its own ICU, which doesn't 100% map to UsingLimitedCultures
14+
// Browser uses JS to get the NativeName that is missing in ICU (in the singlethreaded runtime only)
15+
private static bool SupportFullIcuResources =>
16+
!PlatformDetection.IsWasi && !PlatformDetection.IsAndroid && PlatformDetection.IsIcuGlobalization && !PlatformDetection.IsWasmThreadingSupported;
17+
18+
public static IEnumerable<object[]> SupportedCultures_TestData()
19+
{
20+
// Browser does not support all ICU locales but it uses JS to get the correct native name
21+
if (!PlatformDetection.IsBrowser)
22+
{
23+
yield return new object[] { "aa", "aa", "Afar", "Afar" };
24+
yield return new object[] { "aa-ER", "aa-ER", "Afar (Eritrea)", "Afar (Eritrea)" };
25+
}
26+
yield return new object[] { "en", "en", "English", "English" };
27+
yield return new object[] { "en", "fr", "English", "anglais" };
28+
yield return new object[] { "en-US", "en-US", "English (United States)", "English (United States)" };
29+
yield return new object[] { "en-US", "fr-FR", "English (United States)", "anglais (\u00C9tats-Unis)" };
30+
yield return new object[] { "en-US", "de-DE", "English (United States)", "Englisch (Vereinigte Staaten)" };
31+
yield return new object[] { "", "en-US", "Invariant Language (Invariant Country)", "Invariant Language (Invariant Country)" };
32+
yield return new object[] { "", "fr-FR", "Invariant Language (Invariant Country)", "Invariant Language (Invariant Country)" };
33+
yield return new object[] { "", "", "Invariant Language (Invariant Country)", "Invariant Language (Invariant Country)" };
34+
}
1435

1536
[ConditionalTheory(nameof(SupportFullIcuResources))]
16-
[InlineData("en", "en", "English", "English")]
17-
[InlineData("en", "fr", "English", "anglais")]
18-
[InlineData("aa", "aa", "Afar", "Afar")]
19-
[InlineData("en-US", "en-US", "English (United States)", "English (United States)")]
20-
[InlineData("en-US", "fr-FR", "English (United States)", "anglais (\u00C9tats-Unis)")]
21-
[InlineData("en-US", "de-DE", "English (United States)", "Englisch (Vereinigte Staaten)")]
22-
[InlineData("aa-ER", "aa-ER", "Afar (Eritrea)", "Afar (Eritrea)")]
23-
[InlineData("", "en-US", "Invariant Language (Invariant Country)", "Invariant Language (Invariant Country)")]
24-
[InlineData("", "fr-FR", "Invariant Language (Invariant Country)", "Invariant Language (Invariant Country)")]
25-
[InlineData("", "", "Invariant Language (Invariant Country)", "Invariant Language (Invariant Country)")]
37+
[MemberData(nameof(SupportedCultures_TestData))]
2638
public void TestDisplayName(string cultureName, string uiCultureName, string nativeName, string displayName)
2739
{
2840
using (new ThreadCultureChange(null, CultureInfo.GetCultureInfo(uiCultureName)))

src/libraries/System.Runtime/tests/System.Globalization.Tests/CultureInfo/CultureInfoNativeName.cs

+6-3
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,23 @@ namespace System.Globalization.Tests
88
{
99
public class CultureInfoNativeName
1010
{
11+
// Android has its own ICU, which doesn't 100% map to UsingLimitedCultures
12+
// Browser uses JS to get the NativeName that is missing in ICU (in the singlethreaded runtime only)
13+
private static bool SupportFullIcuResources =>
14+
(!PlatformDetection.IsWasi || PlatformDetection.IsHybridGlobalizationOnApplePlatform) && !PlatformDetection.IsWasmThreadingSupported;
15+
1116
public static IEnumerable<object[]> NativeName_TestData()
1217
{
1318
yield return new object[] { CultureInfo.CurrentCulture.Name, CultureInfo.CurrentCulture.NativeName };
1419

15-
// Android has its own ICU, which doesn't 100% map to UsingLimitedCultures
16-
if (PlatformDetection.IsNotUsingLimitedCultures || PlatformDetection.IsAndroid || PlatformDetection.IsHybridGlobalizationOnApplePlatform)
20+
if (SupportFullIcuResources)
1721
{
1822
yield return new object[] { "en-US", "English (United States)" };
1923
yield return new object[] { "en-CA", "English (Canada)" };
2024
yield return new object[] { "en-GB", "English (United Kingdom)" };
2125
}
2226
else
2327
{
24-
// Mobile / Browser ICU doesn't contain CultureInfo.NativeName
2528
yield return new object[] { "en-US", "en (US)" };
2629
yield return new object[] { "en-CA", "en (CA)" };
2730
}

src/libraries/System.Runtime/tests/System.Globalization.Tests/System/Globalization/RegionInfoTests.cs

+7-7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ namespace System.Globalization.Tests
1212
{
1313
public class RegionInfoPropertyTests
1414
{
15+
// Android has its own ICU, which doesn't 100% map to UsingLimitedCultures
16+
// Browser uses JS to get the NativeName that is missing in ICU (in the singlethreaded runtime only)
17+
public static bool SupportFullGlobalizationData =>
18+
(!PlatformDetection.IsWasi || PlatformDetection.IsHybridGlobalizationOnApplePlatform) && !PlatformDetection.IsWasmThreadingSupported;
19+
1520
[Theory]
1621
[InlineData("US", "US", "US")]
1722
[InlineData("IT", "IT", "IT")]
@@ -100,7 +105,6 @@ public void ValidateUsingCasedRegionName(string regionName)
100105
[Theory]
101106
[InlineData("en-US", "United States")]
102107
[OuterLoop("May fail on machines with multiple language packs installed")] // see https://github.com/dotnet/runtime/issues/30132
103-
[ActiveIssue("https://github.com/dotnet/runtime/issues/45951", TestPlatforms.Browser)]
104108
public void DisplayName(string name, string expected)
105109
{
106110
using (new ThreadCultureChange(null, new CultureInfo(name)))
@@ -111,16 +115,14 @@ public void DisplayName(string name, string expected)
111115

112116
public static IEnumerable<object[]> NativeName_TestData()
113117
{
114-
// Android has its own ICU, which doesn't 100% map to UsingLimitedCultures
115-
if (PlatformDetection.IsNotUsingLimitedCultures || PlatformDetection.IsAndroid || PlatformDetection.IsHybridGlobalizationOnApplePlatform)
118+
if (SupportFullGlobalizationData)
116119
{
117120
yield return new object[] { "GB", "United Kingdom" };
118121
yield return new object[] { "SE", "Sverige" };
119122
yield return new object[] { "FR", "France" };
120123
}
121124
else
122125
{
123-
// Browser's ICU doesn't contain RegionInfo.NativeName
124126
yield return new object[] { "GB", "GB" };
125127
yield return new object[] { "SE", "SE" };
126128
yield return new object[] { "FR", "FR" };
@@ -136,8 +138,7 @@ public void NativeName(string name, string expected)
136138

137139
public static IEnumerable<object[]> EnglishName_TestData()
138140
{
139-
// Android has its own ICU, which doesn't 100% map to UsingLimitedCultures
140-
if (PlatformDetection.IsNotUsingLimitedCultures || PlatformDetection.IsAndroid || PlatformDetection.IsHybridGlobalizationOnApplePlatform)
141+
if (SupportFullGlobalizationData)
141142
{
142143
yield return new object[] { "en-US", new string[] { "United States" } };
143144
yield return new object[] { "US", new string[] { "United States" } };
@@ -146,7 +147,6 @@ public static IEnumerable<object[]> EnglishName_TestData()
146147
}
147148
else
148149
{
149-
// Browser's ICU doesn't contain RegionInfo.EnglishName
150150
yield return new object[] { "en-US", new string[] { "US" } };
151151
yield return new object[] { "US", new string[] { "US" } };
152152
yield return new object[] { "zh-CN", new string[] { "CN" }};

src/mono/browser/runtime/corebindings.c

+2
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ extern mono_bool mono_wasm_starts_with (MonoString **culture, const uint16_t* st
6464
extern mono_bool mono_wasm_ends_with (MonoString **culture, const uint16_t* str1, int32_t str1Length, const uint16_t* str2, int32_t str2Length, int32_t options, int *is_exception, MonoObject** ex_result);
6565
extern int mono_wasm_index_of (MonoString **culture, const uint16_t* str1, int32_t str1Length, const uint16_t* str2, int32_t str2Length, int32_t options, mono_bool fromBeginning, int *is_exception, MonoObject** ex_result);
6666
extern int mono_wasm_get_calendar_info (MonoString **culture, int32_t calendarId, const uint16_t* result, int32_t resultLength, int *is_exception, MonoObject** ex_result);
67+
extern int mono_wasm_get_locale_info (MonoString **locale, MonoString **culture, const uint16_t* result, int32_t resultLength, int *is_exception, MonoObject** ex_result);
6768
extern int mono_wasm_get_culture_info (MonoString **culture, const uint16_t* result, int32_t resultLength, int *is_exception, MonoObject** ex_result);
6869
extern int mono_wasm_get_first_day_of_week (MonoString **culture, int *is_exception, MonoObject** ex_result);
6970
extern int mono_wasm_get_first_week_of_year (MonoString **culture, int *is_exception, MonoObject** ex_result);
@@ -105,6 +106,7 @@ void bindings_initialize_internals (void)
105106
mono_add_internal_call ("Interop/JsGlobalization::EndsWith", mono_wasm_ends_with);
106107
mono_add_internal_call ("Interop/JsGlobalization::IndexOf", mono_wasm_index_of);
107108
mono_add_internal_call ("Interop/JsGlobalization::GetCalendarInfo", mono_wasm_get_calendar_info);
109+
mono_add_internal_call ("Interop/JsGlobalization::GetLocaleInfo", mono_wasm_get_locale_info);
108110
mono_add_internal_call ("Interop/JsGlobalization::GetCultureInfo", mono_wasm_get_culture_info);
109111
mono_add_internal_call ("Interop/JsGlobalization::GetFirstDayOfWeek", mono_wasm_get_first_day_of_week);
110112
mono_add_internal_call ("Interop/JsGlobalization::GetFirstWeekOfYear", mono_wasm_get_first_week_of_year);

src/mono/browser/runtime/exports-binding.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { mono_wasm_compare_string, mono_wasm_ends_with, mono_wasm_starts_with, m
2222
import { mono_wasm_get_calendar_info } from "./hybrid-globalization/calendar";
2323

2424
import { mono_wasm_get_culture_info } from "./hybrid-globalization/culture-info";
25-
import { mono_wasm_get_first_day_of_week, mono_wasm_get_first_week_of_year } from "./hybrid-globalization/locales";
25+
import { mono_wasm_get_locale_info, mono_wasm_get_first_day_of_week, mono_wasm_get_first_week_of_year } from "./hybrid-globalization/locales";
2626
import { mono_wasm_browser_entropy } from "./crypto";
2727
import { mono_wasm_cancel_promise } from "./cancelable-promise";
2828

@@ -107,6 +107,7 @@ export const mono_wasm_imports = [
107107
mono_wasm_index_of,
108108
mono_wasm_get_calendar_info,
109109
mono_wasm_get_culture_info,
110+
mono_wasm_get_locale_info,
110111
mono_wasm_get_first_day_of_week,
111112
mono_wasm_get_first_week_of_year,
112113
];

src/mono/browser/runtime/hybrid-globalization/helpers.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ export function normalizeLocale(locale: string | null)
2525
const canonicalLocales = (Intl as any).getCanonicalLocales(locale.replace("_", "-"));
2626
return canonicalLocales.length > 0 ? canonicalLocales[0] : undefined;
2727
}
28-
catch(ex: any)
28+
catch
2929
{
30-
throw new Error(`Get culture info failed for culture = ${locale} with error: ${ex}`);
30+
return undefined;
3131
}
3232
}
3333

0 commit comments

Comments
 (0)