Skip to content

Commit 2ad1b6d

Browse files
authored
Merged PR 9371: Revive support for globalization and localization in Blazor WASM (#24773)
* Merged PR 9371: Revive support for globalization and localization in Blazor WASM Revive support for globalization and localization in Blazor WASM * Load icu and timezone data files * Unskip tests Fixes #24174 Fixes #22975 Fixes #23260
2 parents f68f5b0 + 8a8a1ac commit 2ad1b6d

16 files changed

+140
-64
lines changed

src/Components/Web.JS/dist/Release/blazor.server.js

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/dist/Release/blazor.webassembly.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import { attachDebuggerHotkey, hasDebuggingEnabled } from './MonoDebugger';
33
import { showErrorNotification } from '../../BootErrors';
44
import { WebAssemblyResourceLoader, LoadingResource } from '../WebAssemblyResourceLoader';
55
import { Platform, System_Array, Pointer, System_Object, System_String, HeapLock } from '../Platform';
6-
import { loadTimezoneData } from './TimezoneDataFile';
76
import { WebAssemblyBootResourceType } from '../WebAssemblyStartOptions';
87

98
let mono_wasm_add_assembly: (name: string, heapAddress: number, length: number) => void;
109
const appBinDirName = 'appBinDir';
10+
const icuDataResourceName = 'icudt.dat';
1111
const uint64HighOrderShift = Math.pow(2, 32);
1212
const maxSafeNumberHighPart = Math.pow(2, 21) - 1; // The high-order int32 from Number.MAX_SAFE_INTEGER
1313

@@ -239,14 +239,23 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
239239
/* hash */ resourceLoader.bootConfig.resources.runtime[dotnetWasmResourceName],
240240
/* type */ 'dotnetwasm');
241241

242-
const dotnetTimeZoneResourceName = 'dotnet.timezones.dat';
242+
const dotnetTimeZoneResourceName = 'dotnet.timezones.blat';
243243
let timeZoneResource: LoadingResource | undefined;
244244
if (resourceLoader.bootConfig.resources.runtime.hasOwnProperty(dotnetTimeZoneResourceName)) {
245245
timeZoneResource = resourceLoader.loadResource(
246246
dotnetTimeZoneResourceName,
247247
`_framework/${dotnetTimeZoneResourceName}`,
248248
resourceLoader.bootConfig.resources.runtime[dotnetTimeZoneResourceName],
249-
'timezonedata');
249+
'globalization');
250+
}
251+
252+
let icuDataResource: LoadingResource | undefined;
253+
if (resourceLoader.bootConfig.resources.runtime.hasOwnProperty(icuDataResourceName)) {
254+
icuDataResource = resourceLoader.loadResource(
255+
icuDataResourceName,
256+
`_framework/${icuDataResourceName}`,
257+
resourceLoader.bootConfig.resources.runtime[icuDataResourceName],
258+
'globalization');
250259
}
251260

252261
// Override the mechanism for fetching the main wasm file so we can connect it to our cache
@@ -274,6 +283,13 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
274283
loadTimezone(timeZoneResource);
275284
}
276285

286+
if (icuDataResource) {
287+
loadICUData(icuDataResource);
288+
} else {
289+
// Use invariant culture if the app does not carry icu data.
290+
MONO.mono_wasm_setenv("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "1");
291+
}
292+
277293
// Fetch the assemblies and PDBs in the background, telling Mono to wait until they are loaded
278294
// Mono requires the assembly filenames to have a '.dll' extension, so supply such names regardless
279295
// of the extensions in the URLs. This allows loading assemblies with arbitrary filenames.
@@ -358,7 +374,11 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
358374
resourceLoader.purgeUnusedCacheEntriesAsync(); // Don't await - it's fine to run in background
359375

360376
MONO.mono_wasm_setenv("MONO_URI_DOTNETRELATIVEORABSOLUTE", "true");
361-
MONO.mono_wasm_setenv("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "1");
377+
let timeZone = "UTC";
378+
try {
379+
timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
380+
} catch { }
381+
MONO.mono_wasm_setenv("TZ", timeZone);
362382
// Turn off full-gc to prevent browser freezing.
363383
const mono_wasm_enable_on_demand_gc = cwrap('mono_wasm_enable_on_demand_gc', null, ['number']);
364384
mono_wasm_enable_on_demand_gc(0);
@@ -459,8 +479,27 @@ async function loadTimezone(timeZoneResource: LoadingResource) : Promise<void> {
459479

460480
const request = await timeZoneResource.response;
461481
const arrayBuffer = await request.arrayBuffer();
462-
loadTimezoneData(arrayBuffer)
463482

483+
Module['FS_createPath']('/', 'usr', true, true);
484+
Module['FS_createPath']('/usr/', 'share', true, true);
485+
Module['FS_createPath']('/usr/share/', 'zoneinfo', true, true);
486+
MONO.mono_wasm_load_data_archive(new Uint8Array(arrayBuffer), '/usr/share/zoneinfo/');
487+
488+
removeRunDependency(runDependencyId);
489+
}
490+
491+
async function loadICUData(icuDataResource: LoadingResource) : Promise<void> {
492+
const runDependencyId = `blazor:icudata`;
493+
addRunDependency(runDependencyId);
494+
495+
const request = await icuDataResource.response;
496+
const array = new Uint8Array(await request.arrayBuffer());
497+
498+
const offset = MONO.mono_wasm_load_bytes_into_heap(array);
499+
if (!MONO.mono_wasm_load_icu_data(offset))
500+
{
501+
throw new Error("Error loading ICU asset.");
502+
}
464503
removeRunDependency(runDependencyId);
465504
}
466505

src/Components/Web.JS/src/Platform/Mono/MonoTypes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ declare interface MONO {
66
loaded_files: string[];
77
mono_wasm_runtime_ready (): void;
88
mono_wasm_setenv (name: string, value: string): void;
9+
mono_wasm_load_data_archive(data: Uint8Array, prefix: string): void;
10+
mono_wasm_load_bytes_into_heap (data: Uint8Array): Pointer;
11+
mono_wasm_load_icu_data(heapAddress: Pointer): boolean;
912
}
1013

1114
// Mono uses this global to hold low-level interop APIs

src/Components/Web.JS/src/Platform/Mono/TimezoneDataFile.ts

Lines changed: 0 additions & 43 deletions
This file was deleted.

src/Components/Web.JS/src/Platform/WebAssemblyStartOptions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ export interface WebAssemblyStartOptions {
1919
// This type doesn't have to align with anything in BootConfig.
2020
// Instead, this represents the public API through which certain aspects
2121
// of boot resource loading can be customized.
22-
export type WebAssemblyBootResourceType = 'assembly' | 'pdb' | 'dotnetjs' | 'dotnetwasm' | 'timezonedata';
22+
export type WebAssemblyBootResourceType = 'assembly' | 'pdb' | 'dotnetjs' | 'dotnetwasm' | 'globalization';

src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIntegrationTest.cs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public async Task BuildMinimal_Works()
3131
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json");
3232
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazor.webassembly.js");
3333
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "dotnet.wasm");
34+
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "dotnet.timezones.blat");
3435
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "dotnet.wasm.gz");
3536
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", DotNetJsFileName);
3637
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazorwasm-minimal.dll");
@@ -169,7 +170,7 @@ public async Task Build_InRelease_ProducesBootJsonDataWithExpectedContent()
169170
Assert.Null(bootJsonData.resources.satelliteResources);
170171
}
171172

172-
[Fact(Skip = "https://github.com/dotnet/aspnetcore/issues/22975")]
173+
[Fact]
173174
public async Task Build_WithBlazorEnableTimeZoneSupportDisabled_DoesNotCopyTimeZoneInfo()
174175
{
175176
// Arrange
@@ -192,10 +193,39 @@ public async Task Build_WithBlazorEnableTimeZoneSupportDisabled_DoesNotCopyTimeZ
192193

193194
var runtime = bootJsonData.resources.runtime.Keys;
194195
Assert.Contains("dotnet.wasm", runtime);
195-
Assert.DoesNotContain("dotnet.timezones.dat", runtime);
196+
Assert.DoesNotContain("dotnet.timezones.blat", runtime);
197+
198+
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "dotnet.wasm");
199+
Assert.FileDoesNotExist(result, buildOutputDirectory, "wwwroot", "_framework", "dotnet.timezones.blat");
200+
}
201+
202+
[Fact]
203+
public async Task Build_WithInvariantGlobalizationEnabled_DoesNotCopyGlobalizationData()
204+
{
205+
// Arrange
206+
using var project = ProjectDirectory.Create("blazorwasm-minimal");
207+
project.AddProjectFileContent(
208+
@"
209+
<PropertyGroup>
210+
<InvariantGlobalization>true</InvariantGlobalization>
211+
</PropertyGroup>");
212+
213+
var result = await MSBuildProcessManager.DotnetMSBuild(project);
214+
215+
Assert.BuildPassed(result);
216+
217+
var buildOutputDirectory = project.BuildOutputDirectory;
218+
219+
var bootJsonPath = Path.Combine(buildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json");
220+
var bootJsonData = ReadBootJsonData(result, bootJsonPath);
221+
222+
var runtime = bootJsonData.resources.runtime.Keys;
223+
Assert.Contains("dotnet.wasm", runtime);
224+
Assert.Contains("dotnet.timezones.blat", runtime);
225+
Assert.DoesNotContain("icudt.dat", runtime);
196226

197-
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "wasm", "dotnet.wasm");
198-
Assert.FileDoesNotExist(result, buildOutputDirectory, "wwwroot", "_framework", "wasm", "dotnet.timezones.dat");
227+
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "dotnet.wasm");
228+
Assert.FileDoesNotExist(result, buildOutputDirectory, "wwwroot", "_framework", "icudt.dat");
199229
}
200230

201231
[Fact]

src/Components/WebAssembly/Sdk/integrationtests/WasmPublishIntegrationTest.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,34 @@ private static void AssertRIDPublishOuput(ProjectDirectory project, MSBuildResul
823823
assetsManifestPath: "custom-service-worker-assets.js");
824824
}
825825

826+
[Fact]
827+
public async Task Publish_WithInvariantGlobalizationEnabled_DoesNotCopyGlobalizationData()
828+
{
829+
// Arrange
830+
using var project = ProjectDirectory.Create("blazorwasm-minimal");
831+
project.AddProjectFileContent(
832+
@"
833+
<PropertyGroup>
834+
<InvariantGlobalization>true</InvariantGlobalization>
835+
</PropertyGroup>");
836+
837+
var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish");
838+
839+
Assert.BuildPassed(result);
840+
841+
var publishOutputDirectory = project.PublishOutputDirectory;
842+
843+
var bootJsonPath = Path.Combine(publishOutputDirectory, "wwwroot", "_framework", "blazor.boot.json");
844+
var bootJsonData = ReadBootJsonData(result, bootJsonPath);
845+
846+
var runtime = bootJsonData.resources.runtime.Keys;
847+
Assert.Contains("dotnet.wasm", runtime);
848+
Assert.DoesNotContain("icudt.dat", runtime);
849+
850+
Assert.FileExists(result, publishOutputDirectory, "wwwroot", "_framework", "dotnet.wasm");
851+
Assert.FileDoesNotExist(result, publishOutputDirectory, "wwwroot", "_framework", "icudt.dat");
852+
}
853+
826854
private static void AddWasmProjectContent(ProjectDirectory project, string content)
827855
{
828856
var path = Path.Combine(project.SolutionPath, "blazorwasm", "blazorwasm.csproj");

src/Components/WebAssembly/Sdk/src/targets/BlazorWasm.web.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<configuration>
33
<system.webServer>
44
<staticContent>
5+
<remove fileExtension=".blat" />
56
<remove fileExtension=".dat" />
67
<remove fileExtension=".dll" />
78
<remove fileExtension=".json" />

src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,12 @@ Copyright (c) .NET Foundation. All rights reserved.
123123
<!-- Clear out temporary build artifacts that the runtime packages -->
124124
<ReferenceCopyLocalPaths Remove="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.a'" />
125125

126+
<ReferenceCopyLocalPaths Remove="@(ReferenceCopyLocalPaths)"
127+
Condition="'$(BlazorEnableTimeZoneSupport)' == 'false' AND '%(ReferenceCopyLocalPaths.FileName)%(ReferenceCopyLocalPaths.Extension)' == 'dotnet.timezones.blat'" />
128+
129+
<ReferenceCopyLocalPaths Remove="@(ReferenceCopyLocalPaths)"
130+
Condition="'$(InvariantGlobalization)' == 'true' AND '%(ReferenceCopyLocalPaths.FileName)%(ReferenceCopyLocalPaths.Extension)' == 'icudt.dat'" />
131+
126132
<!--
127133
ReferenceCopyLocalPaths includes satellite assemblies from referenced projects but are inexpicably missing
128134
any metadata that might allow them to be differentiated. We'll explicitly add those
@@ -430,6 +436,12 @@ Copyright (c) .NET Foundation. All rights reserved.
430436

431437
<ResolvedFileToPublish Remove="@(ResolvedFileToPublish)" Condition="'%(Extension)' == '.a'" />
432438

439+
<ResolvedFileToPublish Remove="@(ResolvedFileToPublish)"
440+
Condition="'$(BlazorEnableTimeZoneSupport)' == 'false' AND '%(ResolvedFileToPublish.FileName)%(ResolvedFileToPublish.Extension)' == 'dotnet.timezones.blat'" />
441+
442+
<ResolvedFileToPublish Remove="@(ResolvedFileToPublish)"
443+
Condition="'$(InvariantGlobalization)' == 'true' AND '%(ResolvedFileToPublish.FileName)%(ResolvedFileToPublish.Extension)' == 'icudt.dat'" />
444+
433445
<!-- Remove dotnet.js from publish output -->
434446
<ResolvedFileToPublish Remove="@(ResolvedFileToPublish)" Condition="'%(ResolvedFileToPublish.RelativePath)' == 'dotnet.js'" />
435447

src/Components/WebAssembly/Sdk/testassets/Directory.Build.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<RazorSdkCurrentVersionTargets>$(RepoRoot)src\Razor\Microsoft.NET.Sdk.Razor\src\build\netstandard2.0\Sdk.Razor.CurrentVersion.targets</RazorSdkCurrentVersionTargets>
2121
<RazorSdkArtifactsDirectory>$(RepoRoot)artifacts\bin\Microsoft.NET.Sdk.Razor\</RazorSdkArtifactsDirectory>
2222
<BlazorWebAssemblySdkArtifactsDirectory>$(RepoRoot)artifacts\bin\Microsoft.NET.Sdk.BlazorWebAssembly\</BlazorWebAssemblySdkArtifactsDirectory>
23+
<_BlazorWebAssemblyTargetsFile>$(RepoRoot)src\Components\WebAssembly\Sdk\src\targets\Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets</_BlazorWebAssemblyTargetsFile>
2324
<BlazorWebAssemblyJSPath>$(MSBuildThisFileDirectory)blazor.webassembly.js</BlazorWebAssemblyJSPath>
2425
</PropertyGroup>
2526

src/Components/WebAssembly/Server/src/ComponentsWebAssemblyApplicationBuilderExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ private static StaticFileOptions CreateStaticFilesOptions(IFileProvider webRootF
7777
AddMapping(contentTypeProvider, ".pdb", MediaTypeNames.Application.Octet);
7878
AddMapping(contentTypeProvider, ".br", MediaTypeNames.Application.Octet);
7979
AddMapping(contentTypeProvider, ".dat", MediaTypeNames.Application.Octet);
80+
AddMapping(contentTypeProvider, ".blat", MediaTypeNames.Application.Octet);
8081

8182
options.ContentTypeProvider = contentTypeProvider;
8283

src/Components/WebAssembly/WebAssembly/src/Hosting/SatelliteResourcesLoader.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33

44
using System.Collections.Generic;
55
using System.Globalization;
6+
using System.IO;
67
using System.Reflection;
8+
using System.Runtime.Loader;
79
using System.Threading.Tasks;
810
using Microsoft.AspNetCore.Components.WebAssembly.Services;
911

@@ -55,7 +57,8 @@ public virtual async ValueTask LoadCurrentCultureResourcesAsync()
5557

5658
for (var i = 0; i < assemblies.Length; i++)
5759
{
58-
Assembly.Load((byte[])assemblies[i]);
60+
using var stream = new MemoryStream((byte[])assemblies[i]);
61+
AssemblyLoadContext.Default.LoadFromStream(stream);
5962
}
6063
}
6164

src/Components/test/E2ETest/Tests/BindTest.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -979,8 +979,8 @@ public void CanBindTextboxNullableDateTime_InvalidValue()
979979
// Modify target to something invalid - the invalid change is reverted
980980
// back to the last valid value
981981
target.SendKeys(Keys.Control + "a"); // select all
982-
target.SendKeys("05/06A");
983-
Browser.Equal("05/06A", () => target.GetAttribute("value"));
982+
target.SendKeys("05/06X");
983+
Browser.Equal("05/06X", () => target.GetAttribute("value"));
984984
target.SendKeys("\t");
985985
Browser.Equal(expected, () => DateTime.Parse(target.GetAttribute("value")));
986986
Assert.Equal(expected, DateTime.Parse(boundValue.Text));
@@ -1017,8 +1017,8 @@ public void CanBindTextboxDateTimeOffset_InvalidValue()
10171017
// Modify target to something invalid - the invalid change is reverted
10181018
// back to the last valid value
10191019
target.SendKeys(Keys.Control + "a"); // select all
1020-
target.SendKeys("05/06A");
1021-
Browser.Equal("05/06A", () => target.GetAttribute("value"));
1020+
target.SendKeys("05/06X");
1021+
Browser.Equal("05/06X", () => target.GetAttribute("value"));
10221022
target.SendKeys("\t");
10231023
Browser.Equal(expected.DateTime, () => DateTimeOffset.Parse(target.GetAttribute("value")).DateTime);
10241024
Assert.Equal(expected.DateTime, DateTimeOffset.Parse(boundValue.Text).DateTime);

src/Components/test/E2ETest/Tests/WebAssemblyLocalizationTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public WebAssemblyLocalizationTest(
2222
{
2323
}
2424

25-
[Theory(Skip = "https://github.com/dotnet/runtime/issues/38124")]
25+
[Theory]
2626
[InlineData("en-US", "Hello!")]
2727
[InlineData("fr-FR", "Bonjour!")]
2828
public void CanSetCultureAndReadLocalizedResources(string culture, string message)

src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/BlazorWasm.web.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<configuration>
33
<system.webServer>
44
<staticContent>
5+
<remove fileExtension=".blat" />
56
<remove fileExtension=".dat" />
67
<remove fileExtension=".dll" />
78
<remove fileExtension=".json" />

0 commit comments

Comments
 (0)