Skip to content

Merged PR 9371: Revive support for globalization and localization in Blazor WASM #24773

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a typo or a legit file format?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've heard Larry talk about this so I guess it's the name of the archive file format. Apparently it's like a tar file.

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 { }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it OK to ignore errors thrown here?

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah -- OK. So the blat file extension is intentional...


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]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

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