Skip to content
This repository was archived by the owner on Nov 29, 2018. It is now read-only.

Commit b2ef91d

Browse files
Localization finds resources in class libraries
1 parent f650de8 commit b2ef91d

17 files changed

Lines changed: 732 additions & 38 deletions

File tree

Localization.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "LocalizationWebsite", "test
3131
EndProject
3232
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}"
3333
EndProject
34+
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ResourcesClassLibraryWithAttribute", "test\ResourcesClassLibraryWithAttribute\ResourcesClassLibraryWithAttribute.xproj", "{F27639B9-913E-43AF-9D64-BBD98D9A420A}"
35+
EndProject
36+
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ResourcesClassLibraryNoAttribute", "test\ResourcesClassLibraryNoAttribute\ResourcesClassLibraryNoAttribute.xproj", "{34740578-D5B5-4FB4-AFD4-5E87B5443E20}"
37+
EndProject
3438
Global
3539
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3640
Debug|Any CPU = Debug|Any CPU
@@ -73,6 +77,14 @@ Global
7377
{B1B441BA-3AC8-49F8-850D-E5A178E77DE2}.Debug|Any CPU.Build.0 = Debug|Any CPU
7478
{B1B441BA-3AC8-49F8-850D-E5A178E77DE2}.Release|Any CPU.ActiveCfg = Release|Any CPU
7579
{B1B441BA-3AC8-49F8-850D-E5A178E77DE2}.Release|Any CPU.Build.0 = Release|Any CPU
80+
{F27639B9-913E-43AF-9D64-BBD98D9A420A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
81+
{F27639B9-913E-43AF-9D64-BBD98D9A420A}.Debug|Any CPU.Build.0 = Debug|Any CPU
82+
{F27639B9-913E-43AF-9D64-BBD98D9A420A}.Release|Any CPU.ActiveCfg = Release|Any CPU
83+
{F27639B9-913E-43AF-9D64-BBD98D9A420A}.Release|Any CPU.Build.0 = Release|Any CPU
84+
{34740578-D5B5-4FB4-AFD4-5E87B5443E20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
85+
{34740578-D5B5-4FB4-AFD4-5E87B5443E20}.Debug|Any CPU.Build.0 = Debug|Any CPU
86+
{34740578-D5B5-4FB4-AFD4-5E87B5443E20}.Release|Any CPU.ActiveCfg = Release|Any CPU
87+
{34740578-D5B5-4FB4-AFD4-5E87B5443E20}.Release|Any CPU.Build.0 = Release|Any CPU
7688
EndGlobalSection
7789
GlobalSection(SolutionProperties) = preSolution
7890
HideSolutionNode = FALSE
@@ -87,5 +99,7 @@ Global
8799
{19A2A931-5C60-47A0-816A-0DC9C4CE5736} = {B723DB83-A670-4BCB-95FB-195361331AD2}
88100
{EF6C7431-2FB8-4396-8947-F50F31689AF4} = {B723DB83-A670-4BCB-95FB-195361331AD2}
89101
{B1B441BA-3AC8-49F8-850D-E5A178E77DE2} = {B723DB83-A670-4BCB-95FB-195361331AD2}
102+
{F27639B9-913E-43AF-9D64-BBD98D9A420A} = {B723DB83-A670-4BCB-95FB-195361331AD2}
103+
{34740578-D5B5-4FB4-AFD4-5E87B5443E20} = {B723DB83-A670-4BCB-95FB-195361331AD2}
90104
EndGlobalSection
91105
EndGlobal
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
6+
namespace Microsoft.Extensions.Localization
7+
{
8+
/// <summary>
9+
/// Provides the location of resources for an Assembly.
10+
/// </summary>
11+
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)]
12+
public class ResourceLocationAttribute : Attribute
13+
{
14+
/// <summary>
15+
/// Creates a new <see cref="ResourceLocationAttribute"/>.
16+
/// </summary>
17+
/// <param name="resourceLocation">The location of resources for this Assembly.</param>
18+
public ResourceLocationAttribute(string resourceLocation)
19+
{
20+
if (string.IsNullOrEmpty(resourceLocation))
21+
{
22+
throw new ArgumentNullException(nameof(resourceLocation));
23+
}
24+
25+
ResourceLocation = resourceLocation;
26+
}
27+
28+
/// <summary>
29+
/// The location of resources for this Assembly.
30+
/// </summary>
31+
public string ResourceLocation { get; }
32+
}
33+
}

src/Microsoft.Extensions.Localization/ResourceManagerStringLocalizerFactory.cs

Lines changed: 82 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,17 @@ namespace Microsoft.Extensions.Localization
1414
/// <summary>
1515
/// An <see cref="IStringLocalizerFactory"/> that creates instances of <see cref="ResourceManagerStringLocalizer"/>.
1616
/// </summary>
17+
/// <remarks>
18+
/// <see cref="ResourceManagerStringLocalizerFactory"/> offers multiple ways to set the relative path of
19+
/// resources to be used. They are, in order of precedence:
20+
/// <see cref="ResourceLocationAttribute"/> -> <see cref="LocalizationOptions.ResourcesPath"/> -> the project root.
21+
/// </remarks>
1722
public class ResourceManagerStringLocalizerFactory : IStringLocalizerFactory
1823
{
1924
private readonly IResourceNamesCache _resourceNamesCache = new ResourceNamesCache();
2025
private readonly ConcurrentDictionary<string, ResourceManagerStringLocalizer> _localizerCache =
2126
new ConcurrentDictionary<string, ResourceManagerStringLocalizer>();
22-
private readonly IHostingEnvironment _hostingEnvironment;
27+
private readonly string _applicationName;
2328
private readonly string _resourcesRelativePath;
2429

2530
/// <summary>
@@ -41,7 +46,7 @@ public ResourceManagerStringLocalizerFactory(
4146
throw new ArgumentNullException(nameof(localizationOptions));
4247
}
4348

44-
_hostingEnvironment = hostingEnvironment;
49+
_applicationName = hostingEnvironment.ApplicationName;
4550
_resourcesRelativePath = localizationOptions.Value.ResourcesPath ?? string.Empty;
4651
if (!string.IsNullOrEmpty(_resourcesRelativePath))
4752
{
@@ -62,7 +67,7 @@ protected virtual string GetResourcePrefix(TypeInfo typeInfo)
6267
throw new ArgumentNullException(nameof(typeInfo));
6368
}
6469

65-
return GetResourcePrefix(typeInfo, _hostingEnvironment.ApplicationName, _resourcesRelativePath);
70+
return GetResourcePrefix(typeInfo, new AssemblyName(typeInfo.Assembly.FullName).Name, _resourcesRelativePath);
6671
}
6772

6873
/// <summary>
@@ -106,9 +111,16 @@ protected virtual string GetResourcePrefix(string baseResourceName, string baseN
106111
throw new ArgumentNullException(nameof(baseResourceName));
107112
}
108113

109-
var locationPath = baseNamespace == _hostingEnvironment.ApplicationName ?
110-
baseNamespace + "." + _resourcesRelativePath :
111-
baseNamespace + ".";
114+
if (string.IsNullOrEmpty(baseNamespace))
115+
{
116+
throw new ArgumentNullException(nameof(baseNamespace));
117+
}
118+
119+
var assemblyName = new AssemblyName(baseNamespace);
120+
var assembly = Assembly.Load(assemblyName);
121+
var resourceLocation = GetResourcePath(assembly);
122+
var locationPath = baseNamespace + "." + resourceLocation;
123+
112124
baseResourceName = locationPath + TrimPrefix(baseResourceName, baseNamespace + ".");
113125

114126
return baseResourceName;
@@ -129,17 +141,12 @@ public IStringLocalizer Create(Type resourceSource)
129141

130142
var typeInfo = resourceSource.GetTypeInfo();
131143
var assembly = typeInfo.Assembly;
144+
var assemblyName = new AssemblyName(assembly.FullName);
145+
var resourcePath = GetResourcePath(assembly);
132146

133-
// Re-root the base name if a resources path is set
134-
var baseName = GetResourcePrefix(typeInfo);
135-
136-
return _localizerCache.GetOrAdd(baseName, _ =>
137-
new ResourceManagerStringLocalizer(
138-
new ResourceManager(baseName, assembly),
139-
assembly,
140-
baseName,
141-
_resourceNamesCache)
142-
);
147+
var baseName = GetResourcePrefix(typeInfo, assemblyName.Name, resourcePath);
148+
149+
return _localizerCache.GetOrAdd(baseName, _ => CreateResourceManagerStringLocalizer(assembly, baseName));
143150
}
144151

145152
/// <summary>
@@ -155,21 +162,71 @@ public IStringLocalizer Create(string baseName, string location)
155162
throw new ArgumentNullException(nameof(baseName));
156163
}
157164

158-
location = location ?? _hostingEnvironment.ApplicationName;
159-
160-
baseName = GetResourcePrefix(baseName, location);
165+
location = location ?? _applicationName;
161166

162167
return _localizerCache.GetOrAdd($"B={baseName},L={location}", _ =>
163168
{
164-
var assembly = Assembly.Load(new AssemblyName(location));
165-
return new ResourceManagerStringLocalizer(
166-
new ResourceManager(baseName, assembly),
167-
assembly,
168-
baseName,
169-
_resourceNamesCache);
169+
var assemblyName = new AssemblyName(location);
170+
var assembly = Assembly.Load(assemblyName);
171+
baseName = GetResourcePrefix(baseName, location);
172+
173+
return CreateResourceManagerStringLocalizer(assembly, baseName);
170174
});
171175
}
172176

177+
/// <summary>Creates a <see cref="ResourceManagerStringLocalizer"/> for the given input.</summary>
178+
/// <param name="assembly">The assembly to create a <see cref="ResourceManagerStringLocalizer"/> for.</param>
179+
/// <param name="baseName">The base name of the resource to search for.</param>
180+
/// <returns>A <see cref="ResourceManagerStringLocalizer"/> for the given <paramref name="assembly"/> and <paramref name="baseName"/>.</returns>
181+
/// <remarks>This method is virtual for testing purposes only.</remarks>
182+
protected virtual ResourceManagerStringLocalizer CreateResourceManagerStringLocalizer(
183+
Assembly assembly,
184+
string baseName)
185+
{
186+
return new ResourceManagerStringLocalizer(
187+
new ResourceManager(baseName, assembly),
188+
assembly,
189+
baseName,
190+
_resourceNamesCache);
191+
}
192+
193+
/// <summary>
194+
/// Gets the resource prefix used to look up the resource.
195+
/// </summary>
196+
/// <param name="location">The general location of the resource.</param>
197+
/// <param name="baseName">The base name of the resource.</param>
198+
/// <param name="resourceLocation">The location of the resource within <paramref name="location"/>.</param>
199+
/// <returns>The resource prefix used to look up the resource.</returns>
200+
protected virtual string GetResourcePrefix(string location, string baseName, string resourceLocation)
201+
{
202+
// Re-root the base name if a resources path is set
203+
return location + "." + resourceLocation + TrimPrefix(baseName, location + ".");
204+
}
205+
206+
/// <summary>Gets a <see cref="ResourceLocationAttribute"/> from the provided <see cref="Assembly"/>.</summary>
207+
/// <param name="assembly">The assembly to get a <see cref="ResourceLocationAttribute"/> from.</param>
208+
/// <returns>The <see cref="ResourceLocationAttribute"/> associated with the given <see cref="Assembly"/>.</returns>
209+
/// <remarks>This method is protected and virtual for testing purposes only.</remarks>
210+
protected virtual ResourceLocationAttribute GetResourceLocationAttribute(Assembly assembly)
211+
{
212+
return assembly.GetCustomAttribute<ResourceLocationAttribute>();
213+
}
214+
215+
private string GetResourcePath(Assembly assembly)
216+
{
217+
var resourceLocationAttribute = GetResourceLocationAttribute(assembly);
218+
219+
// If we don't have an attribute assume all assemblies use the same resource location.
220+
var resourceLocation = resourceLocationAttribute == null
221+
? _resourcesRelativePath
222+
: resourceLocationAttribute.ResourceLocation + ".";
223+
resourceLocation = resourceLocation
224+
.Replace(Path.DirectorySeparatorChar, '.')
225+
.Replace(Path.AltDirectorySeparatorChar, '.');
226+
227+
return resourceLocation;
228+
}
229+
173230
private static string TrimPrefix(string name, string prefix)
174231
{
175232
if (name.StartsWith(prefix, StringComparison.Ordinal))

src/Microsoft.Extensions.Localization/project.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"Microsoft.AspNetCore.Hosting.Abstractions": "1.1.0-*",
2323
"Microsoft.Extensions.DependencyInjection.Abstractions": "1.1.0-*",
2424
"Microsoft.Extensions.Localization.Abstractions": "1.1.0-*",
25-
"Microsoft.Extensions.Options": "1.1.0-*"
25+
"Microsoft.Extensions.Options": "1.1.0-*",
26+
"System.Reflection.Extensions": "4.0.1-*"
2627
},
2728
"frameworks": {
2829
"net451": {},
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using System.Globalization;
6+
using System.Reflection;
7+
using Microsoft.AspNetCore.Builder;
8+
using Microsoft.AspNetCore.Http;
9+
using Microsoft.AspNetCore.Localization;
10+
using Microsoft.Extensions.DependencyInjection;
11+
using Microsoft.Extensions.Localization;
12+
using Microsoft.Extensions.Logging;
13+
14+
namespace LocalizationWebsite
15+
{
16+
public class StartupResourcesInClassLibrary
17+
{
18+
public void ConfigureServices(IServiceCollection services)
19+
{
20+
services.AddLocalization(options => options.ResourcesPath = "Resources");
21+
}
22+
23+
public void Configure(
24+
IApplicationBuilder app,
25+
ILoggerFactory loggerFactory,
26+
IStringLocalizerFactory stringLocalizerFactory)
27+
{
28+
loggerFactory.AddConsole(minLevel: LogLevel.Warning);
29+
30+
var supportedCultures = new List<CultureInfo>()
31+
{
32+
new CultureInfo("en-US"),
33+
new CultureInfo("fr-FR")
34+
};
35+
36+
app.UseRequestLocalization(new RequestLocalizationOptions
37+
{
38+
DefaultRequestCulture = new RequestCulture("en-US"),
39+
SupportedCultures = supportedCultures,
40+
SupportedUICultures = supportedCultures
41+
});
42+
43+
var noAttributeStringLocalizer = stringLocalizerFactory.Create(typeof(ResourcesClassLibraryNoAttribute.Model));
44+
var withAttributeStringLocalizer = stringLocalizerFactory.Create(typeof(ResourcesClassLibraryWithAttribute.Model));
45+
46+
var noAttributeAssembly = typeof(ResourcesClassLibraryNoAttribute.Model).GetTypeInfo().Assembly;
47+
var noAttributeName = new AssemblyName(noAttributeAssembly.FullName).Name;
48+
var noAttributeNameStringLocalizer = stringLocalizerFactory.Create(
49+
nameof(ResourcesClassLibraryNoAttribute.Model),
50+
noAttributeName);
51+
52+
var withAttributeAssembly = typeof(ResourcesClassLibraryWithAttribute.Model).GetTypeInfo().Assembly;
53+
var withAttributeName = new AssemblyName(withAttributeAssembly.FullName).Name;
54+
var withAttributeNameStringLocalizer = stringLocalizerFactory.Create(
55+
nameof(ResourcesClassLibraryWithAttribute.Model),
56+
withAttributeName);
57+
58+
app.Run(async (context) =>
59+
{
60+
await context.Response.WriteAsync(noAttributeNameStringLocalizer["Hello"]);
61+
await context.Response.WriteAsync(" ");
62+
await context.Response.WriteAsync(noAttributeStringLocalizer["Hello"]);
63+
await context.Response.WriteAsync(" ");
64+
await context.Response.WriteAsync(withAttributeNameStringLocalizer["Hello"]);
65+
await context.Response.WriteAsync(" ");
66+
await context.Response.WriteAsync(withAttributeStringLocalizer["Hello"]);
67+
});
68+
}
69+
}
70+
}

test/LocalizationWebsite/StartupResourcesInFolder.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Collections.Generic;
55
using System.Globalization;
6+
using System.Reflection;
67
using LocalizationWebsite.Models;
78
using Microsoft.AspNetCore.Builder;
89
using Microsoft.AspNetCore.Http;
@@ -43,6 +44,9 @@ public void Configure(
4344
});
4445

4546
var stringLocalizer = stringLocalizerFactory.Create("Test", location: null);
47+
var assembly = typeof(StartupResourcesInFolder).GetTypeInfo().Assembly;
48+
var assemblyName = new AssemblyName(assembly.FullName).Name;
49+
var stringLocalizerExplicitLocation = stringLocalizerFactory.Create("Test", assemblyName);
4650

4751
app.Run(async (context) =>
4852
{
@@ -51,6 +55,8 @@ public void Configure(
5155
await context.Response.WriteAsync(stringLocalizer["Hello"]);
5256
await context.Response.WriteAsync(" ");
5357
await context.Response.WriteAsync(custromerStringLocalizer["Hello"]);
58+
await context.Response.WriteAsync(" ");
59+
await context.Response.WriteAsync(stringLocalizerExplicitLocation["Hello"]);
5460
});
5561
}
5662
}

test/LocalizationWebsite/project.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
"Microsoft.AspNetCore.Server.Kestrel": "1.1.0-*",
1010
"Microsoft.Extensions.Configuration.CommandLine": "1.1.0-*",
1111
"Microsoft.Extensions.Localization": "1.1.0-*",
12-
"Microsoft.Extensions.Logging.Console": "1.1.0-*"
12+
"Microsoft.Extensions.Logging.Console": "1.1.0-*",
13+
"ResourcesClassLibraryNoAttribute": "1.0.0-*",
14+
"ResourcesClassLibraryWithAttribute": "1.0.0-*"
1315
},
1416
"frameworks": {
1517
"netcoreapp1.0": {

0 commit comments

Comments
 (0)