diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyCultureProvider.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyCultureProvider.cs index 060bd827b93f..99d1476dc10d 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyCultureProvider.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyCultureProvider.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Linq; using System.Runtime.InteropServices.JavaScript; namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting; @@ -62,7 +63,7 @@ public virtual async ValueTask LoadCurrentCultureResourcesAsync() throw new PlatformNotSupportedException("This method is only supported in the browser."); } - var culturesToLoad = GetCultures(CultureInfo.CurrentCulture); + var culturesToLoad = GetCultures(CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture); if (culturesToLoad.Length == 0) { @@ -72,30 +73,47 @@ public virtual async ValueTask LoadCurrentCultureResourcesAsync() await WebAssemblyCultureProviderInterop.LoadSatelliteAssemblies(culturesToLoad); } - internal static string[] GetCultures(CultureInfo cultureInfo) + internal static string[] GetCultures(CultureInfo cultureInfo, CultureInfo? uiCultureInfo = null) { - var culturesToLoad = new List(); - // Once WASM is ready, we have to use .NET's assembly loading to load additional assemblies. // First calculate all possible cultures that the application might want to load. We do this by // starting from the current culture and walking up the graph of parents. // At the end of the the walk, we'll have a list of culture names that look like // [ "fr-FR", "fr" ] - while (cultureInfo != null && cultureInfo != CultureInfo.InvariantCulture) - { - culturesToLoad.Add(cultureInfo.Name); - if (cultureInfo.Parent == cultureInfo) + var culturesToLoad = GetCultureHierarchy(cultureInfo); + if (cultureInfo != uiCultureInfo) + { + foreach (var culture in GetCultureHierarchy(uiCultureInfo)) { - break; + if (!culturesToLoad.Contains(culture)) + { + culturesToLoad = culturesToLoad.Append(culture); + } + // If the culture is in the list, we can break because we found the common parent. + else + { + break; + } } - - cultureInfo = cultureInfo.Parent; } return culturesToLoad.ToArray(); } + private static IEnumerable GetCultureHierarchy(CultureInfo? culture) + { + while (culture != CultureInfo.InvariantCulture && culture != null) + { + yield return culture.Name; + if (culture == culture.Parent) + { + break; + } + culture = culture.Parent; + } + } + private partial class WebAssemblyCultureProviderInterop { [JSImport("INTERNAL.loadSatelliteAssemblies")] diff --git a/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyCultureProviderTest.cs b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyCultureProviderTest.cs index 4e4d0dc943ee..22ad7d18fffc 100644 --- a/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyCultureProviderTest.cs +++ b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyCultureProviderTest.cs @@ -24,6 +24,21 @@ public void GetCultures_ReturnsCultureClosure(string cultureName, string[] expec Assert.Equal(expected, actual); } + [Theory] + [InlineData("fr-FR", "tzm-Latn-DZ", new[] { "fr-FR", "fr", "tzm-Latn-DZ", "tzm-Latn", "tzm" })] + [InlineData("en-US", "en-GB", new[] { "en-US", "en", "en-GB" })] + [InlineData("fr-FR", null, new[] { "fr-FR", "fr" })] + public void GetCultures_ReturnCultureClosureWithUICulture(string cultureName, string uiCultureName, string[] expected) + { + // Arrange + var culture = cultureName != null ? new CultureInfo(cultureName) : null; + var uiCulture = uiCultureName != null ? new CultureInfo(uiCultureName) : null; + // Act + var actual = WebAssemblyCultureProvider.GetCultures(culture, uiCulture); + // Assert + Assert.Equal(expected, actual); + } + [Fact] public void ThrowIfCultureChangeIsUnsupported_ThrowsIfCulturesAreDifferentAndICUShardingIsUsed() { diff --git a/src/Components/test/E2ETest/Tests/WebAssemblyLocalizationTest.cs b/src/Components/test/E2ETest/Tests/WebAssemblyLocalizationTest.cs index 2265de49da5f..0f04220d02a2 100644 --- a/src/Components/test/E2ETest/Tests/WebAssemblyLocalizationTest.cs +++ b/src/Components/test/E2ETest/Tests/WebAssemblyLocalizationTest.cs @@ -35,4 +35,23 @@ public void CanSetCultureAndReadLocalizedResources(string culture, string messag var messageDisplay = Browser.Exists(By.Id("message-display")); Assert.Equal(message, messageDisplay.Text); } + + [Theory] + [InlineData("en-US", "fr-FR", "Bonjour!")] + [InlineData("fr-FR", "en-US", "Hello!")] + public void CanSetCultureAndDifferentiateBetweenCurrentAndUICulture(string culture, string cultureUI, string message) + { + Navigate($"{ServerPathBase}/?culture={culture}&cultureUI={cultureUI}"); + + Browser.MountTestComponent(); + + var cultureDisplay = Browser.Exists(By.Id("culture-name-display")); + Assert.Equal($"Culture is: {culture}", cultureDisplay.Text); + + var cultureUIDisplay = Browser.Exists(By.Id("culture-ui-display")); + Assert.Equal($"CultureUI is: {cultureUI}", cultureUIDisplay.Text); + + var messageDisplay = Browser.Exists(By.Id("message-display")); + Assert.Equal(message, messageDisplay.Text); + } } diff --git a/src/Components/test/testassets/BasicTestApp/LocalizedText.razor b/src/Components/test/testassets/BasicTestApp/LocalizedText.razor index a86579e3c975..79ac237568b7 100644 --- a/src/Components/test/testassets/BasicTestApp/LocalizedText.razor +++ b/src/Components/test/testassets/BasicTestApp/LocalizedText.razor @@ -1,2 +1,3 @@ 

Culture is: @System.Globalization.CultureInfo.CurrentCulture.Name

+

CultureUI is: @System.Globalization.CultureInfo.CurrentUICulture.Name

@Resources.Message

diff --git a/src/Components/test/testassets/BasicTestApp/Program.cs b/src/Components/test/testassets/BasicTestApp/Program.cs index 554e65ba1ffe..0332b3915c88 100644 --- a/src/Components/test/testassets/BasicTestApp/Program.cs +++ b/src/Components/test/testassets/BasicTestApp/Program.cs @@ -63,6 +63,7 @@ private static void ConfigureCulture(WebAssemblyHost host) { // In the absence of a specified value, we want the culture to be en-US so that the tests for bind can work consistently. var culture = new CultureInfo("en-US"); + var cultureUI = new CultureInfo("en-US"); Uri uri = null; try @@ -77,12 +78,18 @@ private static void ConfigureCulture(WebAssemblyHost host) if (uri != null && HttpUtility.ParseQueryString(uri.Query)["culture"] is string cultureName) { culture = new CultureInfo(cultureName); + cultureUI = culture; // Default to the same culture for UI if not specified + } + + if (uri != null && HttpUtility.ParseQueryString(uri.Query)["cultureUI"] is string cultureUIName) + { + cultureUI = new CultureInfo(cultureUIName); } // CultureInfo.CurrentCulture is async-scoped and will not affect the culture in sibling scopes. // Use CultureInfo.DefaultThreadCurrentCulture instead to modify the application's default scope. CultureInfo.DefaultThreadCurrentCulture = culture; - CultureInfo.DefaultThreadCurrentUICulture = culture; + CultureInfo.DefaultThreadCurrentUICulture = cultureUI; } // Supports E2E tests in StartupErrorNotificationTest