Use ICU sharding (#25521)
This commit is contained in:
parent
70725b554c
commit
2228c98b88
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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:(.*?)-->.*?";
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue