Use ICU sharding (#25521)

This commit is contained in:
Pranav K 2020-09-10 10:17:02 -07:00 committed by GitHub
parent 70725b554c
commit 2228c98b88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 781 additions and 60 deletions

View File

@ -1507,6 +1507,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.App.Un
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Localization", "Localization", "{3D34C81F-2CB5-459E-87E9-0CC04757A2A0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GlobalizationWasmApp", "src\Components\test\testassets\GlobalizationWasmApp\GlobalizationWasmApp.csproj", "{04CFE286-6D32-41EF-8887-4B5F8086A365}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Localization.Abstractions", "src\Localization\Abstractions\src\Microsoft.Extensions.Localization.Abstractions.csproj", "{FEF97646-9BC9-4D1B-A939-784D915C18A4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Localization", "src\Localization\Localization\src\Microsoft.Extensions.Localization.csproj", "{839CE175-E0D9-43B9-9FA8-F32C47E7F56B}"
@ -7193,6 +7195,18 @@ Global
{BAD47859-95DF-4C8F-9AF7-C48B68F478A1}.Release|x64.Build.0 = Release|Any CPU
{BAD47859-95DF-4C8F-9AF7-C48B68F478A1}.Release|x86.ActiveCfg = Release|Any CPU
{BAD47859-95DF-4C8F-9AF7-C48B68F478A1}.Release|x86.Build.0 = Release|Any CPU
{04CFE286-6D32-41EF-8887-4B5F8086A365}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{04CFE286-6D32-41EF-8887-4B5F8086A365}.Debug|Any CPU.Build.0 = Debug|Any CPU
{04CFE286-6D32-41EF-8887-4B5F8086A365}.Debug|x64.ActiveCfg = Debug|Any CPU
{04CFE286-6D32-41EF-8887-4B5F8086A365}.Debug|x64.Build.0 = Debug|Any CPU
{04CFE286-6D32-41EF-8887-4B5F8086A365}.Debug|x86.ActiveCfg = Debug|Any CPU
{04CFE286-6D32-41EF-8887-4B5F8086A365}.Debug|x86.Build.0 = Debug|Any CPU
{04CFE286-6D32-41EF-8887-4B5F8086A365}.Release|Any CPU.ActiveCfg = Release|Any CPU
{04CFE286-6D32-41EF-8887-4B5F8086A365}.Release|Any CPU.Build.0 = Release|Any CPU
{04CFE286-6D32-41EF-8887-4B5F8086A365}.Release|x64.ActiveCfg = Release|Any CPU
{04CFE286-6D32-41EF-8887-4B5F8086A365}.Release|x64.Build.0 = Release|Any CPU
{04CFE286-6D32-41EF-8887-4B5F8086A365}.Release|x86.ActiveCfg = Release|Any CPU
{04CFE286-6D32-41EF-8887-4B5F8086A365}.Release|x86.Build.0 = Release|Any CPU
{010A9638-F20E-4FE6-A186-85732BFC9CB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{010A9638-F20E-4FE6-A186-85732BFC9CB0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{010A9638-F20E-4FE6-A186-85732BFC9CB0}.Debug|x64.ActiveCfg = Debug|Any CPU
@ -7205,7 +7219,6 @@ Global
{010A9638-F20E-4FE6-A186-85732BFC9CB0}.Release|x64.Build.0 = Release|Any CPU
{010A9638-F20E-4FE6-A186-85732BFC9CB0}.Release|x86.ActiveCfg = Release|Any CPU
{010A9638-F20E-4FE6-A186-85732BFC9CB0}.Release|x86.Build.0 = Release|Any CPU
=======
{FEF97646-9BC9-4D1B-A939-784D915C18A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FEF97646-9BC9-4D1B-A939-784D915C18A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FEF97646-9BC9-4D1B-A939-784D915C18A4}.Debug|x64.ActiveCfg = Debug|Any CPU
@ -8030,6 +8043,7 @@ Global
{37329855-01B8-4B03-9765-1A941B06E43C} = {8C15FD04-7F90-43FC-B488-023432FE3CE1}
{D3246226-BC1A-47F1-8E3E-C3380A8F13FB} = {8C15FD04-7F90-43FC-B488-023432FE3CE1}
{B06ADD57-E855-4D8C-85DC-B323509AE540} = {898F7E0B-1671-42CB-9DFB-689AFF212ED3}
{04CFE286-6D32-41EF-8887-4B5F8086A365} = {6126DCE4-9692-4EE2-B240-C65743572995}
{BAD47859-95DF-4C8F-9AF7-C48B68F478A1} = {A4C26078-B6D8-4FD8-87A6-7C15A3482038}
{010A9638-F20E-4FE6-A186-85732BFC9CB0} = {A4C26078-B6D8-4FD8-87A6-7C15A3482038}
{3D34C81F-2CB5-459E-87E9-0CC04757A2A0} = {017429CC-C5FB-48B4-9C46-034E29EE2F06}

View File

@ -98,6 +98,7 @@
"src\\Components\\Ignitor\\src\\Ignitor.csproj",
"src\\Components\\Ignitor\\test\\Ignitor.Test.csproj",
"src\\Components\\test\\testassets\\ComponentsApp.Server\\ComponentsApp.Server.csproj",
"src\\Components\\test\\testassets\\GlobalizationWasmApp\\GlobalizationWasmApp.csproj",
"src\\Components\\benchmarkapps\\Wasm.Performance\\Driver\\Wasm.Performance.Driver.csproj",
"src\\Components\\benchmarkapps\\Wasm.Performance\\TestApp\\Wasm.Performance.TestApp.csproj",
"src\\Components\\Web.Extensions\\src\\Microsoft.AspNetCore.Components.Web.Extensions.csproj",

View File

@ -44,6 +44,7 @@
"src\\Components\\test\\E2ETest\\Microsoft.AspNetCore.Components.E2ETests.csproj",
"src\\Components\\test\\testassets\\BasicTestApp\\BasicTestApp.csproj",
"src\\Components\\test\\testassets\\ComponentsApp.Server\\ComponentsApp.Server.csproj",
"src\\Components\\test\\testassets\\GlobalizationWasmApp\\GlobalizationWasmApp.csproj",
"src\\Components\\test\\testassets\\TestContentPackage\\TestContentPackage.csproj",
"src\\Components\\test\\testassets\\TestServer\\Components.TestServer.csproj"
]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -26,6 +26,7 @@ export interface BootJsonData {
readonly linkerEnabled: boolean;
readonly cacheBootResources: boolean;
readonly config: string[];
readonly icuDataMode: ICUDataMode;
}
export interface ResourceGroups {
@ -37,3 +38,9 @@ export interface ResourceGroups {
}
export type ResourceList = { [name: string]: string };
export enum ICUDataMode {
Sharded,
All,
Invariant
}

View File

@ -4,10 +4,10 @@ import { showErrorNotification } from '../../BootErrors';
import { WebAssemblyResourceLoader, LoadingResource } from '../WebAssemblyResourceLoader';
import { Platform, System_Array, Pointer, System_Object, System_String, HeapLock } from '../Platform';
import { WebAssemblyBootResourceType } from '../WebAssemblyStartOptions';
import { BootJsonData, ICUDataMode } from '../BootConfig';
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
@ -138,13 +138,13 @@ export const monoPlatform: Platform = {
return ((baseAddress as any as number) + (fieldOffset || 0)) as any as T;
},
beginHeapLock: function() {
beginHeapLock: function () {
assertHeapIsNotLocked();
currentHeapLock = new MonoHeapLock();
return currentHeapLock;
},
invokeWhenHeapUnlocked: function(callback) {
invokeWhenHeapUnlocked: function (callback) {
// This is somewhat like a sync context. If we're not locked, just pass through the call directly.
if (!currentHeapLock) {
callback();
@ -183,7 +183,7 @@ function addScriptTagsToDocument(resourceLoader: WebAssemblyResourceLoader) {
const resourceType: WebAssemblyBootResourceType = 'dotnetjs';
const customSrc = resourceLoader.startOptions.loadBootResource(
resourceType, dotnetJsResourceName, scriptElem.src, dotnetJsContentHash);
if (typeof(customSrc) === 'string') {
if (typeof (customSrc) === 'string') {
scriptElem.src = customSrc;
} else if (customSrc) {
// Since we must load this via a <script> tag, it's only valid to supply a URI (and not a Request, say)
@ -213,7 +213,7 @@ function addGlobalModuleScriptTagsToDocument(callback: () => void) {
function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoader, onReady: () => void, onError: (reason?: any) => void) {
const resources = resourceLoader.bootConfig.resources;
const module = (window['Module'] || { }) as typeof Module;
const module = (window['Module'] || {}) as typeof Module;
const suppressMessages = ['DEBUGGING ENABLED'];
module.print = line => (suppressMessages.indexOf(line) < 0 && console.log(line));
@ -250,7 +250,9 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
}
let icuDataResource: LoadingResource | undefined;
if (resourceLoader.bootConfig.resources.runtime.hasOwnProperty(icuDataResourceName)) {
if (resourceLoader.bootConfig.icuDataMode != ICUDataMode.Invariant) {
const applicationCulture = resourceLoader.startOptions.applicationCulture || (navigator.languages && navigator.languages[0]);
const icuDataResourceName = getICUResourceName(resourceLoader.bootConfig, applicationCulture);
icuDataResource = resourceLoader.loadResource(
icuDataResourceName,
`_framework/${icuDataResourceName}`,
@ -287,7 +289,7 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
loadICUData(icuDataResource);
} else {
// Use invariant culture if the app does not carry icu data.
MONO.mono_wasm_setenv("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "1");
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
@ -302,16 +304,22 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
// Wire-up callbacks for satellite assemblies. Blazor will call these as part of the application
// startup sequence to load satellite assemblies for the application's culture.
window['Blazor']._internal.getSatelliteAssemblies = (culturesToLoadDotNetArray: System_Array<System_String>) : System_Object => {
window['Blazor']._internal.getSatelliteAssemblies = (culturesToLoadDotNetArray: System_Array<System_String>): System_Object => {
const culturesToLoad = BINDING.mono_array_to_js_array<System_String, string>(culturesToLoadDotNetArray);
const satelliteResources = resourceLoader.bootConfig.resources.satelliteResources;
const applicationCulture = resourceLoader.startOptions.applicationCulture || (navigator.languages && navigator.languages[0]);
if (resourceLoader.bootConfig.icuDataMode == ICUDataMode.Sharded && culturesToLoad && culturesToLoad[0] !== applicationCulture) {
// We load an initial icu file based on the browser's locale. However if the application's culture requires a different set, flag this as an error.
throw new Error('To change culture dynamically during startup, set <BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData> in the application\'s project file.');
}
if (satelliteResources) {
const resourcePromises = Promise.all(culturesToLoad
.filter(culture => satelliteResources.hasOwnProperty(culture))
.map(culture => resourceLoader.loadResources(satelliteResources[culture], fileName => `_framework/${fileName}`, 'assembly'))
.reduce((previous, next) => previous.concat(next), new Array<LoadingResource>())
.map(async resource => (await resource.response).arrayBuffer()));
.filter(culture => satelliteResources.hasOwnProperty(culture))
.map(culture => resourceLoader.loadResources(satelliteResources[culture], fileName => `_framework/${fileName}`, 'assembly'))
.reduce((previous, next) => previous.concat(next), new Array<LoadingResource>())
.map(async resource => (await resource.response).arrayBuffer()));
return BINDING.js_to_mono_obj(
resourcePromises.then(resourcesToLoad => {
@ -322,16 +330,16 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
BINDING.mono_obj_array_set(array, i, BINDING.js_typed_array_to_array(new Uint8Array(resourcesToLoad[i])));
}
return array;
};
}
};
}
return resourcesToLoad.length;
}));
return resourcesToLoad.length;
}));
}
return BINDING.js_to_mono_obj(Promise.resolve(0));
}
window['Blazor']._internal.getLazyAssemblies = (assembliesToLoadDotNetArray: System_Array<System_String>) : System_Object => {
window['Blazor']._internal.getLazyAssemblies = (assembliesToLoadDotNetArray: System_Array<System_String>): System_Object => {
const assembliesToLoad = BINDING.mono_array_to_js_array<System_String, string>(assembliesToLoadDotNetArray);
const lazyAssemblies = resourceLoader.bootConfig.resources.lazyAssembly;
@ -347,24 +355,24 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
}
const resourcePromises = Promise.all(assembliesMarkedAsLazy
.map(assembly => resourceLoader.loadResource(assembly, `_framework/${assembly}`, lazyAssemblies[assembly], 'assembly'))
.map(async resource => (await resource.response).arrayBuffer()));
.map(assembly => resourceLoader.loadResource(assembly, `_framework/${assembly}`, lazyAssemblies[assembly], 'assembly'))
.map(async resource => (await resource.response).arrayBuffer()));
return BINDING.js_to_mono_obj(
resourcePromises.then(resourcesToLoad => {
if (resourcesToLoad.length) {
window['Blazor']._internal.readLazyAssemblies = () => {
const array = BINDING.mono_obj_array_new(resourcesToLoad.length);
for (var i = 0; i < resourcesToLoad.length; i++) {
BINDING.mono_obj_array_set(array, i, BINDING.js_typed_array_to_array(new Uint8Array(resourcesToLoad[i])));
}
return array;
return BINDING.js_to_mono_obj(
resourcePromises.then(resourcesToLoad => {
if (resourcesToLoad.length) {
window['Blazor']._internal.readLazyAssemblies = () => {
const array = BINDING.mono_obj_array_new(resourcesToLoad.length);
for (var i = 0; i < resourcesToLoad.length; i++) {
BINDING.mono_obj_array_set(array, i, BINDING.js_typed_array_to_array(new Uint8Array(resourcesToLoad[i])));
}
return array;
};
}
return resourcesToLoad.length;
}));
}
}
});
module.postRun.push(() => {
@ -385,7 +393,7 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
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);
MONO.mono_wasm_runtime_ready ();
MONO.mono_wasm_runtime_ready();
attachInteropInvoker();
onReady();
});
@ -408,8 +416,8 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
mono_wasm_add_assembly(loadAsName, heapAddress, data.length);
MONO.loaded_files.push(toAbsoluteUrl(dependency.url));
} catch (errorInfo) {
onError(errorInfo);
return;
onError(errorInfo);
return;
}
removeRunDependency(runDependencyId);
@ -433,7 +441,7 @@ function bindStaticMethod(assembly: string, typeName: string, method: string) {
}
function attachInteropInvoker(): void {
const dotNetDispatcherInvokeMethodHandle = bindStaticMethod('Microsoft.AspNetCore.Components.WebAssembly', 'Microsoft.AspNetCore.Components.WebAssembly.Services.DefaultWebAssemblyJSRuntime', 'InvokeDotNet');
const dotNetDispatcherInvokeMethodHandle = bindStaticMethod('Microsoft.AspNetCore.Components.WebAssembly', 'Microsoft.AspNetCore.Components.WebAssembly.Services.DefaultWebAssemblyJSRuntime', 'InvokeDotNet');
const dotNetDispatcherBeginInvokeMethodHandle = bindStaticMethod('Microsoft.AspNetCore.Components.WebAssembly', 'Microsoft.AspNetCore.Components.WebAssembly.Services.DefaultWebAssemblyJSRuntime', 'BeginInvokeDotNet');
const dotNetDispatcherEndInvokeJSMethodHandle = bindStaticMethod('Microsoft.AspNetCore.Components.WebAssembly', 'Microsoft.AspNetCore.Components.WebAssembly.Services.DefaultWebAssemblyJSRuntime', 'EndInvokeJS');
@ -449,12 +457,12 @@ function attachInteropInvoker(): void {
? dotNetObjectId.toString()
: assemblyName;
dotNetDispatcherBeginInvokeMethodHandle(
callId ? callId.toString() : null,
assemblyNameOrDotNetObjectId,
methodIdentifier,
argsJson,
);
dotNetDispatcherBeginInvokeMethodHandle(
callId ? callId.toString() : null,
assemblyNameOrDotNetObjectId,
methodIdentifier,
argsJson,
);
},
endInvokeJSFromDotNet: (asyncHandle, succeeded, serializedArgs): void => {
dotNetDispatcherEndInvokeJSMethodHandle(
@ -473,7 +481,7 @@ function attachInteropInvoker(): void {
});
}
async function loadTimezone(timeZoneResource: LoadingResource) : Promise<void> {
async function loadTimezone(timeZoneResource: LoadingResource): Promise<void> {
const runDependencyId = `blazor:timezonedata`;
addRunDependency(runDependencyId);
@ -488,7 +496,23 @@ async function loadTimezone(timeZoneResource: LoadingResource) : Promise<void> {
removeRunDependency(runDependencyId);
}
async function loadICUData(icuDataResource: LoadingResource) : Promise<void> {
function getICUResourceName(bootConfig: BootJsonData, culture: string | undefined): string {
const combinedICUResourceName = 'icudt.dat';
if (!culture || bootConfig.icuDataMode == ICUDataMode.All) {
return combinedICUResourceName;
}
const prefix = culture.split('-')[0];
if (['en', 'fr', 'it', 'de', 'es'].includes(prefix)) {
return 'icudt_EFIGS.dat';
} else if (['zh', 'ko', 'ja'].includes(prefix)) {
return 'icudt_CJK.dat';
} else {
return 'icudt_no_CJK.dat';
}
}
async function loadICUData(icuDataResource: LoadingResource): Promise<void> {
const runDependencyId = `blazor:icudata`;
addRunDependency(runDependencyId);
@ -496,8 +520,7 @@ async function loadICUData(icuDataResource: LoadingResource) : Promise<void> {
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))
{
if (!MONO.mono_wasm_load_icu_data(offset)) {
throw new Error("Error loading ICU asset.");
}
removeRunDependency(runDependencyId);

View File

@ -14,6 +14,11 @@ export interface WebAssemblyStartOptions {
* Override built-in environment setting on start.
*/
environment?: string;
/**
* Gets the application culture. This is a name specified in the BCP 47 format. See https://tools.ietf.org/html/bcp47
*/
applicationCulture?: string;
}
// This type doesn't have to align with anything in BootConfig.

View File

@ -219,13 +219,52 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
var bootJsonPath = Path.Combine(buildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json");
var bootJsonData = ReadBootJsonData(result, bootJsonPath);
Assert.Equal(ICUDataMode.Invariant, bootJsonData.icuDataMode);
var runtime = bootJsonData.resources.runtime.Keys;
Assert.Contains("dotnet.wasm", runtime);
Assert.Contains("dotnet.timezones.blat", runtime);
Assert.DoesNotContain("icudt.dat", runtime);
Assert.DoesNotContain("icudt_EFIGS.dat", runtime);
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "dotnet.wasm");
Assert.FileDoesNotExist(result, buildOutputDirectory, "wwwroot", "_framework", "icudt.dat");
Assert.FileDoesNotExist(result, buildOutputDirectory, "wwwroot", "_framework", "icudt_CJK.dat");
Assert.FileDoesNotExist(result, buildOutputDirectory, "wwwroot", "_framework", "icudt_EFIGS.dat");
Assert.FileDoesNotExist(result, buildOutputDirectory, "wwwroot", "_framework", "icudt_no_CJK.dat");
}
[Fact]
public async Task Build_WithBlazorWebAssemblyLoadAllGlobalizationData_SetsICUDataMode()
{
// Arrange
using var project = ProjectDirectory.Create("blazorwasm-minimal");
project.AddProjectFileContent(
@"
<PropertyGroup>
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
</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);
Assert.Equal(ICUDataMode.All, bootJsonData.icuDataMode);
var runtime = bootJsonData.resources.runtime.Keys;
Assert.Contains("dotnet.wasm", runtime);
Assert.Contains("dotnet.timezones.blat", runtime);
Assert.Contains("icudt.dat", runtime);
Assert.Contains("icudt_EFIGS.dat", runtime);
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "dotnet.wasm");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "icudt.dat");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "icudt_CJK.dat");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "icudt_EFIGS.dat");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "icudt_no_CJK.dat");
}
[Fact]

View File

@ -845,12 +845,17 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
var bootJsonPath = Path.Combine(publishOutputDirectory, "wwwroot", "_framework", "blazor.boot.json");
var bootJsonData = ReadBootJsonData(result, bootJsonPath);
Assert.Equal(ICUDataMode.Invariant, bootJsonData.icuDataMode);
var runtime = bootJsonData.resources.runtime.Keys;
Assert.Contains("dotnet.wasm", runtime);
Assert.DoesNotContain("icudt.dat", runtime);
Assert.DoesNotContain("icudt_EFIGS.dat", runtime);
Assert.FileExists(result, publishOutputDirectory, "wwwroot", "_framework", "dotnet.wasm");
Assert.FileDoesNotExist(result, publishOutputDirectory, "wwwroot", "_framework", "icudt.dat");
Assert.FileDoesNotExist(result, publishOutputDirectory, "wwwroot", "_framework", "icudt_CJK.dat");
Assert.FileDoesNotExist(result, publishOutputDirectory, "wwwroot", "_framework", "icudt_EFIGS.dat");
Assert.FileDoesNotExist(result, publishOutputDirectory, "wwwroot", "_framework", "icudt_no_CJK.dat");
}
private static void AddWasmProjectContent(ProjectDirectory project, string content)

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
@ -49,6 +49,11 @@ namespace Microsoft.NET.Sdk.BlazorWebAssembly
/// Config files for the application
/// </summary>
public List<string> config { get; set; }
/// <summary>
/// Gets or sets the <see cref="ICUDataMode"/> that determines how icu files are loaded.
/// </summary>
public ICUDataMode icuDataMode { get; set; }
}
public class ResourcesData
@ -81,5 +86,23 @@ namespace Microsoft.NET.Sdk.BlazorWebAssembly
[DataMember(EmitDefaultValue = false)]
public ResourceHashesByNameDictionary lazyAssembly { get; set; }
}
public enum ICUDataMode
{
/// <summary>
/// Load optimized icu data file based on the user's locale
/// </summary>
Sharded,
/// <summary>
/// Use the combined icudt.dat file
/// </summary>
All,
/// <summary>
/// Do not load any icu data files.
/// </summary>
Invariant,
}
#pragma warning restore IDE1006 // Naming Styles
}

View File

@ -31,6 +31,10 @@ namespace Microsoft.NET.Sdk.BlazorWebAssembly
[Required]
public bool CacheBootResources { get; set; }
public bool LoadAllICUData { get; set; }
public string InvariantGlobalization { get; set; }
public ITaskItem[] ConfigurationFiles { get; set; }
[Required]
@ -58,6 +62,17 @@ namespace Microsoft.NET.Sdk.BlazorWebAssembly
// Internal for tests
public void WriteBootJson(Stream output, string entryAssemblyName)
{
var icuDataMode = ICUDataMode.Sharded;
if (string.Equals(InvariantGlobalization, "true", StringComparison.OrdinalIgnoreCase))
{
icuDataMode = ICUDataMode.Invariant;
}
else if (LoadAllICUData)
{
icuDataMode = ICUDataMode.All;
}
var result = new BootJsonData
{
entryAssembly = entryAssemblyName,
@ -66,6 +81,7 @@ namespace Microsoft.NET.Sdk.BlazorWebAssembly
linkerEnabled = LinkerEnabled,
resources = new ResourcesData(),
config = new List<string>(),
icuDataMode = icuDataMode,
};
// Build a two-level dictionary of the form:

View File

@ -126,6 +126,9 @@ Copyright (c) .NET Foundation. All rights reserved.
<_BlazorSatelliteAssemblyCacheFile>$(IntermediateOutputPath)blazor.satelliteasm.props</_BlazorSatelliteAssemblyCacheFile>
<!-- Workaround for https://github.com/dotnet/sdk/issues/12114-->
<PublishDir Condition="'$(AppendRuntimeIdentifierToOutputPath)' != 'true' AND '$(PublishDir)' == '$(OutputPath)$(RuntimeIdentifier)\$(PublishDirName)\'">$(OutputPath)$(PublishDirName)\</PublishDir>
<_BlazorWebAssemblyLoadAllGlobalizationData>$(BlazorWebAssemblyLoadAllGlobalizationData)</_BlazorWebAssemblyLoadAllGlobalizationData>
<_BlazorWebAssemblyLoadAllGlobalizationData Condition="'$(_BlazorWebAssemblyLoadAllGlobalizationData)' == ''">false</_BlazorWebAssemblyLoadAllGlobalizationData>
</PropertyGroup>
<ItemGroup>
@ -141,7 +144,7 @@ Copyright (c) .NET Foundation. All rights reserved.
Condition="'$(BlazorEnableTimeZoneSupport)' == 'false' AND '%(ReferenceCopyLocalPaths.FileName)%(ReferenceCopyLocalPaths.Extension)' == 'dotnet.timezones.blat'" />
<ReferenceCopyLocalPaths Remove="@(ReferenceCopyLocalPaths)"
Condition="'$(InvariantGlobalization)' == 'true' AND '%(ReferenceCopyLocalPaths.FileName)%(ReferenceCopyLocalPaths.Extension)' == 'icudt.dat'" />
Condition="'$(InvariantGlobalization)' == 'true' AND '%(ReferenceCopyLocalPaths.Extension)' == '.dat' AND $([System.String]::Copy('%(ReferenceCopyLocalPaths.FileName)').StartsWith('icudt'))" />
<!--
ReferenceCopyLocalPaths includes satellite assemblies from referenced projects but are inexpicably missing
@ -281,6 +284,7 @@ Copyright (c) .NET Foundation. All rights reserved.
<Target Name="_BlazorWasmPrepareForRun" DependsOnTargets="_ProcessBlazorWasmOutputs" BeforeTargets="_RazorPrepareForRun" AfterTargets="GetCurrentProjectStaticWebAssets">
<PropertyGroup>
<_BlazorBuildBootJsonPath>$(IntermediateOutputPath)blazor.boot.json</_BlazorBuildBootJsonPath>
<_BlazorWebAssemblyLoadAllGlobalizationData Condition="'$(BlazorWebAssemblyLoadAllGlobalizationData)' == ''">false</_BlazorWebAssemblyLoadAllGlobalizationData>
</PropertyGroup>
<GenerateBlazorWebAssemblyBootJson
@ -291,7 +295,9 @@ Copyright (c) .NET Foundation. All rights reserved.
CacheBootResources="$(BlazorCacheBootResources)"
OutputPath="$(_BlazorBuildBootJsonPath)"
ConfigurationFiles="@(_BlazorConfigFile)"
LazyLoadedAssemblies="@(BlazorWebAssemblyLazyLoad)" />
LazyLoadedAssemblies="@(BlazorWebAssemblyLazyLoad)"
InvariantGlobalization="$(InvariantGlobalization)"
LoadAllICUData="$(_BlazorWebAssemblyLoadAllGlobalizationData)" />
<ItemGroup>
<FileWrites Include="$(OutDir)$(_BlazorOutputPath)blazor.boot.json" />
@ -461,7 +467,7 @@ Copyright (c) .NET Foundation. All rights reserved.
Condition="'$(BlazorEnableTimeZoneSupport)' == 'false' AND '%(ResolvedFileToPublish.FileName)%(ResolvedFileToPublish.Extension)' == 'dotnet.timezones.blat'" />
<ResolvedFileToPublish Remove="@(ResolvedFileToPublish)"
Condition="'$(InvariantGlobalization)' == 'true' AND '%(ResolvedFileToPublish.FileName)%(ResolvedFileToPublish.Extension)' == 'icudt.dat'" />
Condition="'$(InvariantGlobalization)' == 'true' AND '%(ResolvedFileToPublish.Extension)' == '.dat' AND $([System.String]::Copy('%(ResolvedFileToPublish.FileName)').StartsWith('icudt'))" />
<!-- Remove dotnet.js from publish output -->
<ResolvedFileToPublish Remove="@(ResolvedFileToPublish)" Condition="'%(ResolvedFileToPublish.RelativePath)' == 'dotnet.js'" />
@ -502,7 +508,9 @@ Copyright (c) .NET Foundation. All rights reserved.
CacheBootResources="$(BlazorCacheBootResources)"
OutputPath="$(IntermediateOutputPath)blazor.publish.boot.json"
ConfigurationFiles="@(_BlazorConfigFile)"
LazyLoadedAssemblies="@(BlazorWebAssemblyLazyLoad)" />
LazyLoadedAssemblies="@(BlazorWebAssemblyLazyLoad)"
InvariantGlobalization="$(InvariantGlobalization)"
LoadAllICUData="$(_BlazorWebAssemblyLoadAllGlobalizationData)" />
<ItemGroup>
<ResolvedFileToPublish

View File

@ -252,6 +252,7 @@ namespace Wasm.Performance.Driver
static void PrettyPrint(BenchmarkResult benchmarkResult)
{
Console.WriteLine();
Console.WriteLine($"Download size: {(benchmarkResult.DownloadSize / 1024)}kb.");
Console.WriteLine("| Name | Description | Duration | NumExecutions | ");
Console.WriteLine("--------------------------");
foreach (var result in benchmarkResult.ScenarioResults)

View File

@ -45,6 +45,7 @@
<ProjectReference Include="..\..\WebAssembly\testassets\HostedInAspNet.Server\HostedInAspNet.Server.csproj" />
<ProjectReference Include="..\..\WebAssembly\testassets\StandaloneApp\StandaloneApp.csproj" />
<ProjectReference Include="..\testassets\BasicTestApp\BasicTestApp.csproj" />
<ProjectReference Include="..\testassets\GlobalizationWasmApp\GlobalizationWasmApp.csproj" />
<ProjectReference Include="..\testassets\TestServer\Components.TestServer.csproj" />
<ProjectReference Include="..\..\WebAssembly\testassets\Wasm.Authentication.Server\Wasm.Authentication.Server.csproj" />
</ItemGroup>

View File

@ -16,7 +16,7 @@ using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
{
public class ClientRenderingMultpleComponentsTest : ServerTestBase<BasicTestAppServerSiteFixture<MultipleComponents>>
public class ClientRenderingMultpleComponentsTest : E2ETest.Infrastructure.ServerTestBase<BasicTestAppServerSiteFixture<MultipleComponents>>
{
private const string MarkerPattern = ".*?<!--Blazor:(.*?)-->.*?";

View File

@ -12,7 +12,7 @@ using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
{
public class HeadComponentsTest : ServerTestBase<ToggleExecutionModeServerFixture<Program>>
public class HeadComponentsTest : E2ETest.Infrastructure.ServerTestBase<ToggleExecutionModeServerFixture<Program>>
{
public HeadComponentsTest(
BrowserFixture browserFixture,

View File

@ -13,7 +13,7 @@ using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
{
public class VirtualizationTest : ServerTestBase<ToggleExecutionModeServerFixture<Program>>
public class VirtualizationTest : E2ETest.Infrastructure.ServerTestBase<ToggleExecutionModeServerFixture<Program>>
{
public VirtualizationTest(
BrowserFixture browserFixture,

View File

@ -14,9 +14,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
{
// For now this is limited to server-side execution because we don't have the ability to set the
// culture in client-side Blazor.
// This type is internal since localization currently does not work.
// Make it public onc https://github.com/dotnet/runtime/issues/38124 is resolved.
internal class WebAssemblyGlobalizationTest : GlobalizationTest<ToggleExecutionModeServerFixture<Program>>
public class WebAssemblyGlobalizationTest : GlobalizationTest<ToggleExecutionModeServerFixture<Program>>
{
public WebAssemblyGlobalizationTest(
BrowserFixture browserFixture,

View File

@ -0,0 +1,124 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Globalization;
using System.Linq;
using GlobalizationWasmApp;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
{
// Blazor WebAssembly loads ICU (globalization) data for subset of cultures by default.
// This app covers testing this along with verifying the behavior for fallback culture for localized resources.
public class WebAssemblyICUShardingTest : ServerTestBase<ToggleExecutionModeServerFixture<Program>>
{
private readonly DateTime DisplayTime = new DateTime(2020, 09, 02);
public WebAssemblyICUShardingTest(
BrowserFixture browserFixture,
ToggleExecutionModeServerFixture<Program> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
}
[Fact]
public void LoadingApp_FrenchLanguage_Works()
{
// Arrange
// This verifies the EFIGS icu data set.
var culture = new CultureInfo("fr-FR");
Initialize(culture);
var cultureDisplay = Browser.Exists(By.Id("culture"));
Assert.Equal(culture.ToString(), cultureDisplay.Text);
var dateDisplay = Browser.Exists(By.Id("dateTime"));
Assert.Equal(DisplayTime.ToString(culture), dateDisplay.Text);
var localizedDisplay = Browser.Exists(By.Id("localizedString"));
Assert.Equal("Bonjour!", localizedDisplay.Text);
}
[Fact]
public void LoadingApp_KoreanLanguage_Works()
{
// Arrange
// This verifies the CJK icu data set.
var culture = new CultureInfo("ko-KO");
Initialize(culture);
var cultureDisplay = Browser.Exists(By.Id("culture"));
Assert.Equal(culture.ToString(), cultureDisplay.Text);
var dateDisplay = Browser.Exists(By.Id("dateTime"));
Assert.Equal(DisplayTime.ToString(culture), dateDisplay.Text);
var localizedDisplay = Browser.Exists(By.Id("localizedString"));
// The app has a "ko" resx file. This test verifies that we can walk up the culture hierarchy correctly.
Assert.Equal("안녕하세요", localizedDisplay.Text);
}
[Fact]
public void LoadingApp_RussianLanguage_Works()
{
// Arrange
// This verifies the non-CJK icu data set.
var culture = new CultureInfo("ru");
Initialize(culture);
var cultureDisplay = Browser.Exists(By.Id("culture"));
Assert.Equal(culture.ToString(), cultureDisplay.Text);
var dateDisplay = Browser.Exists(By.Id("dateTime"));
Assert.Equal(DisplayTime.ToString(culture), dateDisplay.Text);
var localizedDisplay = Browser.Exists(By.Id("localizedString"));
Assert.Equal("Hello", localizedDisplay.Text); // No localized resources for this culture.
}
[Fact(Skip = "Figure out why this is broken")]
public void LoadingApp_KannadaLanguage_Works()
{
// Arrange
// This verifies the non-CJK icu data set.
var culture = new CultureInfo("kn");
Initialize(culture);
var cultureDisplay = Browser.Exists(By.Id("culture"));
Assert.Equal(culture.ToString(), cultureDisplay.Text);
var dateDisplay = Browser.Exists(By.Id("dateTime"));
Assert.Equal(DisplayTime.ToString(culture), dateDisplay.Text);
var localizedDisplay = Browser.Exists(By.Id("localizedString"));
Assert.Equal("ಹಲೋ", localizedDisplay.Text);
}
[Fact]
public void LoadingApp_DynamicallySetLanguageThrows()
{
// Arrange
// This verifies that we complain if the app programtically configures a language.
var expected = "This application's globalization settings requires using the combined globalization data file.";
Navigate($"{ServerPathBase}/?culture=fr&dotNetCulture=es", noReload: false);
var errorUi = Browser.Exists(By.Id("blazor-error-ui"));
Browser.Equal("block", () => errorUi.GetCssValue("display"));
var logs = Browser.GetBrowserLogs(LogLevel.Severe).Select(l => l.Message);
Assert.True(logs.Any(l => l.Contains(expected)),
$"Expected to see globalization error message in the browser logs: {string.Join(Environment.NewLine, logs)}.");
}
private void Initialize(CultureInfo culture)
{
Navigate($"{ServerPathBase}/?culture={culture}", noReload: false);
}
}
}

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Hello" xml:space="preserve">
<value>Bonjour!</value>
</data>
</root>

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Hello" xml:space="preserve">
<value>ಹಲೋ</value>
</data>
</root>

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Hello" xml:space="preserve">
<value>안녕하세요</value>
</data>
</root>

View File

@ -0,0 +1,8 @@
@using System.Globalization
@inject Microsoft.Extensions.Localization.IStringLocalizer<App> Loc
<p>App's culture: <span id="culture">@CultureInfo.CurrentCulture</span></p>
<p>DateTime: <span id="dateTime">@(new DateTime(2020, 09, 02))</span></p>
<p>Localized string: <span id="localizedString">@Loc["Hello"]</span></p>

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<AdditionalRunArguments>--pathbase /subdir</AdditionalRunArguments>
<!-- Resx generation on Resources.resx only -->
<GenerateResxSource>false</GenerateResxSource>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Components.WebAssembly" />
<Reference Include="Microsoft.Extensions.Localization" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,39 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Globalization;
using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
namespace GlobalizationWasmApp
{
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddLocalization();
builder.RootComponents.Add<App>("app");
var host = builder.Build();
ConfigureCulture(host);
await host.RunAsync();
}
private static void ConfigureCulture(WebAssemblyHost host)
{
var uri = new Uri(host.Services.GetService<NavigationManager>().Uri);
var cultureName = HttpUtility.ParseQueryString(uri.Query)["dotNetCulture"] ?? HttpUtility.ParseQueryString(uri.Query)["culture"];
var culture = new CultureInfo(cultureName);
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
}
}
}

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<head>
<style>
#blazor-error-ui { display: none }
</style>
</head>
<body>
<div id="blazor-error-ui">Error</div>
<app>Loading...</app>
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
<script>
(function(){
const search = new window.URLSearchParams(window.location.search);
const culture = search.get('culture');
Blazor.start({ applicationCulture: culture });
})();
</script>
</body>
</html>