Skip to content
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

[browser] Revert to full NativeName by interop with JS #99956

Merged
merged 18 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
ilonatommy marked this conversation as resolved.
Show resolved Hide resolved
{
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)
ilonatommy marked this conversation as resolved.
Show resolved Hide resolved
{
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
Loading