Skip to content

Commit

Permalink
[browser] Revert to full NativeName by interop with JS (#99956)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* 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 <[email protected]>
  • Loading branch information
ilonatommy and mkhamoyan authored Mar 26, 2024
1 parent 9038180 commit 0c30a1b
Show file tree
Hide file tree
Showing 15 changed files with 224 additions and 42 deletions.
2 changes: 2 additions & 0 deletions src/libraries/Common/src/Interop/Browser/Interop.Locale.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@ internal static unsafe partial class JsGlobalization
internal static extern unsafe int GetFirstDayOfWeek(in string culture, out int exceptionalResult, out object result);
[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal static extern unsafe int GetFirstWeekOfYear(in string culture, out int exceptionalResult, out object result);
[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal static extern unsafe int GetLocaleInfo(in string locale, in string culture, char* buffer, int bufferLength, out int exceptionalResult, out object result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,62 @@ namespace System.Globalization
internal sealed partial class CultureData
{
private const int CULTURE_INFO_BUFFER_LEN = 50;
private const int LOCALE_INFO_BUFFER_LEN = 80;

private void JSInitLocaleInfo()
{
string? localeName = _sName;
if (string.IsNullOrEmpty(localeName))
{
_sEnglishLanguage = "Invariant Language";
_sNativeLanguage = _sEnglishLanguage;
_sEnglishCountry = "Invariant Country";
_sNativeCountry = _sEnglishCountry;
_sEnglishDisplayName = $"{_sEnglishLanguage} ({_sEnglishCountry})";
_sNativeDisplayName = _sEnglishDisplayName;
}
else
{
// English locale info
(_sEnglishLanguage, _sEnglishCountry) = JSGetLocaleInfo("en-US", localeName);
_sEnglishDisplayName = string.IsNullOrEmpty(_sEnglishCountry) ?
_sEnglishLanguage :
$"{_sEnglishLanguage} ({_sEnglishCountry})";
// Native locale info
(_sNativeLanguage, _sNativeCountry) = JSGetLocaleInfo(localeName, localeName);
_sNativeDisplayName = string.IsNullOrEmpty(_sNativeCountry) ?
_sNativeLanguage :
$"{_sNativeLanguage} ({_sNativeCountry})";
}
}

private unsafe (string, string) JSGetLocaleInfo(string cultureName, string localeName)
{
char* buffer = stackalloc char[LOCALE_INFO_BUFFER_LEN];
int resultLength = Interop.JsGlobalization.GetLocaleInfo(cultureName, localeName, buffer, LOCALE_INFO_BUFFER_LEN, out int exception, out object exResult);
if (exception != 0)
throw new Exception((string)exResult);
string result = new string(buffer, 0, resultLength);
string[] subresults = result.Split("##");
if (subresults.Length == 0)
throw new Exception("LocaleInfo recieved from the Browser is in incorrect format.");
if (subresults.Length == 1)
return (subresults[0], ""); // Neutral culture
return (subresults[0], subresults[1]);
}

private string JSGetNativeDisplayName(string localeName, string cultureName)
{
(string languageName, string countryName) = JSGetLocaleInfo(localeName, cultureName);
return string.IsNullOrEmpty(countryName) ?
languageName :
$"{languageName} ({countryName})";
}

private static unsafe CultureData JSLoadCultureInfoFromBrowser(string localeName, CultureData culture)
{
char* buffer = stackalloc char[CULTURE_INFO_BUFFER_LEN];
int exception;
object exResult;
int resultLength = Interop.JsGlobalization.GetCultureInfo(localeName, buffer, CULTURE_INFO_BUFFER_LEN, out exception, out exResult);
int resultLength = Interop.JsGlobalization.GetCultureInfo(localeName, buffer, CULTURE_INFO_BUFFER_LEN, out int exception, out object exResult);
if (exception != 0)
throw new Exception((string)exResult);
string result = new string(buffer, 0, resultLength);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@ private string IcuGetLocaleInfo(LocaleStringData type, string? uiCultureName = n
Debug.Assert(!GlobalizationMode.Invariant);
Debug.Assert(!GlobalizationMode.UseNls);
Debug.Assert(_sWindowsName != null, "[CultureData.IcuGetLocaleInfo] Expected _sWindowsName to be populated already");
#if TARGET_BROWSER && !FEATURE_WASM_MANAGED_THREADS
if (type == LocaleStringData.NativeDisplayName)
{
return JSGetNativeDisplayName(_sWindowsName, uiCultureName ?? _sWindowsName);
}
#endif
return IcuGetLocaleInfo(_sWindowsName, type, uiCultureName);
}

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

private string IcuGetLanguageDisplayName(string cultureName) => IcuGetLocaleInfo(cultureName, LocaleStringData.LocalizedDisplayName, CultureInfo.CurrentUICulture.Name);
private string IcuGetLanguageDisplayName(string cultureName)
{
#if TARGET_BROWSER && !FEATURE_WASM_MANAGED_THREADS
return JSGetNativeDisplayName(CultureInfo.CurrentUICulture.Name, cultureName);
#else
return IcuGetLocaleInfo(cultureName, LocaleStringData.LocalizedDisplayName, CultureInfo.CurrentUICulture.Name);
#endif
}

// use the fallback which is to return NativeName
private static string? IcuGetRegionDisplayName() => null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -816,12 +816,13 @@ private static string NormalizeCultureName(string name, out bool isNeutralName)
{
return null;
}
#if TARGET_BROWSER
#if TARGET_BROWSER && !FEATURE_WASM_MANAGED_THREADS
// populate fields for which ICU does not provide data in Hybrid mode
if (GlobalizationMode.Hybrid && !string.IsNullOrEmpty(culture._sName))
{
culture = JSLoadCultureInfoFromBrowser(culture._sName, culture);
}
culture.JSInitLocaleInfo();
#endif

// We need _sWindowsName to be initialized to know if we're using overrides.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,22 @@ namespace System.Globalization.Tests
public class CultureInfoEnglishName
{
// Android has its own ICU, which doesn't 100% map to UsingLimitedCultures
public static bool SupportFullGlobalizationData => PlatformDetection.IsNotUsingLimitedCultures || PlatformDetection.IsAndroid;
// Browser uses JS to get the NativeName that is missing in ICU (in the singlethreaded runtime only)
public static bool SupportFullGlobalizationData =>
(!PlatformDetection.IsWasi || PlatformDetection.IsHybridGlobalizationOnApplePlatform) && !PlatformDetection.IsWasmThreadingSupported;

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

if (SupportFullGlobalizationData || PlatformDetection.IsHybridGlobalizationOnApplePlatform)
if (SupportFullGlobalizationData)
{
yield return new object[] { "en-US", "English (United States)" };
yield return new object[] { "fr-FR", "French (France)" };
yield return new object[] { "uz-Cyrl", "Uzbek (Cyrillic)" };
}
else
{
// Mobile / Browser ICU doesn't contain CultureInfo.EnglishName
yield return new object[] { "en-US", "en (US)" };
yield return new object[] { "fr-FR", "fr (FR)" };
}
Expand All @@ -41,12 +42,12 @@ public void EnglishName(string name, string expected)
public void ChineseNeutralEnglishName()
{
CultureInfo ci = new CultureInfo("zh-Hans");
Assert.True(ci.EnglishName == "Chinese (Simplified)" || ci.EnglishName == "Chinese, Simplified",
$"'{ci.EnglishName}' not equal to `Chinese (Simplified)` nor `Chinese, Simplified`");
Assert.True(ci.EnglishName == "Chinese (Simplified)" || ci.EnglishName == "Chinese, Simplified" || ci.EnglishName == "Simplified Chinese",
$"'{ci.EnglishName}' not equal to `Chinese (Simplified)` nor `Chinese, Simplified` nor `Simplified Chinese`");

ci = new CultureInfo("zh-HanT");
Assert.True(ci.EnglishName == "Chinese (Traditional)" || ci.EnglishName == "Chinese, Traditional",
$"'{ci.EnglishName}' not equal to `Chinese (Traditional)` nor `Chinese, Traditional`");
Assert.True(ci.EnglishName == "Chinese (Traditional)" || ci.EnglishName == "Chinese, Traditional" || ci.EnglishName == "Traditional Chinese",
$"'{ci.EnglishName}' not equal to `Chinese (Traditional)` nor `Chinese, Traditional` nor `Traditional Chinese`");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,31 @@ namespace System.Globalization.Tests
{
public class CultureInfoNames
{
private static bool SupportFullIcuResources => (PlatformDetection.IsNotMobile && PlatformDetection.IsIcuGlobalization) || PlatformDetection.IsHybridGlobalizationOnApplePlatform;
// Android has its own ICU, which doesn't 100% map to UsingLimitedCultures
// Browser uses JS to get the NativeName that is missing in ICU (in the singlethreaded runtime only)
private static bool SupportFullIcuResources =>
!PlatformDetection.IsWasi && !PlatformDetection.IsAndroid && PlatformDetection.IsIcuGlobalization && !PlatformDetection.IsWasmThreadingSupported;

public static IEnumerable<object[]> SupportedCultures_TestData()
{
// Browser does not support all ICU locales but it uses JS to get the correct native name
if (!PlatformDetection.IsBrowser)
{
yield return new object[] { "aa", "aa", "Afar", "Afar" };
yield return new object[] { "aa-ER", "aa-ER", "Afar (Eritrea)", "Afar (Eritrea)" };
}
yield return new object[] { "en", "en", "English", "English" };
yield return new object[] { "en", "fr", "English", "anglais" };
yield return new object[] { "en-US", "en-US", "English (United States)", "English (United States)" };
yield return new object[] { "en-US", "fr-FR", "English (United States)", "anglais (\u00C9tats-Unis)" };
yield return new object[] { "en-US", "de-DE", "English (United States)", "Englisch (Vereinigte Staaten)" };
yield return new object[] { "", "en-US", "Invariant Language (Invariant Country)", "Invariant Language (Invariant Country)" };
yield return new object[] { "", "fr-FR", "Invariant Language (Invariant Country)", "Invariant Language (Invariant Country)" };
yield return new object[] { "", "", "Invariant Language (Invariant Country)", "Invariant Language (Invariant Country)" };
}

[ConditionalTheory(nameof(SupportFullIcuResources))]
[InlineData("en", "en", "English", "English")]
[InlineData("en", "fr", "English", "anglais")]
[InlineData("aa", "aa", "Afar", "Afar")]
[InlineData("en-US", "en-US", "English (United States)", "English (United States)")]
[InlineData("en-US", "fr-FR", "English (United States)", "anglais (\u00C9tats-Unis)")]
[InlineData("en-US", "de-DE", "English (United States)", "Englisch (Vereinigte Staaten)")]
[InlineData("aa-ER", "aa-ER", "Afar (Eritrea)", "Afar (Eritrea)")]
[InlineData("", "en-US", "Invariant Language (Invariant Country)", "Invariant Language (Invariant Country)")]
[InlineData("", "fr-FR", "Invariant Language (Invariant Country)", "Invariant Language (Invariant Country)")]
[InlineData("", "", "Invariant Language (Invariant Country)", "Invariant Language (Invariant Country)")]
[MemberData(nameof(SupportedCultures_TestData))]
public void TestDisplayName(string cultureName, string uiCultureName, string nativeName, string displayName)
{
using (new ThreadCultureChange(null, CultureInfo.GetCultureInfo(uiCultureName)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,23 @@ namespace System.Globalization.Tests
{
public class CultureInfoNativeName
{
// Android has its own ICU, which doesn't 100% map to UsingLimitedCultures
// Browser uses JS to get the NativeName that is missing in ICU (in the singlethreaded runtime only)
private static bool SupportFullIcuResources =>
(!PlatformDetection.IsWasi || PlatformDetection.IsHybridGlobalizationOnApplePlatform) && !PlatformDetection.IsWasmThreadingSupported;

public static IEnumerable<object[]> NativeName_TestData()
{
yield return new object[] { CultureInfo.CurrentCulture.Name, CultureInfo.CurrentCulture.NativeName };

// Android has its own ICU, which doesn't 100% map to UsingLimitedCultures
if (PlatformDetection.IsNotUsingLimitedCultures || PlatformDetection.IsAndroid || PlatformDetection.IsHybridGlobalizationOnApplePlatform)
if (SupportFullIcuResources)
{
yield return new object[] { "en-US", "English (United States)" };
yield return new object[] { "en-CA", "English (Canada)" };
yield return new object[] { "en-GB", "English (United Kingdom)" };
}
else
{
// Mobile / Browser ICU doesn't contain CultureInfo.NativeName
yield return new object[] { "en-US", "en (US)" };
yield return new object[] { "en-CA", "en (CA)" };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ namespace System.Globalization.Tests
{
public class RegionInfoPropertyTests
{
// Android has its own ICU, which doesn't 100% map to UsingLimitedCultures
// Browser uses JS to get the NativeName that is missing in ICU (in the singlethreaded runtime only)
public static bool SupportFullGlobalizationData =>
(!PlatformDetection.IsWasi || PlatformDetection.IsHybridGlobalizationOnApplePlatform) && !PlatformDetection.IsWasmThreadingSupported;

[Theory]
[InlineData("US", "US", "US")]
[InlineData("IT", "IT", "IT")]
Expand Down Expand Up @@ -100,7 +105,6 @@ public void ValidateUsingCasedRegionName(string regionName)
[Theory]
[InlineData("en-US", "United States")]
[OuterLoop("May fail on machines with multiple language packs installed")] // see https://github.com/dotnet/runtime/issues/30132
[ActiveIssue("https://github.com/dotnet/runtime/issues/45951", TestPlatforms.Browser)]
public void DisplayName(string name, string expected)
{
using (new ThreadCultureChange(null, new CultureInfo(name)))
Expand All @@ -111,16 +115,14 @@ public void DisplayName(string name, string expected)

public static IEnumerable<object[]> NativeName_TestData()
{
// Android has its own ICU, which doesn't 100% map to UsingLimitedCultures
if (PlatformDetection.IsNotUsingLimitedCultures || PlatformDetection.IsAndroid || PlatformDetection.IsHybridGlobalizationOnApplePlatform)
if (SupportFullGlobalizationData)
{
yield return new object[] { "GB", "United Kingdom" };
yield return new object[] { "SE", "Sverige" };
yield return new object[] { "FR", "France" };
}
else
{
// Browser's ICU doesn't contain RegionInfo.NativeName
yield return new object[] { "GB", "GB" };
yield return new object[] { "SE", "SE" };
yield return new object[] { "FR", "FR" };
Expand All @@ -136,8 +138,7 @@ public void NativeName(string name, string expected)

public static IEnumerable<object[]> EnglishName_TestData()
{
// Android has its own ICU, which doesn't 100% map to UsingLimitedCultures
if (PlatformDetection.IsNotUsingLimitedCultures || PlatformDetection.IsAndroid || PlatformDetection.IsHybridGlobalizationOnApplePlatform)
if (SupportFullGlobalizationData)
{
yield return new object[] { "en-US", new string[] { "United States" } };
yield return new object[] { "US", new string[] { "United States" } };
Expand All @@ -146,7 +147,6 @@ public static IEnumerable<object[]> EnglishName_TestData()
}
else
{
// Browser's ICU doesn't contain RegionInfo.EnglishName
yield return new object[] { "en-US", new string[] { "US" } };
yield return new object[] { "US", new string[] { "US" } };
yield return new object[] { "zh-CN", new string[] { "CN" }};
Expand Down
2 changes: 2 additions & 0 deletions src/mono/browser/runtime/corebindings.c
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ extern mono_bool mono_wasm_starts_with (MonoString **culture, const uint16_t* st
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);
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);
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);
extern int mono_wasm_get_locale_info (MonoString **locale, MonoString **culture, const uint16_t* result, int32_t resultLength, int *is_exception, MonoObject** ex_result);
extern int mono_wasm_get_culture_info (MonoString **culture, const uint16_t* result, int32_t resultLength, int *is_exception, MonoObject** ex_result);
extern int mono_wasm_get_first_day_of_week (MonoString **culture, int *is_exception, MonoObject** ex_result);
extern int mono_wasm_get_first_week_of_year (MonoString **culture, int *is_exception, MonoObject** ex_result);
Expand Down Expand Up @@ -105,6 +106,7 @@ void bindings_initialize_internals (void)
mono_add_internal_call ("Interop/JsGlobalization::EndsWith", mono_wasm_ends_with);
mono_add_internal_call ("Interop/JsGlobalization::IndexOf", mono_wasm_index_of);
mono_add_internal_call ("Interop/JsGlobalization::GetCalendarInfo", mono_wasm_get_calendar_info);
mono_add_internal_call ("Interop/JsGlobalization::GetLocaleInfo", mono_wasm_get_locale_info);
mono_add_internal_call ("Interop/JsGlobalization::GetCultureInfo", mono_wasm_get_culture_info);
mono_add_internal_call ("Interop/JsGlobalization::GetFirstDayOfWeek", mono_wasm_get_first_day_of_week);
mono_add_internal_call ("Interop/JsGlobalization::GetFirstWeekOfYear", mono_wasm_get_first_week_of_year);
Expand Down
3 changes: 2 additions & 1 deletion src/mono/browser/runtime/exports-binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { mono_wasm_compare_string, mono_wasm_ends_with, mono_wasm_starts_with, m
import { mono_wasm_get_calendar_info } from "./hybrid-globalization/calendar";

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

Expand Down Expand Up @@ -107,6 +107,7 @@ export const mono_wasm_imports = [
mono_wasm_index_of,
mono_wasm_get_calendar_info,
mono_wasm_get_culture_info,
mono_wasm_get_locale_info,
mono_wasm_get_first_day_of_week,
mono_wasm_get_first_week_of_year,
];
Expand Down
4 changes: 2 additions & 2 deletions src/mono/browser/runtime/hybrid-globalization/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ export function normalizeLocale(locale: string | null)
const canonicalLocales = (Intl as any).getCanonicalLocales(locale.replace("_", "-"));
return canonicalLocales.length > 0 ? canonicalLocales[0] : undefined;
}
catch(ex: any)
catch
{
throw new Error(`Get culture info failed for culture = ${locale} with error: ${ex}`);
return undefined;
}
}

Expand Down
Loading

0 comments on commit 0c30a1b

Please sign in to comment.