Skip to content

Commit

Permalink
Merged PR 9371: Revive support for globalization and localization in …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
pranavkm authored Aug 18, 2020
2 parents f68f5b0 + 8a8a1ac commit 2ad1b6d
Show file tree
Hide file tree
Showing 16 changed files with 140 additions and 64 deletions.
8 changes: 4 additions & 4 deletions src/Components/Web.JS/dist/Release/blazor.server.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.webassembly.js

Large diffs are not rendered by default.

49 changes: 44 additions & 5 deletions src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { attachDebuggerHotkey, hasDebuggingEnabled } from './MonoDebugger';
import { showErrorNotification } from '../../BootErrors';
import { WebAssemblyResourceLoader, LoadingResource } from '../WebAssemblyResourceLoader';
import { Platform, System_Array, Pointer, System_Object, System_String, HeapLock } from '../Platform';
import { loadTimezoneData } from './TimezoneDataFile';
import { WebAssemblyBootResourceType } from '../WebAssemblyStartOptions';

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

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

const dotnetTimeZoneResourceName = 'dotnet.timezones.dat';
const dotnetTimeZoneResourceName = 'dotnet.timezones.blat';
let timeZoneResource: LoadingResource | undefined;
if (resourceLoader.bootConfig.resources.runtime.hasOwnProperty(dotnetTimeZoneResourceName)) {
timeZoneResource = resourceLoader.loadResource(
dotnetTimeZoneResourceName,
`_framework/${dotnetTimeZoneResourceName}`,
resourceLoader.bootConfig.resources.runtime[dotnetTimeZoneResourceName],
'timezonedata');
'globalization');
}

let icuDataResource: LoadingResource | undefined;
if (resourceLoader.bootConfig.resources.runtime.hasOwnProperty(icuDataResourceName)) {
icuDataResource = resourceLoader.loadResource(
icuDataResourceName,
`_framework/${icuDataResourceName}`,
resourceLoader.bootConfig.resources.runtime[icuDataResourceName],
'globalization');
}

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

if (icuDataResource) {
loadICUData(icuDataResource);
} else {
// Use invariant culture if the app does not carry icu data.
MONO.mono_wasm_setenv("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "1");
}

// Fetch the assemblies and PDBs in the background, telling Mono to wait until they are loaded
// Mono requires the assembly filenames to have a '.dll' extension, so supply such names regardless
// of the extensions in the URLs. This allows loading assemblies with arbitrary filenames.
Expand Down Expand Up @@ -358,7 +374,11 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
resourceLoader.purgeUnusedCacheEntriesAsync(); // Don't await - it's fine to run in background

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

const request = await timeZoneResource.response;
const arrayBuffer = await request.arrayBuffer();
loadTimezoneData(arrayBuffer)

Module['FS_createPath']('/', 'usr', true, true);
Module['FS_createPath']('/usr/', 'share', true, true);
Module['FS_createPath']('/usr/share/', 'zoneinfo', true, true);
MONO.mono_wasm_load_data_archive(new Uint8Array(arrayBuffer), '/usr/share/zoneinfo/');

removeRunDependency(runDependencyId);
}

async function loadICUData(icuDataResource: LoadingResource) : Promise<void> {
const runDependencyId = `blazor:icudata`;
addRunDependency(runDependencyId);

const request = await icuDataResource.response;
const array = new Uint8Array(await request.arrayBuffer());

const offset = MONO.mono_wasm_load_bytes_into_heap(array);
if (!MONO.mono_wasm_load_icu_data(offset))
{
throw new Error("Error loading ICU asset.");
}
removeRunDependency(runDependencyId);
}

Expand Down
3 changes: 3 additions & 0 deletions src/Components/Web.JS/src/Platform/Mono/MonoTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ declare interface MONO {
loaded_files: string[];
mono_wasm_runtime_ready (): void;
mono_wasm_setenv (name: string, value: string): void;
mono_wasm_load_data_archive(data: Uint8Array, prefix: string): void;
mono_wasm_load_bytes_into_heap (data: Uint8Array): Pointer;
mono_wasm_load_icu_data(heapAddress: Pointer): boolean;
}

// Mono uses this global to hold low-level interop APIs
Expand Down
43 changes: 0 additions & 43 deletions src/Components/Web.JS/src/Platform/Mono/TimezoneDataFile.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ export interface WebAssemblyStartOptions {
// This type doesn't have to align with anything in BootConfig.
// Instead, this represents the public API through which certain aspects
// of boot resource loading can be customized.
export type WebAssemblyBootResourceType = 'assembly' | 'pdb' | 'dotnetjs' | 'dotnetwasm' | 'timezonedata';
export type WebAssemblyBootResourceType = 'assembly' | 'pdb' | 'dotnetjs' | 'dotnetwasm' | 'globalization';
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public async Task BuildMinimal_Works()
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazor.webassembly.js");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "dotnet.wasm");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "dotnet.timezones.blat");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "dotnet.wasm.gz");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", DotNetJsFileName);
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazorwasm-minimal.dll");
Expand Down Expand Up @@ -169,7 +170,7 @@ public async Task Build_InRelease_ProducesBootJsonDataWithExpectedContent()
Assert.Null(bootJsonData.resources.satelliteResources);
}

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

var runtime = bootJsonData.resources.runtime.Keys;
Assert.Contains("dotnet.wasm", runtime);
Assert.DoesNotContain("dotnet.timezones.dat", runtime);
Assert.DoesNotContain("dotnet.timezones.blat", runtime);

Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "dotnet.wasm");
Assert.FileDoesNotExist(result, buildOutputDirectory, "wwwroot", "_framework", "dotnet.timezones.blat");
}

[Fact]
public async Task Build_WithInvariantGlobalizationEnabled_DoesNotCopyGlobalizationData()
{
// Arrange
using var project = ProjectDirectory.Create("blazorwasm-minimal");
project.AddProjectFileContent(
@"
<PropertyGroup>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>");

var result = await MSBuildProcessManager.DotnetMSBuild(project);

Assert.BuildPassed(result);

var buildOutputDirectory = project.BuildOutputDirectory;

var bootJsonPath = Path.Combine(buildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json");
var bootJsonData = ReadBootJsonData(result, bootJsonPath);

var runtime = bootJsonData.resources.runtime.Keys;
Assert.Contains("dotnet.wasm", runtime);
Assert.Contains("dotnet.timezones.blat", runtime);
Assert.DoesNotContain("icudt.dat", runtime);

Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "wasm", "dotnet.wasm");
Assert.FileDoesNotExist(result, buildOutputDirectory, "wwwroot", "_framework", "wasm", "dotnet.timezones.dat");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "dotnet.wasm");
Assert.FileDoesNotExist(result, buildOutputDirectory, "wwwroot", "_framework", "icudt.dat");
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,34 @@ private static void AssertRIDPublishOuput(ProjectDirectory project, MSBuildResul
assetsManifestPath: "custom-service-worker-assets.js");
}

[Fact]
public async Task Publish_WithInvariantGlobalizationEnabled_DoesNotCopyGlobalizationData()
{
// Arrange
using var project = ProjectDirectory.Create("blazorwasm-minimal");
project.AddProjectFileContent(
@"
<PropertyGroup>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>");

var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish");

Assert.BuildPassed(result);

var publishOutputDirectory = project.PublishOutputDirectory;

var bootJsonPath = Path.Combine(publishOutputDirectory, "wwwroot", "_framework", "blazor.boot.json");
var bootJsonData = ReadBootJsonData(result, bootJsonPath);

var runtime = bootJsonData.resources.runtime.Keys;
Assert.Contains("dotnet.wasm", runtime);
Assert.DoesNotContain("icudt.dat", runtime);

Assert.FileExists(result, publishOutputDirectory, "wwwroot", "_framework", "dotnet.wasm");
Assert.FileDoesNotExist(result, publishOutputDirectory, "wwwroot", "_framework", "icudt.dat");
}

private static void AddWasmProjectContent(ProjectDirectory project, string content)
{
var path = Path.Combine(project.SolutionPath, "blazorwasm", "blazorwasm.csproj");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<configuration>
<system.webServer>
<staticContent>
<remove fileExtension=".blat" />
<remove fileExtension=".dat" />
<remove fileExtension=".dll" />
<remove fileExtension=".json" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ Copyright (c) .NET Foundation. All rights reserved.
<!-- Clear out temporary build artifacts that the runtime packages -->
<ReferenceCopyLocalPaths Remove="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.a'" />

<ReferenceCopyLocalPaths Remove="@(ReferenceCopyLocalPaths)"
Condition="'$(BlazorEnableTimeZoneSupport)' == 'false' AND '%(ReferenceCopyLocalPaths.FileName)%(ReferenceCopyLocalPaths.Extension)' == 'dotnet.timezones.blat'" />

<ReferenceCopyLocalPaths Remove="@(ReferenceCopyLocalPaths)"
Condition="'$(InvariantGlobalization)' == 'true' AND '%(ReferenceCopyLocalPaths.FileName)%(ReferenceCopyLocalPaths.Extension)' == 'icudt.dat'" />

<!--
ReferenceCopyLocalPaths includes satellite assemblies from referenced projects but are inexpicably missing
any metadata that might allow them to be differentiated. We'll explicitly add those
Expand Down Expand Up @@ -430,6 +436,12 @@ Copyright (c) .NET Foundation. All rights reserved.

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

<ResolvedFileToPublish Remove="@(ResolvedFileToPublish)"
Condition="'$(BlazorEnableTimeZoneSupport)' == 'false' AND '%(ResolvedFileToPublish.FileName)%(ResolvedFileToPublish.Extension)' == 'dotnet.timezones.blat'" />

<ResolvedFileToPublish Remove="@(ResolvedFileToPublish)"
Condition="'$(InvariantGlobalization)' == 'true' AND '%(ResolvedFileToPublish.FileName)%(ResolvedFileToPublish.Extension)' == 'icudt.dat'" />

<!-- Remove dotnet.js from publish output -->
<ResolvedFileToPublish Remove="@(ResolvedFileToPublish)" Condition="'%(ResolvedFileToPublish.RelativePath)' == 'dotnet.js'" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<RazorSdkCurrentVersionTargets>$(RepoRoot)src\Razor\Microsoft.NET.Sdk.Razor\src\build\netstandard2.0\Sdk.Razor.CurrentVersion.targets</RazorSdkCurrentVersionTargets>
<RazorSdkArtifactsDirectory>$(RepoRoot)artifacts\bin\Microsoft.NET.Sdk.Razor\</RazorSdkArtifactsDirectory>
<BlazorWebAssemblySdkArtifactsDirectory>$(RepoRoot)artifacts\bin\Microsoft.NET.Sdk.BlazorWebAssembly\</BlazorWebAssemblySdkArtifactsDirectory>
<_BlazorWebAssemblyTargetsFile>$(RepoRoot)src\Components\WebAssembly\Sdk\src\targets\Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets</_BlazorWebAssemblyTargetsFile>
<BlazorWebAssemblyJSPath>$(MSBuildThisFileDirectory)blazor.webassembly.js</BlazorWebAssemblyJSPath>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ private static StaticFileOptions CreateStaticFilesOptions(IFileProvider webRootF
AddMapping(contentTypeProvider, ".pdb", MediaTypeNames.Application.Octet);
AddMapping(contentTypeProvider, ".br", MediaTypeNames.Application.Octet);
AddMapping(contentTypeProvider, ".dat", MediaTypeNames.Application.Octet);
AddMapping(contentTypeProvider, ".blat", MediaTypeNames.Application.Octet);

options.ContentTypeProvider = contentTypeProvider;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Services;

Expand Down Expand Up @@ -55,7 +57,8 @@ public virtual async ValueTask LoadCurrentCultureResourcesAsync()

for (var i = 0; i < assemblies.Length; i++)
{
Assembly.Load((byte[])assemblies[i]);
using var stream = new MemoryStream((byte[])assemblies[i]);
AssemblyLoadContext.Default.LoadFromStream(stream);
}
}

Expand Down
8 changes: 4 additions & 4 deletions src/Components/test/E2ETest/Tests/BindTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -979,8 +979,8 @@ public void CanBindTextboxNullableDateTime_InvalidValue()
// Modify target to something invalid - the invalid change is reverted
// back to the last valid value
target.SendKeys(Keys.Control + "a"); // select all
target.SendKeys("05/06A");
Browser.Equal("05/06A", () => target.GetAttribute("value"));
target.SendKeys("05/06X");
Browser.Equal("05/06X", () => target.GetAttribute("value"));
target.SendKeys("\t");
Browser.Equal(expected, () => DateTime.Parse(target.GetAttribute("value")));
Assert.Equal(expected, DateTime.Parse(boundValue.Text));
Expand Down Expand Up @@ -1017,8 +1017,8 @@ public void CanBindTextboxDateTimeOffset_InvalidValue()
// Modify target to something invalid - the invalid change is reverted
// back to the last valid value
target.SendKeys(Keys.Control + "a"); // select all
target.SendKeys("05/06A");
Browser.Equal("05/06A", () => target.GetAttribute("value"));
target.SendKeys("05/06X");
Browser.Equal("05/06X", () => target.GetAttribute("value"));
target.SendKeys("\t");
Browser.Equal(expected.DateTime, () => DateTimeOffset.Parse(target.GetAttribute("value")).DateTime);
Assert.Equal(expected.DateTime, DateTimeOffset.Parse(boundValue.Text).DateTime);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public WebAssemblyLocalizationTest(
{
}

[Theory(Skip = "https://github.com/dotnet/runtime/issues/38124")]
[Theory]
[InlineData("en-US", "Hello!")]
[InlineData("fr-FR", "Bonjour!")]
public void CanSetCultureAndReadLocalizedResources(string culture, string message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<configuration>
<system.webServer>
<staticContent>
<remove fileExtension=".blat" />
<remove fileExtension=".dat" />
<remove fileExtension=".dll" />
<remove fileExtension=".json" />
Expand Down

0 comments on commit 2ad1b6d

Please sign in to comment.