From 0c30a1bd30a2523491c4fe419fa2a654129e581f Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz <32700855+ilonatommy@users.noreply.github.com> Date: Tue, 26 Mar 2024 12:10:59 +0100 Subject: [PATCH] [browser] Revert to full `NativeName` by interop with JS (#99956) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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> --- .../src/Interop/Browser/Interop.Locale.cs | 2 + .../Globalization/CultureData.Browser.cs | 55 ++++++++++- .../System/Globalization/CultureData.Icu.cs | 15 ++- .../src/System/Globalization/CultureData.cs | 3 +- .../CultureInfo/CultureInfoEnglishName.cs | 15 +-- .../CultureInfo/CultureInfoNames.cs | 34 ++++--- .../CultureInfo/CultureInfoNativeName.cs | 9 +- .../System/Globalization/RegionInfoTests.cs | 14 +-- src/mono/browser/runtime/corebindings.c | 2 + src/mono/browser/runtime/exports-binding.ts | 3 +- .../runtime/hybrid-globalization/helpers.ts | 4 +- .../runtime/hybrid-globalization/locales.ts | 92 ++++++++++++++++++- .../Blazor/WorkloadRequiredTests.cs | 3 +- .../InvariantGlobalizationTests.cs | 4 +- .../InvariantGlobalization.cs | 11 ++- 15 files changed, 224 insertions(+), 42 deletions(-) diff --git a/src/libraries/Common/src/Interop/Browser/Interop.Locale.cs b/src/libraries/Common/src/Interop/Browser/Interop.Locale.cs index c882d88afac255..b831e72e70cdbc 100644 --- a/src/libraries/Common/src/Interop/Browser/Interop.Locale.cs +++ b/src/libraries/Common/src/Interop/Browser/Interop.Locale.cs @@ -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); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Browser.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Browser.cs index fcc5943e7b766a..8a8edadaf326d0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Browser.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Browser.cs @@ -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); diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Icu.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Icu.cs index 435e7ba9d45fd2..31cc5bdd601552 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Icu.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Icu.cs @@ -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); } @@ -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; diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs index 7013429ad4c0e5..189ce06de70335 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs @@ -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. diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CultureInfo/CultureInfoEnglishName.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CultureInfo/CultureInfoEnglishName.cs index 02ee539981adbc..0868869622a63e 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CultureInfo/CultureInfoEnglishName.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CultureInfo/CultureInfoEnglishName.cs @@ -9,13 +9,15 @@ 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 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)" }; @@ -23,7 +25,6 @@ public static IEnumerable EnglishName_TestData() } 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)" }; } @@ -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`"); } } } diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CultureInfo/CultureInfoNames.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CultureInfo/CultureInfoNames.cs index d2c0828bc68503..b4ba35adb85e4b 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CultureInfo/CultureInfoNames.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CultureInfo/CultureInfoNames.cs @@ -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 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))) diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CultureInfo/CultureInfoNativeName.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CultureInfo/CultureInfoNativeName.cs index 9a834dbf2d1dbd..c61f8364e20c8a 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/CultureInfo/CultureInfoNativeName.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/CultureInfo/CultureInfoNativeName.cs @@ -8,12 +8,16 @@ 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 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)" }; @@ -21,7 +25,6 @@ public static IEnumerable NativeName_TestData() } 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)" }; } diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/System/Globalization/RegionInfoTests.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/System/Globalization/RegionInfoTests.cs index e05fd4e1a7422c..ee39b219adb3d2 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/System/Globalization/RegionInfoTests.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/System/Globalization/RegionInfoTests.cs @@ -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")] @@ -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))) @@ -111,8 +115,7 @@ public void DisplayName(string name, string expected) public static IEnumerable 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" }; @@ -120,7 +123,6 @@ public static IEnumerable NativeName_TestData() } 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" }; @@ -136,8 +138,7 @@ public void NativeName(string name, string expected) public static IEnumerable 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" } }; @@ -146,7 +147,6 @@ public static IEnumerable 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" }}; diff --git a/src/mono/browser/runtime/corebindings.c b/src/mono/browser/runtime/corebindings.c index b2a271b4e91eb8..4ad5f3aed03583 100644 --- a/src/mono/browser/runtime/corebindings.c +++ b/src/mono/browser/runtime/corebindings.c @@ -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); @@ -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); diff --git a/src/mono/browser/runtime/exports-binding.ts b/src/mono/browser/runtime/exports-binding.ts index bb88ac3b65fbbd..153ac946010cdb 100644 --- a/src/mono/browser/runtime/exports-binding.ts +++ b/src/mono/browser/runtime/exports-binding.ts @@ -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"; @@ -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, ]; diff --git a/src/mono/browser/runtime/hybrid-globalization/helpers.ts b/src/mono/browser/runtime/hybrid-globalization/helpers.ts index 1590936d8eaccc..4cd1b9f4eb2420 100644 --- a/src/mono/browser/runtime/hybrid-globalization/helpers.ts +++ b/src/mono/browser/runtime/hybrid-globalization/helpers.ts @@ -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; } } diff --git a/src/mono/browser/runtime/hybrid-globalization/locales.ts b/src/mono/browser/runtime/hybrid-globalization/locales.ts index 4330d0977e3606..5b7c82a8faa58d 100644 --- a/src/mono/browser/runtime/hybrid-globalization/locales.ts +++ b/src/mono/browser/runtime/hybrid-globalization/locales.ts @@ -3,10 +3,98 @@ import { wrap_error_root, wrap_no_error_root } from "../invoke-js"; import { mono_wasm_new_external_root } from "../roots"; -import { monoStringToString } from "../strings"; +import { monoStringToString, stringToUTF16 } from "../strings"; import { Int32Ptr } from "../types/emscripten"; import { MonoObject, MonoObjectRef, MonoString, MonoStringRef } from "../types/internal"; -import { normalizeLocale } from "./helpers"; +import { OUTER_SEPARATOR, normalizeLocale } from "./helpers"; + +export function mono_wasm_get_locale_info(culture: MonoStringRef, locale: MonoStringRef, dst: number, dstLength: number, isException: Int32Ptr, exAddress: MonoObjectRef) : number +{ + const localeRoot = mono_wasm_new_external_root(locale), + cultureRoot = mono_wasm_new_external_root(culture), + exceptionRoot = mono_wasm_new_external_root(exAddress); + try { + const localeNameOriginal = monoStringToString(localeRoot); + const localeName = normalizeLocale(localeNameOriginal); + if (!localeName && localeNameOriginal) + { + // handle non-standard or malformed locales by forwarding the locale code + stringToUTF16(dst, dst + 2 * localeNameOriginal.length, localeNameOriginal); + wrap_no_error_root(isException, exceptionRoot); + return localeNameOriginal.length; + } + const cultureNameOriginal = monoStringToString(cultureRoot); + const cultureName = normalizeLocale(cultureNameOriginal); + + if (!localeName || !cultureName) + throw new Error(`Locale or culture name is null or empty. localeName=${localeName}, cultureName=${cultureName}`); + + const localeParts = localeName.split("-"); + // cultureName can be in a form of: + // 1) "language", e.g. "zh" + // 2) "language-region", e.g. "zn-CN" + // 3) "language-script-region", e.g. "zh-Hans-CN" + // 4) "language-script", e.g. "zh-Hans" (served in the catch block below) + let languageName, regionName; + try + { + const region = localeParts.length > 1 ? localeParts.pop() : undefined; + // this line might fail if form 4 from the comment above is used: + regionName = region ? new Intl.DisplayNames([cultureName], {type: "region"}).of(region) : undefined; + const language = localeParts.join("-"); + languageName = new Intl.DisplayNames([cultureName], {type: "language"}).of(language); + } + catch (error) + { + if (error instanceof RangeError && error.message === "invalid_argument") + { + // if it failed from this reason then cultureName is in a form "language-script", without region + try + { + languageName = new Intl.DisplayNames([cultureName], {type: "language"}).of(localeName); + } + catch (error) + { + if (error instanceof RangeError && error.message === "invalid_argument" && localeNameOriginal) + { + // handle non-standard or malformed locales by forwarding the locale code, e.g. "xx-u-xx" + stringToUTF16(dst, dst + 2 * localeNameOriginal.length, localeNameOriginal); + wrap_no_error_root(isException, exceptionRoot); + return localeNameOriginal.length; + } + throw error; + } + } + else + { + throw error; + } + } + const localeInfo = { + LanguageName: languageName, + RegionName: regionName, + }; + const result = Object.values(localeInfo).join(OUTER_SEPARATOR); + + if (!result) + throw new Error(`Locale info for locale=${localeName} is null or empty.`); + + if (result.length > dstLength) + throw new Error(`Locale info for locale=${localeName} exceeds length of ${dstLength}.`); + + stringToUTF16(dst, dst + 2 * result.length, result); + wrap_no_error_root(isException, exceptionRoot); + return result.length; + } + catch (ex: any) { + wrap_error_root(isException, ex, exceptionRoot); + return -1; + } + finally { + cultureRoot.release(); + exceptionRoot.release(); + } +} export function mono_wasm_get_first_day_of_week(culture: MonoStringRef, isException: Int32Ptr, exAddress: MonoObjectRef): number{ diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/WorkloadRequiredTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/WorkloadRequiredTests.cs index 0fd604cfea6e24..a735e2af15e444 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/WorkloadRequiredTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/WorkloadRequiredTests.cs @@ -144,7 +144,8 @@ await BlazorRunTest(new BlazorRunOptions() Assert.DoesNotContain("Could not create es-ES culture", output); Assert.DoesNotContain("invalid culture", output); Assert.DoesNotContain("CurrentCulture.NativeName: Invariant Language (Invariant Country)", output); - Assert.Contains("es-ES: Is-LCID-InvariantCulture: False, NativeName: es (ES)", output); + Assert.Contains("es-ES: Is-LCID-InvariantCulture: False", output); + Assert.Contains("NativeName: espa\u00F1ol (Espa\u00F1a)", output); // ignoring the last line of the output which prints the current culture } diff --git a/src/mono/wasm/Wasm.Build.Tests/InvariantGlobalizationTests.cs b/src/mono/wasm/Wasm.Build.Tests/InvariantGlobalizationTests.cs index 25f16fca67af4b..816f088666731c 100644 --- a/src/mono/wasm/Wasm.Build.Tests/InvariantGlobalizationTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/InvariantGlobalizationTests.cs @@ -69,8 +69,8 @@ private void TestInvariantGlobalization(BuildArgs buildArgs, bool? invariantGlob } else { - string output = RunAndTestWasmApp(buildArgs, expectedExitCode: 42, host: host, id: id); - Assert.Contains("es-ES: Is Invariant LCID: False, NativeName: es (ES)", output); + string output = RunAndTestWasmApp(buildArgs, expectedExitCode: 42, host: host, id: id, args: "nativename=\"espa\u00F1ol (Espa\u00F1a)\""); + Assert.Contains("es-ES: Is Invariant LCID: False", output); // ignoring the last line of the output which prints the current culture } diff --git a/src/mono/wasm/testassets/Wasm.Buid.Tests.Programs/InvariantGlobalization.cs b/src/mono/wasm/testassets/Wasm.Buid.Tests.Programs/InvariantGlobalization.cs index 9237110cbc4980..c7b1219b6aabde 100644 --- a/src/mono/wasm/testassets/Wasm.Buid.Tests.Programs/InvariantGlobalization.cs +++ b/src/mono/wasm/testassets/Wasm.Buid.Tests.Programs/InvariantGlobalization.cs @@ -1,11 +1,20 @@ using System; using System.Globalization; +using System.Linq; // https://github.com/dotnet/runtime/blob/main/docs/design/features/globalization-invariant-mode.md#cultures-and-culture-data try { CultureInfo culture = new ("es-ES", false); - Console.WriteLine($"es-ES: Is Invariant LCID: {culture.LCID == CultureInfo.InvariantCulture.LCID}, NativeName: {culture.NativeName}"); + Console.WriteLine($"es-ES: Is Invariant LCID: {culture.LCID == CultureInfo.InvariantCulture.LCID}"); + + var nativeNameArg = args.FirstOrDefault(arg => arg.StartsWith("nativename=")); + if (nativeNameArg == null) + throw new ArgumentException($"When not in invariant mode, InvariantGlobalization.cs expects nativename argument with expected es-ES NativeName."); + string expectedNativeName = nativeNameArg.Substring(11).Trim('"'); // skip nativename= + string nativeName = culture.NativeName; + if (nativeName != expectedNativeName) + throw new ArgumentException($"Expected es-ES NativeName: {expectedNativeName}, but got: {nativeName}"); } catch (CultureNotFoundException cnfe) {