-
Notifications
You must be signed in to change notification settings - Fork 10.3k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,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 | ||
|
@@ -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. | ||
|
@@ -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 { } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
@@ -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); | ||
} | ||
|
||
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"); | ||
|
@@ -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 | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah -- OK. So the |
||
|
||
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] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 @@ 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]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
AssemblyLoadContext.Default.LoadFromStream(stream); | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.