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

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
Pranav Krishnamoorthy committed Jul 28, 2020
1 parent 864a292 commit 3e7a106
Show file tree
Hide file tree
Showing 15 changed files with 75 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,12 +3,12 @@ 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';
import { initializeProfiling } from '../Profiling';

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

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

if (icuDataResource) {
loadICUData(icuDataResource);
}

// 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 @@ -363,7 +379,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);
const load_runtime = cwrap('mono_wasm_load_runtime', null, ['string', 'number']);
// -1 enables debugging with logging disabled. 0 disables debugging entirely.
load_runtime(appBinDirName, hasDebuggingEnabled() ? -1 : 0);
Expand Down Expand Up @@ -461,8 +481,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 @@ -14,4 +14,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,10 @@ 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", "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", "dotnet.timezones.blat");
}

[Fact]
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,8 @@ 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)' == 'dotnet.timezones'" />

<!--
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
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 @@ -974,8 +974,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 @@ -1012,8 +1012,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
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ Copyright (c) .NET Foundation. All rights reserved.
<!-- Clear out temporary build artifacts that the runtime packages -->
<ReferenceCopyLocalPaths Remove="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.a'" />

<!-- Remove the timezone file if time-zone support is disabled -->
<ReferenceCopyLocalPaths Remove="@(ReferenceCopyLocalPaths)" Condition="'$(BlazorEnableTimeZoneSupport)' == 'false' AND '%(ReferenceCopyLocalPaths.FileName)' == 'dotnet.timezones'" />

<!--
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

0 comments on commit 3e7a106

Please sign in to comment.