Skip to content

Commit c3df503

Browse files
Pranav Krishnamoorthypranavkm
Pranav Krishnamoorthy
authored andcommitted
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 059e2fd commit c3df503

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,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,26 @@ 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');
259+
} else {
260+
// Use invariant culture if the app does not carry icu data.
261+
MONO.mono_wasm_setenv("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "1");
250262
}
251263

252264
// Override the mechanism for fetching the main wasm file so we can connect it to our cache
@@ -274,6 +286,10 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
274286
loadTimezone(timeZoneResource);
275287
}
276288

289+
if (icuDataResource) {
290+
loadICUData(icuDataResource);
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: 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
@@ -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" />

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)