Localization finds resources in class libraries

This commit is contained in:
Ryan Brandenburg 2016-09-16 12:24:34 -07:00
parent f650de8cdf
commit b2ef91df9f
17 changed files with 731 additions and 37 deletions

View File

@ -31,6 +31,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "LocalizationWebsite", "test
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Localization.FunctionalTests", "test\Microsoft.AspNetCore.Localization.FunctionalTests\Microsoft.AspNetCore.Localization.FunctionalTests.xproj", "{B1B441BA-3AC8-49F8-850D-E5A178E77DE2}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ResourcesClassLibraryWithAttribute", "test\ResourcesClassLibraryWithAttribute\ResourcesClassLibraryWithAttribute.xproj", "{F27639B9-913E-43AF-9D64-BBD98D9A420A}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ResourcesClassLibraryNoAttribute", "test\ResourcesClassLibraryNoAttribute\ResourcesClassLibraryNoAttribute.xproj", "{34740578-D5B5-4FB4-AFD4-5E87B5443E20}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -73,6 +77,14 @@ Global
{B1B441BA-3AC8-49F8-850D-E5A178E77DE2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B1B441BA-3AC8-49F8-850D-E5A178E77DE2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B1B441BA-3AC8-49F8-850D-E5A178E77DE2}.Release|Any CPU.Build.0 = Release|Any CPU
{F27639B9-913E-43AF-9D64-BBD98D9A420A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F27639B9-913E-43AF-9D64-BBD98D9A420A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F27639B9-913E-43AF-9D64-BBD98D9A420A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F27639B9-913E-43AF-9D64-BBD98D9A420A}.Release|Any CPU.Build.0 = Release|Any CPU
{34740578-D5B5-4FB4-AFD4-5E87B5443E20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{34740578-D5B5-4FB4-AFD4-5E87B5443E20}.Debug|Any CPU.Build.0 = Debug|Any CPU
{34740578-D5B5-4FB4-AFD4-5E87B5443E20}.Release|Any CPU.ActiveCfg = Release|Any CPU
{34740578-D5B5-4FB4-AFD4-5E87B5443E20}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -87,5 +99,7 @@ Global
{19A2A931-5C60-47A0-816A-0DC9C4CE5736} = {B723DB83-A670-4BCB-95FB-195361331AD2}
{EF6C7431-2FB8-4396-8947-F50F31689AF4} = {B723DB83-A670-4BCB-95FB-195361331AD2}
{B1B441BA-3AC8-49F8-850D-E5A178E77DE2} = {B723DB83-A670-4BCB-95FB-195361331AD2}
{F27639B9-913E-43AF-9D64-BBD98D9A420A} = {B723DB83-A670-4BCB-95FB-195361331AD2}
{34740578-D5B5-4FB4-AFD4-5E87B5443E20} = {B723DB83-A670-4BCB-95FB-195361331AD2}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,33 @@
// 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;
namespace Microsoft.Extensions.Localization
{
/// <summary>
/// Provides the location of resources for an Assembly.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)]
public class ResourceLocationAttribute : Attribute
{
/// <summary>
/// Creates a new <see cref="ResourceLocationAttribute"/>.
/// </summary>
/// <param name="resourceLocation">The location of resources for this Assembly.</param>
public ResourceLocationAttribute(string resourceLocation)
{
if (string.IsNullOrEmpty(resourceLocation))
{
throw new ArgumentNullException(nameof(resourceLocation));
}
ResourceLocation = resourceLocation;
}
/// <summary>
/// The location of resources for this Assembly.
/// </summary>
public string ResourceLocation { get; }
}
}

View File

@ -14,12 +14,17 @@ namespace Microsoft.Extensions.Localization
/// <summary>
/// An <see cref="IStringLocalizerFactory"/> that creates instances of <see cref="ResourceManagerStringLocalizer"/>.
/// </summary>
/// <remarks>
/// <see cref="ResourceManagerStringLocalizerFactory"/> offers multiple ways to set the relative path of
/// resources to be used. They are, in order of precedence:
/// <see cref="ResourceLocationAttribute"/> -> <see cref="LocalizationOptions.ResourcesPath"/> -> the project root.
/// </remarks>
public class ResourceManagerStringLocalizerFactory : IStringLocalizerFactory
{
private readonly IResourceNamesCache _resourceNamesCache = new ResourceNamesCache();
private readonly ConcurrentDictionary<string, ResourceManagerStringLocalizer> _localizerCache =
new ConcurrentDictionary<string, ResourceManagerStringLocalizer>();
private readonly IHostingEnvironment _hostingEnvironment;
private readonly string _applicationName;
private readonly string _resourcesRelativePath;
/// <summary>
@ -41,7 +46,7 @@ namespace Microsoft.Extensions.Localization
throw new ArgumentNullException(nameof(localizationOptions));
}
_hostingEnvironment = hostingEnvironment;
_applicationName = hostingEnvironment.ApplicationName;
_resourcesRelativePath = localizationOptions.Value.ResourcesPath ?? string.Empty;
if (!string.IsNullOrEmpty(_resourcesRelativePath))
{
@ -62,7 +67,7 @@ namespace Microsoft.Extensions.Localization
throw new ArgumentNullException(nameof(typeInfo));
}
return GetResourcePrefix(typeInfo, _hostingEnvironment.ApplicationName, _resourcesRelativePath);
return GetResourcePrefix(typeInfo, new AssemblyName(typeInfo.Assembly.FullName).Name, _resourcesRelativePath);
}
/// <summary>
@ -106,9 +111,16 @@ namespace Microsoft.Extensions.Localization
throw new ArgumentNullException(nameof(baseResourceName));
}
var locationPath = baseNamespace == _hostingEnvironment.ApplicationName ?
baseNamespace + "." + _resourcesRelativePath :
baseNamespace + ".";
if (string.IsNullOrEmpty(baseNamespace))
{
throw new ArgumentNullException(nameof(baseNamespace));
}
var assemblyName = new AssemblyName(baseNamespace);
var assembly = Assembly.Load(assemblyName);
var resourceLocation = GetResourcePath(assembly);
var locationPath = baseNamespace + "." + resourceLocation;
baseResourceName = locationPath + TrimPrefix(baseResourceName, baseNamespace + ".");
return baseResourceName;
@ -129,17 +141,12 @@ namespace Microsoft.Extensions.Localization
var typeInfo = resourceSource.GetTypeInfo();
var assembly = typeInfo.Assembly;
var assemblyName = new AssemblyName(assembly.FullName);
var resourcePath = GetResourcePath(assembly);
// Re-root the base name if a resources path is set
var baseName = GetResourcePrefix(typeInfo);
var baseName = GetResourcePrefix(typeInfo, assemblyName.Name, resourcePath);
return _localizerCache.GetOrAdd(baseName, _ =>
new ResourceManagerStringLocalizer(
new ResourceManager(baseName, assembly),
assembly,
baseName,
_resourceNamesCache)
);
return _localizerCache.GetOrAdd(baseName, _ => CreateResourceManagerStringLocalizer(assembly, baseName));
}
/// <summary>
@ -155,21 +162,71 @@ namespace Microsoft.Extensions.Localization
throw new ArgumentNullException(nameof(baseName));
}
location = location ?? _hostingEnvironment.ApplicationName;
baseName = GetResourcePrefix(baseName, location);
location = location ?? _applicationName;
return _localizerCache.GetOrAdd($"B={baseName},L={location}", _ =>
{
var assembly = Assembly.Load(new AssemblyName(location));
return new ResourceManagerStringLocalizer(
new ResourceManager(baseName, assembly),
assembly,
baseName,
_resourceNamesCache);
var assemblyName = new AssemblyName(location);
var assembly = Assembly.Load(assemblyName);
baseName = GetResourcePrefix(baseName, location);
return CreateResourceManagerStringLocalizer(assembly, baseName);
});
}
/// <summary>Creates a <see cref="ResourceManagerStringLocalizer"/> for the given input.</summary>
/// <param name="assembly">The assembly to create a <see cref="ResourceManagerStringLocalizer"/> for.</param>
/// <param name="baseName">The base name of the resource to search for.</param>
/// <returns>A <see cref="ResourceManagerStringLocalizer"/> for the given <paramref name="assembly"/> and <paramref name="baseName"/>.</returns>
/// <remarks>This method is virtual for testing purposes only.</remarks>
protected virtual ResourceManagerStringLocalizer CreateResourceManagerStringLocalizer(
Assembly assembly,
string baseName)
{
return new ResourceManagerStringLocalizer(
new ResourceManager(baseName, assembly),
assembly,
baseName,
_resourceNamesCache);
}
/// <summary>
/// Gets the resource prefix used to look up the resource.
/// </summary>
/// <param name="location">The general location of the resource.</param>
/// <param name="baseName">The base name of the resource.</param>
/// <param name="resourceLocation">The location of the resource within <paramref name="location"/>.</param>
/// <returns>The resource prefix used to look up the resource.</returns>
protected virtual string GetResourcePrefix(string location, string baseName, string resourceLocation)
{
// Re-root the base name if a resources path is set
return location + "." + resourceLocation + TrimPrefix(baseName, location + ".");
}
/// <summary>Gets a <see cref="ResourceLocationAttribute"/> from the provided <see cref="Assembly"/>.</summary>
/// <param name="assembly">The assembly to get a <see cref="ResourceLocationAttribute"/> from.</param>
/// <returns>The <see cref="ResourceLocationAttribute"/> associated with the given <see cref="Assembly"/>.</returns>
/// <remarks>This method is protected and virtual for testing purposes only.</remarks>
protected virtual ResourceLocationAttribute GetResourceLocationAttribute(Assembly assembly)
{
return assembly.GetCustomAttribute<ResourceLocationAttribute>();
}
private string GetResourcePath(Assembly assembly)
{
var resourceLocationAttribute = GetResourceLocationAttribute(assembly);
// If we don't have an attribute assume all assemblies use the same resource location.
var resourceLocation = resourceLocationAttribute == null
? _resourcesRelativePath
: resourceLocationAttribute.ResourceLocation + ".";
resourceLocation = resourceLocation
.Replace(Path.DirectorySeparatorChar, '.')
.Replace(Path.AltDirectorySeparatorChar, '.');
return resourceLocation;
}
private static string TrimPrefix(string name, string prefix)
{
if (name.StartsWith(prefix, StringComparison.Ordinal))

View File

@ -22,7 +22,8 @@
"Microsoft.AspNetCore.Hosting.Abstractions": "1.1.0-*",
"Microsoft.Extensions.DependencyInjection.Abstractions": "1.1.0-*",
"Microsoft.Extensions.Localization.Abstractions": "1.1.0-*",
"Microsoft.Extensions.Options": "1.1.0-*"
"Microsoft.Extensions.Options": "1.1.0-*",
"System.Reflection.Extensions": "4.0.1-*"
},
"frameworks": {
"net451": {},

View File

@ -0,0 +1,70 @@
// 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;
using System.Globalization;
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
namespace LocalizationWebsite
{
public class StartupResourcesInClassLibrary
{
public void ConfigureServices(IServiceCollection services)
{
services.AddLocalization(options => options.ResourcesPath = "Resources");
}
public void Configure(
IApplicationBuilder app,
ILoggerFactory loggerFactory,
IStringLocalizerFactory stringLocalizerFactory)
{
loggerFactory.AddConsole(minLevel: LogLevel.Warning);
var supportedCultures = new List<CultureInfo>()
{
new CultureInfo("en-US"),
new CultureInfo("fr-FR")
};
app.UseRequestLocalization(new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture("en-US"),
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures
});
var noAttributeStringLocalizer = stringLocalizerFactory.Create(typeof(ResourcesClassLibraryNoAttribute.Model));
var withAttributeStringLocalizer = stringLocalizerFactory.Create(typeof(ResourcesClassLibraryWithAttribute.Model));
var noAttributeAssembly = typeof(ResourcesClassLibraryNoAttribute.Model).GetTypeInfo().Assembly;
var noAttributeName = new AssemblyName(noAttributeAssembly.FullName).Name;
var noAttributeNameStringLocalizer = stringLocalizerFactory.Create(
nameof(ResourcesClassLibraryNoAttribute.Model),
noAttributeName);
var withAttributeAssembly = typeof(ResourcesClassLibraryWithAttribute.Model).GetTypeInfo().Assembly;
var withAttributeName = new AssemblyName(withAttributeAssembly.FullName).Name;
var withAttributeNameStringLocalizer = stringLocalizerFactory.Create(
nameof(ResourcesClassLibraryWithAttribute.Model),
withAttributeName);
app.Run(async (context) =>
{
await context.Response.WriteAsync(noAttributeNameStringLocalizer["Hello"]);
await context.Response.WriteAsync(" ");
await context.Response.WriteAsync(noAttributeStringLocalizer["Hello"]);
await context.Response.WriteAsync(" ");
await context.Response.WriteAsync(withAttributeNameStringLocalizer["Hello"]);
await context.Response.WriteAsync(" ");
await context.Response.WriteAsync(withAttributeStringLocalizer["Hello"]);
});
}
}
}

View File

@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using LocalizationWebsite.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
@ -43,6 +44,9 @@ namespace LocalizationWebsite
});
var stringLocalizer = stringLocalizerFactory.Create("Test", location: null);
var assembly = typeof(StartupResourcesInFolder).GetTypeInfo().Assembly;
var assemblyName = new AssemblyName(assembly.FullName).Name;
var stringLocalizerExplicitLocation = stringLocalizerFactory.Create("Test", assemblyName);
app.Run(async (context) =>
{
@ -51,6 +55,8 @@ namespace LocalizationWebsite
await context.Response.WriteAsync(stringLocalizer["Hello"]);
await context.Response.WriteAsync(" ");
await context.Response.WriteAsync(custromerStringLocalizer["Hello"]);
await context.Response.WriteAsync(" ");
await context.Response.WriteAsync(stringLocalizerExplicitLocation["Hello"]);
});
}
}

View File

@ -9,7 +9,9 @@
"Microsoft.AspNetCore.Server.Kestrel": "1.1.0-*",
"Microsoft.Extensions.Configuration.CommandLine": "1.1.0-*",
"Microsoft.Extensions.Localization": "1.1.0-*",
"Microsoft.Extensions.Logging.Console": "1.1.0-*"
"Microsoft.Extensions.Logging.Console": "1.1.0-*",
"ResourcesClassLibraryNoAttribute": "1.0.0-*",
"ResourcesClassLibraryWithAttribute": "1.0.0-*"
},
"frameworks": {
"netcoreapp1.0": {

View File

@ -12,7 +12,43 @@ namespace Microsoft.AspNetCore.Localization.FunctionalTests
public class LocalizationTest
{
private static readonly string _applicationPath = Path.Combine("test", "LocalizationWebsite");
[ConditionalTheory]
[OSSkipCondition(OperatingSystems.Linux)]
[OSSkipCondition(OperatingSystems.MacOSX)]
[InlineData(RuntimeFlavor.Clr, "http://localhost:5070/", RuntimeArchitecture.x64)]
public Task Localization_ResourcesInClassLibrary_ReturnLocalizedValue_Windows(
RuntimeFlavor runtimeFlavor,
string applicationBaseUrl,
RuntimeArchitecture runtimeArchitecture)
{
var testRunner = new TestRunner(_applicationPath);
return testRunner.RunTestAndVerifyResponse(
runtimeFlavor,
runtimeArchitecture,
applicationBaseUrl,
"ResourcesInClassLibrary",
"fr-FR",
"Bonjour from ResourcesClassLibraryNoAttribute Bonjour from ResourcesClassLibraryNoAttribute Bonjour from ResourcesClassLibraryWithAttribute Bonjour from ResourcesClassLibraryWithAttribute");
}
[Theory]
[InlineData(RuntimeFlavor.CoreClr, "http://localhost:5071/", RuntimeArchitecture.x64)]
public Task Localization_ResourcesInClassLibrary_ReturnLocalizedValue_AllOS(
RuntimeFlavor runtimeFlavor,
string applicationBaseUrl,
RuntimeArchitecture runtimeArchitecture)
{
var testRunner = new TestRunner(_applicationPath);
return testRunner.RunTestAndVerifyResponse(
runtimeFlavor,
runtimeArchitecture,
applicationBaseUrl,
"ResourcesInClassLibrary",
"fr-FR",
"Bonjour from ResourcesClassLibraryNoAttribute Bonjour from ResourcesClassLibraryNoAttribute Bonjour from ResourcesClassLibraryWithAttribute Bonjour from ResourcesClassLibraryWithAttribute");
}
[ConditionalTheory]
[OSSkipCondition(OperatingSystems.Linux)]
[OSSkipCondition(OperatingSystems.MacOSX)]
@ -30,7 +66,7 @@ namespace Microsoft.AspNetCore.Localization.FunctionalTests
applicationBaseUrl,
"ResourcesInFolder",
"fr-FR",
"Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder");
"Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder Bonjour from Test in resources folder");
}
[ConditionalTheory]
@ -50,7 +86,7 @@ namespace Microsoft.AspNetCore.Localization.FunctionalTests
applicationBaseUrl,
"ResourcesInFolder",
"fr-FR-test",
"Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder");
"Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder Bonjour from Test in resources folder");
}
[ConditionalTheory]
@ -70,7 +106,7 @@ namespace Microsoft.AspNetCore.Localization.FunctionalTests
applicationBaseUrl,
"ResourcesInFolder",
"fr-FR-test-again-too-deep-to-work",
"Hello Hello Hello");
"Hello Hello Hello Hello");
}
[ConditionalFact]
@ -85,7 +121,7 @@ namespace Microsoft.AspNetCore.Localization.FunctionalTests
"http://localhost:5072",
"ResourcesInFolder",
"fr-FR",
"Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder");
"Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder Bonjour from Test in resources folder");
}
[ConditionalFact]
@ -100,7 +136,7 @@ namespace Microsoft.AspNetCore.Localization.FunctionalTests
"http://localhost:5072",
"ResourcesInFolder",
"fr-FR-test",
"Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder");
"Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder Bonjour from Test in resources folder");
}
[ConditionalFact]
@ -115,7 +151,7 @@ namespace Microsoft.AspNetCore.Localization.FunctionalTests
"http://localhost:5073/",
"ResourcesInFolder",
"fr-FR",
"Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder");
"Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder Bonjour from Test in resources folder");
}
[ConditionalFact]
@ -130,7 +166,7 @@ namespace Microsoft.AspNetCore.Localization.FunctionalTests
"http://localhost:5073/",
"ResourcesInFolder",
"fr-FR-test",
"Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder");
"Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder Bonjour from Test in resources folder");
}
[ConditionalTheory]

View File

@ -1,16 +1,78 @@
// 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.IO;
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace Microsoft.Extensions.Localization.Test
namespace Microsoft.Extensions.Localization.Tests
{
public class TestResourceManagerStringLocalizerFactory : ResourceManagerStringLocalizerFactory
{
private ResourceLocationAttribute _resourceLocationAttribute;
public Assembly Assembly { get; private set; }
public string BaseName { get; private set; }
public TestResourceManagerStringLocalizerFactory(
IHostingEnvironment hostingEnvironment,
IOptions<LocalizationOptions> localizationOptions,
ResourceLocationAttribute resourceLocationAttribute)
: base(hostingEnvironment, localizationOptions)
{
_resourceLocationAttribute = resourceLocationAttribute;
}
protected override ResourceLocationAttribute GetResourceLocationAttribute(Assembly assembly)
{
return _resourceLocationAttribute;
}
protected override ResourceManagerStringLocalizer CreateResourceManagerStringLocalizer(Assembly assembly, string baseName)
{
BaseName = baseName;
Assembly = assembly;
return base.CreateResourceManagerStringLocalizer(assembly, baseName);
}
}
public class ResourceManagerStringLocalizerFactoryTest
{
[Fact]
public void Create_OverloadsProduceSameResult()
{
// Arrange
var hostingEnvironment = new Mock<IHostingEnvironment>();
hostingEnvironment.SetupGet(a => a.ApplicationName).Returns("TestApplication");
var locOptions = new LocalizationOptions();
var options = new Mock<IOptions<LocalizationOptions>>();
options.Setup(o => o.Value).Returns(locOptions);
var resourceLocationAttribute = new ResourceLocationAttribute(Path.Combine("My", "Resources"));
var typeFactory = new TestResourceManagerStringLocalizerFactory(
hostingEnvironment.Object,
options.Object,
resourceLocationAttribute);
var stringFactory = new TestResourceManagerStringLocalizerFactory(
hostingEnvironment.Object,
options.Object,
resourceLocationAttribute);
var type = typeof(ResourceManagerStringLocalizerFactoryTest);
var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName);
// Act
typeFactory.Create(type);
stringFactory.Create(type.Name, assemblyName.Name);
// Assert
Assert.Equal(typeFactory.BaseName, stringFactory.BaseName);
Assert.Equal(typeFactory.Assembly.FullName, stringFactory.Assembly.FullName);
}
[Fact]
public void Create_FromType_ReturnsCachedResultForSameType()
{
@ -49,6 +111,27 @@ namespace Microsoft.Extensions.Localization.Test
Assert.NotSame(result1, result2);
}
[Fact]
public void Create_FromType_ResourcesPathDirectorySeperatorToDot()
{
// Arrange
var hostingEnvironment = new Mock<IHostingEnvironment>();
var locOptions = new LocalizationOptions();
locOptions.ResourcesPath = Path.Combine("My", "Resources");
var options = new Mock<IOptions<LocalizationOptions>>();
options.Setup(o => o.Value).Returns(locOptions);
var factory = new TestResourceManagerStringLocalizerFactory(
hostingEnvironment.Object,
options.Object,
resourceLocationAttribute: null);
// Act
factory.Create(typeof(ResourceManagerStringLocalizerFactoryTest));
// Assert
Assert.Equal("Microsoft.Extensions.Localization.Tests.My.Resources." + nameof(ResourceManagerStringLocalizerFactoryTest), factory.BaseName);
}
[Fact]
public void Create_FromNameLocation_ReturnsCachedResultForSameNameLocation()
{
@ -99,8 +182,8 @@ namespace Microsoft.Extensions.Localization.Test
var options = new Mock<IOptions<LocalizationOptions>>();
options.Setup(o => o.Value).Returns(locOptions);
var factory = new ResourceManagerStringLocalizerFactory(hostingEnvironment.Object, localizationOptions: options.Object);
var location1 = typeof(ResourceManagerStringLocalizer).GetTypeInfo().Assembly.FullName;
var location2 = typeof(ResourceManagerStringLocalizerFactoryTest).GetTypeInfo().Assembly.FullName;
var location1 = new AssemblyName(typeof(ResourceManagerStringLocalizer).GetTypeInfo().Assembly.FullName).Name;
var location2 = new AssemblyName(typeof(ResourceManagerStringLocalizerFactoryTest).GetTypeInfo().Assembly.FullName).Name;
// Act
var result1 = factory.Create("baseName", location1);
@ -109,5 +192,46 @@ namespace Microsoft.Extensions.Localization.Test
// Assert
Assert.NotSame(result1, result2);
}
[Fact]
public void Create_FromNameLocation_ResourcesPathDirectorySeparatorToDot()
{
// Arrange
var hostingEnvironment = new Mock<IHostingEnvironment>();
hostingEnvironment.SetupGet(a => a.ApplicationName).Returns("Microsoft.Extensions.Localization.Tests");
var locOptions = new LocalizationOptions();
locOptions.ResourcesPath = Path.Combine("My", "Resources");
var options = new Mock<IOptions<LocalizationOptions>>();
options.Setup(o => o.Value).Returns(locOptions);
var factory = new TestResourceManagerStringLocalizerFactory(
hostingEnvironment.Object,
options.Object,
resourceLocationAttribute: null);
// Act
var result1 = factory.Create("baseName", location: null);
// Assert
Assert.Equal("Microsoft.Extensions.Localization.Tests.My.Resources.baseName", factory.BaseName);
}
[Fact]
public void Create_FromNameLocation_NullLocationUsesApplicationPath()
{
// Arrange
var hostingEnvironment = new Mock<IHostingEnvironment>();
hostingEnvironment.SetupGet(a => a.ApplicationName).Returns("Microsoft.Extensions.Localization.Tests");
var locOptions = new LocalizationOptions();
var options = new Mock<IOptions<LocalizationOptions>>();
options.Setup(o => o.Value).Returns(locOptions);
var factory = new ResourceManagerStringLocalizerFactory(hostingEnvironment.Object, localizationOptions: options.Object);
// Act
var result1 = factory.Create("baseName", location: null);
var result2 = factory.Create("baseName", location: null);
// Assert
Assert.Same(result1, result2);
}
}
}

View File

@ -0,0 +1,12 @@
// 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.
namespace ResourcesClassLibraryNoAttribute
{
public class Model
{
public Model()
{
}
}
}

View File

@ -0,0 +1,22 @@
// 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.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("ResourcesClassLibraryNoAttribute")]
[assembly: AssemblyTrademark("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("34740578-d5b5-4fb4-afd4-5e87b5443e20")]

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 from ResourcesClassLibraryNoAttribute</value>
</data>
</root>

View File

@ -0,0 +1,17 @@
{
"version": "1.0.0-*",
"buildOptions": {
"warningsAsErrors": true,
"keyFile": "../../tools/Key.snk"
},
"dependencies": {
"NETStandard.Library": "1.6.0"
},
"frameworks": {
"net451": {},
"netstandard1.6": {
"imports": "dnxcore50"
}
}
}

View File

@ -0,0 +1,12 @@
// 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.
namespace ResourcesClassLibraryWithAttribute
{
public class Model
{
public Model()
{
}
}
}

View File

@ -0,0 +1,24 @@
// 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.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Localization;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("ResourcesClassLibraryWithAttribute")]
[assembly: AssemblyTrademark("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("f27639b9-913e-43af-9d64-bbd98d9a420a")]
[assembly: ResourceLocation("ResourceFolder")]

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 from ResourcesClassLibraryWithAttribute</value>
</data>
</root>

View File

@ -0,0 +1,18 @@
{
"version": "1.0.0-*",
"buildOptions": {
"warningsAsErrors": true,
"keyFile": "../../tools/Key.snk"
},
"dependencies": {
"Microsoft.Extensions.Localization": "1.1.0-*",
"NETStandard.Library": "1.6.0"
},
"frameworks": {
"net451": {},
"netstandard1.6": {
"imports": "dnxcore50"
}
}
}