Skip to content

Commit 3e7a106

Browse files
author
Pranav Krishnamoorthy
committed
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
1 parent 864a292 commit 3e7a106

15 files changed

+75
-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,12 +3,12 @@ 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
import { initializeProfiling } from '../Profiling';
98

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

@@ -244,14 +244,26 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
244244
/* hash */ resourceLoader.bootConfig.resources.runtime[dotnetWasmResourceName],
245245
/* type */ 'dotnetwasm');
246246

247-
const dotnetTimeZoneResourceName = 'dotnet.timezones.dat';
247+
const dotnetTimeZoneResourceName = 'dotnet.timezones.blat';
248248
let timeZoneResource: LoadingResource | undefined;
249249
if (resourceLoader.bootConfig.resources.runtime.hasOwnProperty(dotnetTimeZoneResourceName)) {
250250
timeZoneResource = resourceLoader.loadResource(
251251
dotnetTimeZoneResourceName,
252252
`_framework/${dotnetTimeZoneResourceName}`,
253253
resourceLoader.bootConfig.resources.runtime[dotnetTimeZoneResourceName],
254-
'timezonedata');
254+
'globalization');
255+
}
256+
257+
let icuDataResource: LoadingResource | undefined;
258+
if (resourceLoader.bootConfig.resources.runtime.hasOwnProperty(icuDataResourceName)) {
259+
icuDataResource = resourceLoader.loadResource(
260+
icuDataResourceName,
261+
`_framework/${icuDataResourceName}`,
262+
resourceLoader.bootConfig.resources.runtime[icuDataResourceName],
263+
'globalization');
264+
} else {
265+
// Use invariant culture if the app does not carry icu data.
266+
MONO.mono_wasm_setenv("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "1");
255267
}
256268

257269
// Override the mechanism for fetching the main wasm file so we can connect it to our cache
@@ -279,6 +291,10 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
279291
loadTimezone(timeZoneResource);
280292
}
281293

294+
if (icuDataResource) {
295+
loadICUData(icuDataResource);
296+
}
297+
282298
// Fetch the assemblies and PDBs in the background, telling Mono to wait until they are loaded
283299
// Mono requires the assembly filenames to have a '.dll' extension, so supply such names regardless
284300
// of the extensions in the URLs. This allows loading assemblies with arbitrary filenames.
@@ -363,7 +379,11 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
363379
resourceLoader.purgeUnusedCacheEntriesAsync(); // Don't await - it's fine to run in background
364380

365381
MONO.mono_wasm_setenv("MONO_URI_DOTNETRELATIVEORABSOLUTE", "true");
366-
MONO.mono_wasm_setenv("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "1");
382+
let timeZone = "UTC";
383+
try {
384+
timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
385+
} catch { }
386+
MONO.mono_wasm_setenv("TZ", timeZone);
367387
const load_runtime = cwrap('mono_wasm_load_runtime', null, ['string', 'number']);
368388
// -1 enables debugging with logging disabled. 0 disables debugging entirely.
369389
load_runtime(appBinDirName, hasDebuggingEnabled() ? -1 : 0);
@@ -461,8 +481,27 @@ async function loadTimezone(timeZoneResource: LoadingResource) : Promise<void> {
461481

462482
const request = await timeZoneResource.response;
463483
const arrayBuffer = await request.arrayBuffer();
464-
loadTimezoneData(arrayBuffer)
465484

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

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
@@ -14,4 +14,4 @@ export interface WebAssemblyStartOptions {
1414
// This type doesn't have to align with anything in BootConfig.
1515
// Instead, this represents the public API through which certain aspects
1616
// of boot resource loading can be customized.
17-
export type WebAssemblyBootResourceType = 'assembly' | 'pdb' | 'dotnetjs' | 'dotnetwasm' | 'timezonedata';
17+
export type WebAssemblyBootResourceType = 'assembly' | 'pdb' | 'dotnetjs' | 'dotnetwasm' | 'globalization';

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

Lines changed: 5 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,10 @@ 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);
196197

197-
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "wasm", "dotnet.wasm");
198-
Assert.FileDoesNotExist(result, buildOutputDirectory, "wwwroot", "_framework", "wasm", "dotnet.timezones.dat");
198+
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "dotnet.wasm");
199+
Assert.FileDoesNotExist(result, buildOutputDirectory, "wwwroot", "_framework", "dotnet.timezones.blat");
199200
}
200201

201202
[Fact]

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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ 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)" Condition="'$(BlazorEnableTimeZoneSupport)' == 'false' AND '%(ReferenceCopyLocalPaths.FileName)' == 'dotnet.timezones'" />
127+
126128
<!--
127129
ReferenceCopyLocalPaths includes satellite assemblies from referenced projects but are inexpicably missing
128130
any metadata that might allow them to be differentiated. We'll explicitly add those

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
@@ -974,8 +974,8 @@ public void CanBindTextboxNullableDateTime_InvalidValue()
974974
// Modify target to something invalid - the invalid change is reverted
975975
// back to the last valid value
976976
target.SendKeys(Keys.Control + "a"); // select all
977-
target.SendKeys("05/06A");
978-
Browser.Equal("05/06A", () => target.GetAttribute("value"));
977+
target.SendKeys("05/06X");
978+
Browser.Equal("05/06X", () => target.GetAttribute("value"));
979979
target.SendKeys("\t");
980980
Browser.Equal(expected, () => DateTime.Parse(target.GetAttribute("value")));
981981
Assert.Equal(expected, DateTime.Parse(boundValue.Text));
@@ -1012,8 +1012,8 @@ public void CanBindTextboxDateTimeOffset_InvalidValue()
10121012
// Modify target to something invalid - the invalid change is reverted
10131013
// back to the last valid value
10141014
target.SendKeys(Keys.Control + "a"); // select all
1015-
target.SendKeys("05/06A");
1016-
Browser.Equal("05/06A", () => target.GetAttribute("value"));
1015+
target.SendKeys("05/06X");
1016+
Browser.Equal("05/06X", () => target.GetAttribute("value"));
10171017
target.SendKeys("\t");
10181018
Browser.Equal(expected.DateTime, () => DateTimeOffset.Parse(target.GetAttribute("value")).DateTime);
10191019
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" />

src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Microsoft.NET.Sdk.Razor.Components.Wasm.targets

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

109+
<!-- Remove the timezone file if time-zone support is disabled -->
110+
<ReferenceCopyLocalPaths Remove="@(ReferenceCopyLocalPaths)" Condition="'$(BlazorEnableTimeZoneSupport)' == 'false' AND '%(ReferenceCopyLocalPaths.FileName)' == 'dotnet.timezones'" />
111+
109112
<!--
110113
ReferenceCopyLocalPaths includes satellite assemblies from referenced projects but are inexpicably missing
111114
any metadata that might allow them to be differentiated. We'll explicitly add those

0 commit comments

Comments
 (0)