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 https://github.com/dotnet/aspnetcore/issues/24174 Fixes https://github.com/dotnet/aspnetcore/issues/22975 Fixes https://github.com/dotnet/aspnetcore/issues/23260
This commit is contained in:
parent
524c8cda63
commit
4e57d1b041
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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
|
||||
|
||||
|
|
@ -239,14 +239,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
|
||||
|
|
@ -274,6 +286,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.
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
import { readInt32LE } from "../../BinaryDecoder";
|
||||
import { decodeUtf8 } from "../../Utf8Decoder";
|
||||
|
||||
export function loadTimezoneData(arrayBuffer: ArrayBuffer) {
|
||||
let remainingData = new Uint8Array(arrayBuffer);
|
||||
|
||||
// The timezone file is generated by https://github.com/dotnet/blazor/tree/master/src/TimeZoneData.
|
||||
// The file format of the TZ file look like so
|
||||
//
|
||||
// [4 - byte length of manifest]
|
||||
// [json manifest]
|
||||
// [data bytes]
|
||||
//
|
||||
// The json manifest is an array that looks like so:
|
||||
//
|
||||
// [...["America/Fort_Nelson",2249],["America/Glace_Bay",2206]..]
|
||||
//
|
||||
// where the first token in each array is the relative path of the file on disk, and the second is the
|
||||
// length of the file. The starting offset of a file can be calculated using the lengths of all files
|
||||
// that appear prior to it.
|
||||
const manifestSize = readInt32LE(remainingData, 0);
|
||||
remainingData = remainingData.slice(4);
|
||||
const manifestContent = decodeUtf8(remainingData.slice(0, manifestSize));
|
||||
const manifest = JSON.parse(manifestContent) as ManifestEntry[];
|
||||
remainingData = remainingData.slice(manifestSize);
|
||||
|
||||
// Create the folder structure
|
||||
// /zoneinfo
|
||||
// /zoneinfo/Africa
|
||||
// /zoneinfo/Asia
|
||||
// ..
|
||||
Module['FS_createPath']('/', 'zoneinfo', true, true);
|
||||
new Set(manifest.map(m => m[0].split('/')![0])).forEach(folder =>
|
||||
Module['FS_createPath']('/zoneinfo', folder, true, true));
|
||||
|
||||
for (const [name, length] of manifest) {
|
||||
const bytes = remainingData.slice(0, length);
|
||||
Module['FS_createDataFile'](`/zoneinfo/${name}`, null, bytes, true, true, true);
|
||||
remainingData = remainingData.slice(length);
|
||||
}
|
||||
}
|
||||
|
||||
type ManifestEntry = [string, number];
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
|
|||
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");
|
||||
|
|
@ -169,7 +170,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
|
|||
Assert.Null(bootJsonData.resources.satelliteResources);
|
||||
}
|
||||
|
||||
[Fact(Skip = "https://github.com/dotnet/aspnetcore/issues/22975")]
|
||||
[Fact]
|
||||
public async Task Build_WithBlazorEnableTimeZoneSupportDisabled_DoesNotCopyTimeZoneInfo()
|
||||
{
|
||||
// Arrange
|
||||
|
|
@ -192,10 +193,10 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
|
|||
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<configuration>
|
||||
<system.webServer>
|
||||
<staticContent>
|
||||
<remove fileExtension=".blat" />
|
||||
<remove fileExtension=".dat" />
|
||||
<remove fileExtension=".dll" />
|
||||
<remove fileExtension=".json" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ namespace Microsoft.AspNetCore.Builder
|
|||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
@ -55,7 +57,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -979,8 +979,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
// 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));
|
||||
|
|
@ -1017,8 +1017,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
// 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);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
{
|
||||
}
|
||||
|
||||
[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)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<configuration>
|
||||
<system.webServer>
|
||||
<staticContent>
|
||||
<remove fileExtension=".blat" />
|
||||
<remove fileExtension=".dat" />
|
||||
<remove fileExtension=".dll" />
|
||||
<remove fileExtension=".json" />
|
||||
|
|
|
|||
Loading…
Reference in New Issue