diff --git a/src/Localization/Abstractions/src/IStringLocalizer.cs b/src/Localization/Abstractions/src/IStringLocalizer.cs
new file mode 100644
index 0000000000..0e1145bbca
--- /dev/null
+++ b/src/Localization/Abstractions/src/IStringLocalizer.cs
@@ -0,0 +1,45 @@
+// 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;
+
+namespace Microsoft.Extensions.Localization
+{
+ ///
+ /// Represents a service that provides localized strings.
+ ///
+ public interface IStringLocalizer
+ {
+ ///
+ /// Gets the string resource with the given name.
+ ///
+ /// The name of the string resource.
+ /// The string resource as a .
+ LocalizedString this[string name] { get; }
+
+ ///
+ /// Gets the string resource with the given name and formatted with the supplied arguments.
+ ///
+ /// The name of the string resource.
+ /// The values to format the string with.
+ /// The formatted string resource as a .
+ LocalizedString this[string name, params object[] arguments] { get; }
+
+ ///
+ /// Gets all string resources.
+ ///
+ ///
+ /// A indicating whether to include strings from parent cultures.
+ ///
+ /// The strings.
+ IEnumerable GetAllStrings(bool includeParentCultures);
+
+ ///
+ /// Creates a new for a specific .
+ ///
+ /// The to use.
+ /// A culture-specific .
+ IStringLocalizer WithCulture(CultureInfo culture);
+ }
+}
\ No newline at end of file
diff --git a/src/Localization/Abstractions/src/IStringLocalizerFactory.cs b/src/Localization/Abstractions/src/IStringLocalizerFactory.cs
new file mode 100644
index 0000000000..559fa69c30
--- /dev/null
+++ b/src/Localization/Abstractions/src/IStringLocalizerFactory.cs
@@ -0,0 +1,29 @@
+// 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
+{
+ ///
+ /// Represents a factory that creates instances.
+ ///
+ public interface IStringLocalizerFactory
+ {
+ ///
+ /// Creates an using the and
+ /// of the specified .
+ ///
+ /// The .
+ /// The .
+ IStringLocalizer Create(Type resourceSource);
+
+ ///
+ /// Creates an .
+ ///
+ /// The base name of the resource to load strings from.
+ /// The location to load resources from.
+ /// The .
+ IStringLocalizer Create(string baseName, string location);
+ }
+}
\ No newline at end of file
diff --git a/src/Localization/Abstractions/src/IStringLocalizerOfT.cs b/src/Localization/Abstractions/src/IStringLocalizerOfT.cs
new file mode 100644
index 0000000000..695678a900
--- /dev/null
+++ b/src/Localization/Abstractions/src/IStringLocalizerOfT.cs
@@ -0,0 +1,14 @@
+// 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 Microsoft.Extensions.Localization
+{
+ ///
+ /// Represents an that provides strings for .
+ ///
+ /// The to provide strings for.
+ public interface IStringLocalizer : IStringLocalizer
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/src/Localization/Abstractions/src/LocalizedString.cs b/src/Localization/Abstractions/src/LocalizedString.cs
new file mode 100644
index 0000000000..6556da40a0
--- /dev/null
+++ b/src/Localization/Abstractions/src/LocalizedString.cs
@@ -0,0 +1,90 @@
+// 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
+{
+ ///
+ /// A locale specific string.
+ ///
+ public class LocalizedString
+ {
+ ///
+ /// Creates a new .
+ ///
+ /// The name of the string in the resource it was loaded from.
+ /// The actual string.
+ public LocalizedString(string name, string value)
+ : this(name, value, resourceNotFound: false)
+ {
+ }
+
+ ///
+ /// Creates a new .
+ ///
+ /// The name of the string in the resource it was loaded from.
+ /// The actual string.
+ /// Whether the string was not found in a resource. Set this to true to indicate an alternate string value was used.
+ public LocalizedString(string name, string value, bool resourceNotFound)
+ : this(name, value, resourceNotFound, searchedLocation: null)
+ {
+ }
+
+ ///
+ /// Creates a new .
+ ///
+ /// The name of the string in the resource it was loaded from.
+ /// The actual string.
+ /// Whether the string was not found in a resource. Set this to true to indicate an alternate string value was used.
+ /// The location which was searched for a localization value.
+ public LocalizedString(string name, string value, bool resourceNotFound, string searchedLocation)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ Name = name;
+ Value = value;
+ ResourceNotFound = resourceNotFound;
+ SearchedLocation = searchedLocation;
+ }
+
+ public static implicit operator string(LocalizedString localizedString)
+ {
+ return localizedString?.Value;
+ }
+
+ ///
+ /// The name of the string in the resource it was loaded from.
+ ///
+ public string Name { get; }
+
+ ///
+ /// The actual string.
+ ///
+ public string Value { get; }
+
+ ///
+ /// Whether the string was not found in a resource. If true, an alternate string value was used.
+ ///
+ public bool ResourceNotFound { get; }
+
+ ///
+ /// The location which was searched for a localization value.
+ ///
+ public string SearchedLocation { get; }
+
+ ///
+ /// Returns the actual string.
+ ///
+ /// The actual string.
+ public override string ToString() => Value;
+ }
+}
\ No newline at end of file
diff --git a/src/Localization/Abstractions/src/Microsoft.Extensions.Localization.Abstractions.csproj b/src/Localization/Abstractions/src/Microsoft.Extensions.Localization.Abstractions.csproj
new file mode 100644
index 0000000000..8508eb071a
--- /dev/null
+++ b/src/Localization/Abstractions/src/Microsoft.Extensions.Localization.Abstractions.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Microsoft .NET Extensions
+ Abstractions of application localization services.
+Commonly used types:
+Microsoft.Extensions.Localization.IStringLocalizer
+Microsoft.Extensions.Localization.IStringLocalizer<T>
+ netstandard2.0
+ $(NoWarn);CS1591
+ true
+ localization
+
+
+
diff --git a/src/Localization/Abstractions/src/StringLocalizerExtensions.cs b/src/Localization/Abstractions/src/StringLocalizerExtensions.cs
new file mode 100644
index 0000000000..bde47f74f3
--- /dev/null
+++ b/src/Localization/Abstractions/src/StringLocalizerExtensions.cs
@@ -0,0 +1,74 @@
+// 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.Collections.Generic;
+
+namespace Microsoft.Extensions.Localization
+{
+ public static class StringLocalizerExtensions
+ {
+ ///
+ /// Gets the string resource with the given name.
+ ///
+ /// The .
+ /// The name of the string resource.
+ /// The string resource as a .
+ public static LocalizedString GetString(
+ this IStringLocalizer stringLocalizer,
+ string name)
+ {
+ if (stringLocalizer == null)
+ {
+ throw new ArgumentNullException(nameof(stringLocalizer));
+ }
+
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ return stringLocalizer[name];
+ }
+
+ ///
+ /// Gets the string resource with the given name and formatted with the supplied arguments.
+ ///
+ /// The .
+ /// The name of the string resource.
+ /// The values to format the string with.
+ /// The formatted string resource as a .
+ public static LocalizedString GetString(
+ this IStringLocalizer stringLocalizer,
+ string name,
+ params object[] arguments)
+ {
+ if (stringLocalizer == null)
+ {
+ throw new ArgumentNullException(nameof(stringLocalizer));
+ }
+
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ return stringLocalizer[name, arguments];
+ }
+
+ ///
+ /// Gets all string resources including those for parent cultures.
+ ///
+ /// The .
+ /// The string resources.
+ public static IEnumerable GetAllStrings(this IStringLocalizer stringLocalizer)
+ {
+ if (stringLocalizer == null)
+ {
+ throw new ArgumentNullException(nameof(stringLocalizer));
+ }
+
+ return stringLocalizer.GetAllStrings(includeParentCultures: true);
+ }
+ }
+}
diff --git a/src/Localization/Abstractions/src/StringLocalizerOfT.cs b/src/Localization/Abstractions/src/StringLocalizerOfT.cs
new file mode 100644
index 0000000000..131c1126ec
--- /dev/null
+++ b/src/Localization/Abstractions/src/StringLocalizerOfT.cs
@@ -0,0 +1,67 @@
+// 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.Collections.Generic;
+using System.Globalization;
+
+namespace Microsoft.Extensions.Localization
+{
+ ///
+ /// Provides strings for .
+ ///
+ /// The to provide strings for.
+ public class StringLocalizer : IStringLocalizer
+ {
+ private IStringLocalizer _localizer;
+
+ ///
+ /// Creates a new .
+ ///
+ /// The to use.
+ public StringLocalizer(IStringLocalizerFactory factory)
+ {
+ if (factory == null)
+ {
+ throw new ArgumentNullException(nameof(factory));
+ }
+
+ _localizer = factory.Create(typeof(TResourceSource));
+ }
+
+ ///
+ public virtual IStringLocalizer WithCulture(CultureInfo culture) => _localizer.WithCulture(culture);
+
+ ///
+ public virtual LocalizedString this[string name]
+ {
+ get
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ return _localizer[name];
+ }
+ }
+
+ ///
+ public virtual LocalizedString this[string name, params object[] arguments]
+ {
+ get
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ return _localizer[name, arguments];
+ }
+ }
+
+ ///
+ public IEnumerable GetAllStrings(bool includeParentCultures) =>
+ _localizer.GetAllStrings(includeParentCultures);
+ }
+}
\ No newline at end of file
diff --git a/src/Localization/Abstractions/src/baseline.netcore.json b/src/Localization/Abstractions/src/baseline.netcore.json
new file mode 100644
index 0000000000..02ba71db8e
--- /dev/null
+++ b/src/Localization/Abstractions/src/baseline.netcore.json
@@ -0,0 +1,413 @@
+{
+ "AssemblyIdentity": "Microsoft.Extensions.Localization.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.Extensions.Localization.IStringLocalizer",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Item",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.Localization.LocalizedString",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Item",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "arguments",
+ "Type": "System.Object[]",
+ "IsParams": true
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.Localization.LocalizedString",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetAllStrings",
+ "Parameters": [
+ {
+ "Name": "includeParentCultures",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Collections.Generic.IEnumerable",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "WithCulture",
+ "Parameters": [
+ {
+ "Name": "culture",
+ "Type": "System.Globalization.CultureInfo"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.Localization.IStringLocalizer",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.Extensions.Localization.IStringLocalizerFactory",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "Create",
+ "Parameters": [
+ {
+ "Name": "resourceSource",
+ "Type": "System.Type"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.Localization.IStringLocalizer",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Create",
+ "Parameters": [
+ {
+ "Name": "baseName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "location",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.Localization.IStringLocalizer",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.Extensions.Localization.IStringLocalizer",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [
+ "Microsoft.Extensions.Localization.IStringLocalizer"
+ ],
+ "Members": [],
+ "GenericParameters": [
+ {
+ "ParameterName": "T",
+ "ParameterPosition": 0,
+ "BaseTypeOrInterfaces": []
+ }
+ ]
+ },
+ {
+ "Name": "Microsoft.Extensions.Localization.LocalizedString",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "op_Implicit",
+ "Parameters": [
+ {
+ "Name": "localizedString",
+ "Type": "Microsoft.Extensions.Localization.LocalizedString"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Static": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Name",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Value",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_ResourceNotFound",
+ "Parameters": [],
+ "ReturnType": "System.Boolean",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_SearchedLocation",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "ToString",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Virtual": true,
+ "Override": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "value",
+ "Type": "System.String"
+ },
+ {
+ "Name": "resourceNotFound",
+ "Type": "System.Boolean"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "value",
+ "Type": "System.String"
+ },
+ {
+ "Name": "resourceNotFound",
+ "Type": "System.Boolean"
+ },
+ {
+ "Name": "searchedLocation",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.Extensions.Localization.StringLocalizerExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GetString",
+ "Parameters": [
+ {
+ "Name": "stringLocalizer",
+ "Type": "Microsoft.Extensions.Localization.IStringLocalizer"
+ },
+ {
+ "Name": "name",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.Localization.LocalizedString",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetString",
+ "Parameters": [
+ {
+ "Name": "stringLocalizer",
+ "Type": "Microsoft.Extensions.Localization.IStringLocalizer"
+ },
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "arguments",
+ "Type": "System.Object[]",
+ "IsParams": true
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.Localization.LocalizedString",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetAllStrings",
+ "Parameters": [
+ {
+ "Name": "stringLocalizer",
+ "Type": "Microsoft.Extensions.Localization.IStringLocalizer"
+ }
+ ],
+ "ReturnType": "System.Collections.Generic.IEnumerable",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.Extensions.Localization.StringLocalizer",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.Extensions.Localization.IStringLocalizer"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Item",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.Localization.LocalizedString",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizer",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Item",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "arguments",
+ "Type": "System.Object[]",
+ "IsParams": true
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.Localization.LocalizedString",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizer",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetAllStrings",
+ "Parameters": [
+ {
+ "Name": "includeParentCultures",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Collections.Generic.IEnumerable",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizer",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "WithCulture",
+ "Parameters": [
+ {
+ "Name": "culture",
+ "Type": "System.Globalization.CultureInfo"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.Localization.IStringLocalizer",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizer",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "factory",
+ "Type": "Microsoft.Extensions.Localization.IStringLocalizerFactory"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": [
+ {
+ "ParameterName": "TResourceSource",
+ "ParameterPosition": 0,
+ "BaseTypeOrInterfaces": []
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/Localization/Localization/src/IResourceNamesCache.cs b/src/Localization/Localization/src/IResourceNamesCache.cs
new file mode 100644
index 0000000000..90d104aa68
--- /dev/null
+++ b/src/Localization/Localization/src/IResourceNamesCache.cs
@@ -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;
+using System.Collections.Generic;
+
+namespace Microsoft.Extensions.Localization
+{
+ ///
+ /// Represents a cache of string names in resources.
+ ///
+ public interface IResourceNamesCache
+ {
+ ///
+ /// Adds a set of resource names to the cache by using the specified function, if the name does not already exist.
+ ///
+ /// The resource name to add string names for.
+ /// The function used to generate the string names for the resource.
+ /// The string names for the resource.
+ IList GetOrAdd(string name, Func> valueFactory);
+ }
+}
diff --git a/src/Localization/Localization/src/Internal/AssemblyWrapper.cs b/src/Localization/Localization/src/Internal/AssemblyWrapper.cs
new file mode 100644
index 0000000000..b0c3c2bce1
--- /dev/null
+++ b/src/Localization/Localization/src/Internal/AssemblyWrapper.cs
@@ -0,0 +1,28 @@
+// 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.IO;
+using System.Reflection;
+
+namespace Microsoft.Extensions.Localization.Internal
+{
+ public class AssemblyWrapper
+ {
+ public AssemblyWrapper(Assembly assembly)
+ {
+ if (assembly == null)
+ {
+ throw new ArgumentNullException(nameof(assembly));
+ }
+
+ Assembly = assembly;
+ }
+
+ public Assembly Assembly { get; }
+
+ public virtual string FullName => Assembly.FullName;
+
+ public virtual Stream GetManifestResourceStream(string name) => Assembly.GetManifestResourceStream(name);
+ }
+}
diff --git a/src/Localization/Localization/src/Internal/IResourceStringProvider.cs b/src/Localization/Localization/src/Internal/IResourceStringProvider.cs
new file mode 100644
index 0000000000..b74bd80eda
--- /dev/null
+++ b/src/Localization/Localization/src/Internal/IResourceStringProvider.cs
@@ -0,0 +1,13 @@
+// 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;
+
+namespace Microsoft.Extensions.Localization.Internal
+{
+ public interface IResourceStringProvider
+ {
+ IList GetAllResourceStrings(CultureInfo culture, bool throwOnMissing);
+ }
+}
diff --git a/src/Localization/Localization/src/Internal/ResourceManagerStringLocalizerLoggerExtensions.cs b/src/Localization/Localization/src/Internal/ResourceManagerStringLocalizerLoggerExtensions.cs
new file mode 100644
index 0000000000..456e07009e
--- /dev/null
+++ b/src/Localization/Localization/src/Internal/ResourceManagerStringLocalizerLoggerExtensions.cs
@@ -0,0 +1,27 @@
+// 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 Microsoft.Extensions.Logging;
+
+namespace Microsoft.Extensions.Localization.Internal
+{
+ internal static class ResourceManagerStringLocalizerLoggerExtensions
+ {
+ private static readonly Action _searchedLocation;
+
+ static ResourceManagerStringLocalizerLoggerExtensions()
+ {
+ _searchedLocation = LoggerMessage.Define(
+ LogLevel.Debug,
+ 1,
+ $"{nameof(ResourceManagerStringLocalizer)} searched for '{{Key}}' in '{{LocationSearched}}' with culture '{{Culture}}'.");
+ }
+
+ public static void SearchedLocation(this ILogger logger, string key, string searchedLocation, CultureInfo culture)
+ {
+ _searchedLocation(logger, key, searchedLocation, culture, null);
+ }
+ }
+}
diff --git a/src/Localization/Localization/src/Internal/ResourceManagerStringProvider.cs b/src/Localization/Localization/src/Internal/ResourceManagerStringProvider.cs
new file mode 100644
index 0000000000..9eef8c84a8
--- /dev/null
+++ b/src/Localization/Localization/src/Internal/ResourceManagerStringProvider.cs
@@ -0,0 +1,80 @@
+// 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;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Reflection;
+using System.Resources;
+
+namespace Microsoft.Extensions.Localization.Internal
+{
+ public class ResourceManagerStringProvider : IResourceStringProvider
+ {
+ private readonly IResourceNamesCache _resourceNamesCache;
+ private readonly ResourceManager _resourceManager;
+ private readonly Assembly _assembly;
+ private readonly string _resourceBaseName;
+
+ public ResourceManagerStringProvider(
+ IResourceNamesCache resourceCache,
+ ResourceManager resourceManager,
+ Assembly assembly,
+ string baseName)
+ {
+ _resourceManager = resourceManager;
+ _resourceNamesCache = resourceCache;
+ _assembly = assembly;
+ _resourceBaseName = baseName;
+ }
+
+ private string GetResourceCacheKey(CultureInfo culture)
+ {
+ var resourceName = _resourceManager.BaseName;
+
+ return $"Culture={culture.Name};resourceName={resourceName};Assembly={_assembly.FullName}";
+ }
+
+ private string GetResourceName(CultureInfo culture)
+ {
+ var resourceStreamName = _resourceBaseName;
+ if (!string.IsNullOrEmpty(culture.Name))
+ {
+ resourceStreamName += "." + culture.Name;
+ }
+ resourceStreamName += ".resources";
+
+ return resourceStreamName;
+ }
+
+ public IList GetAllResourceStrings(CultureInfo culture, bool throwOnMissing)
+ {
+ var cacheKey = GetResourceCacheKey(culture);
+
+ return _resourceNamesCache.GetOrAdd(cacheKey, _ =>
+ {
+ // We purposly don't dispose the ResourceSet because it causes an ObjectDisposedException when you try to read the values later.
+ var resourceSet = _resourceManager.GetResourceSet(culture, createIfNotExists: true, tryParents: false);
+ if (resourceSet == null)
+ {
+ if (throwOnMissing)
+ {
+ throw new MissingManifestResourceException(Resources.FormatLocalization_MissingManifest(GetResourceName(culture)));
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ var names = new List();
+ foreach (DictionaryEntry entry in resourceSet)
+ {
+ names.Add((string)entry.Key);
+ }
+
+ return names;
+ });
+ }
+ }
+}
diff --git a/src/Localization/Localization/src/LocalizationOptions.cs b/src/Localization/Localization/src/LocalizationOptions.cs
new file mode 100644
index 0000000000..1b7408fe67
--- /dev/null
+++ b/src/Localization/Localization/src/LocalizationOptions.cs
@@ -0,0 +1,16 @@
+// 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 Microsoft.Extensions.Localization
+{
+ ///
+ /// Provides programmatic configuration for localization.
+ ///
+ public class LocalizationOptions
+ {
+ ///
+ /// The relative path under application root where resource files are located.
+ ///
+ public string ResourcesPath { get; set; } = string.Empty;
+ }
+}
diff --git a/src/Localization/Localization/src/LocalizationServiceCollectionExtensions.cs b/src/Localization/Localization/src/LocalizationServiceCollectionExtensions.cs
new file mode 100644
index 0000000000..111c1c40d9
--- /dev/null
+++ b/src/Localization/Localization/src/LocalizationServiceCollectionExtensions.cs
@@ -0,0 +1,76 @@
+// 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 Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Localization;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ ///
+ /// Extension methods for setting up localization services in an .
+ ///
+ public static class LocalizationServiceCollectionExtensions
+ {
+ ///
+ /// Adds services required for application localization.
+ ///
+ /// The to add the services to.
+ /// The so that additional calls can be chained.
+ public static IServiceCollection AddLocalization(this IServiceCollection services)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ services.AddOptions();
+
+ AddLocalizationServices(services);
+
+ return services;
+ }
+
+ ///
+ /// Adds services required for application localization.
+ ///
+ /// The to add the services to.
+ ///
+ /// An to configure the .
+ ///
+ /// The so that additional calls can be chained.
+ public static IServiceCollection AddLocalization(
+ this IServiceCollection services,
+ Action setupAction)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ if (setupAction == null)
+ {
+ throw new ArgumentNullException(nameof(setupAction));
+ }
+
+ AddLocalizationServices(services, setupAction);
+
+ return services;
+ }
+
+ // To enable unit testing
+ internal static void AddLocalizationServices(IServiceCollection services)
+ {
+ services.TryAddSingleton();
+ services.TryAddTransient(typeof(IStringLocalizer<>), typeof(StringLocalizer<>));
+ }
+
+ internal static void AddLocalizationServices(
+ IServiceCollection services,
+ Action setupAction)
+ {
+ AddLocalizationServices(services);
+ services.Configure(setupAction);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Localization/Localization/src/Microsoft.Extensions.Localization.csproj b/src/Localization/Localization/src/Microsoft.Extensions.Localization.csproj
new file mode 100644
index 0000000000..73365a15eb
--- /dev/null
+++ b/src/Localization/Localization/src/Microsoft.Extensions.Localization.csproj
@@ -0,0 +1,19 @@
+
+
+
+ Microsoft .NET Extensions
+ Application localization services and default implementation based on ResourceManager to load localized assembly resources.
+ netstandard2.0
+ $(NoWarn);CS1591
+ true
+ localization
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Localization/Localization/src/Properties/AssemblyInfo.cs b/src/Localization/Localization/src/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..3e297b801e
--- /dev/null
+++ b/src/Localization/Localization/src/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// 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.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.Extensions.Localization.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
diff --git a/src/Localization/Localization/src/Properties/Resources.Designer.cs b/src/Localization/Localization/src/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..1123d648ad
--- /dev/null
+++ b/src/Localization/Localization/src/Properties/Resources.Designer.cs
@@ -0,0 +1,62 @@
+//
+namespace Microsoft.Extensions.Localization
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.Extensions.Localization.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ ///
+ /// The manifest '{0}' was not found.
+ ///
+ internal static string Localization_MissingManifest
+ {
+ get { return GetString("Localization_MissingManifest"); }
+ }
+
+ ///
+ /// The manifest '{0}' was not found.
+ ///
+ internal static string FormatLocalization_MissingManifest(object p0)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Localization_MissingManifest"), p0);
+ }
+
+ ///
+ /// No manifests exist for the current culture.
+ ///
+ internal static string Localization_MissingManifest_Parent
+ {
+ get { return GetString("Localization_MissingManifest_Parent"); }
+ }
+
+ ///
+ /// No manifests exist for the current culture.
+ ///
+ internal static string FormatLocalization_MissingManifest_Parent()
+ {
+ return GetString("Localization_MissingManifest_Parent");
+ }
+
+ private static string GetString(string name, params string[] formatterNames)
+ {
+ var value = _resourceManager.GetString(name);
+
+ System.Diagnostics.Debug.Assert(value != null);
+
+ if (formatterNames != null)
+ {
+ for (var i = 0; i < formatterNames.Length; i++)
+ {
+ value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
+ }
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/src/Localization/Localization/src/ResourceLocationAttribute.cs b/src/Localization/Localization/src/ResourceLocationAttribute.cs
new file mode 100644
index 0000000000..5bf281d90e
--- /dev/null
+++ b/src/Localization/Localization/src/ResourceLocationAttribute.cs
@@ -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
+{
+ ///
+ /// Provides the location of resources for an Assembly.
+ ///
+ [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)]
+ public class ResourceLocationAttribute : Attribute
+ {
+ ///
+ /// Creates a new .
+ ///
+ /// The location of resources for this Assembly.
+ public ResourceLocationAttribute(string resourceLocation)
+ {
+ if (string.IsNullOrEmpty(resourceLocation))
+ {
+ throw new ArgumentNullException(nameof(resourceLocation));
+ }
+
+ ResourceLocation = resourceLocation;
+ }
+
+ ///
+ /// The location of resources for this Assembly.
+ ///
+ public string ResourceLocation { get; }
+ }
+}
diff --git a/src/Localization/Localization/src/ResourceManagerStringLocalizer.cs b/src/Localization/Localization/src/ResourceManagerStringLocalizer.cs
new file mode 100644
index 0000000000..e2e1a3f234
--- /dev/null
+++ b/src/Localization/Localization/src/ResourceManagerStringLocalizer.cs
@@ -0,0 +1,274 @@
+// 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.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Reflection;
+using System.Resources;
+using Microsoft.Extensions.Localization.Internal;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Extensions.Localization
+{
+ ///
+ /// An that uses the and
+ /// to provide localized strings.
+ ///
+ /// This type is thread-safe.
+ public class ResourceManagerStringLocalizer : IStringLocalizer
+ {
+ private readonly ConcurrentDictionary _missingManifestCache = new ConcurrentDictionary();
+ private readonly IResourceNamesCache _resourceNamesCache;
+ private readonly ResourceManager _resourceManager;
+ private readonly IResourceStringProvider _resourceStringProvider;
+ private readonly string _resourceBaseName;
+ private readonly ILogger _logger;
+
+ ///
+ /// Creates a new .
+ ///
+ /// The to read strings from.
+ /// The that contains the strings as embedded resources.
+ /// The base name of the embedded resource that contains the strings.
+ /// Cache of the list of strings for a given resource assembly name.
+ /// The .
+ public ResourceManagerStringLocalizer(
+ ResourceManager resourceManager,
+ Assembly resourceAssembly,
+ string baseName,
+ IResourceNamesCache resourceNamesCache,
+ ILogger logger)
+ : this(
+ resourceManager,
+ new AssemblyWrapper(resourceAssembly),
+ baseName,
+ resourceNamesCache,
+ logger)
+ {
+ }
+
+ ///
+ /// Intended for testing purposes only.
+ ///
+ public ResourceManagerStringLocalizer(
+ ResourceManager resourceManager,
+ AssemblyWrapper resourceAssemblyWrapper,
+ string baseName,
+ IResourceNamesCache resourceNamesCache,
+ ILogger logger)
+ : this(
+ resourceManager,
+ new ResourceManagerStringProvider(
+ resourceNamesCache,
+ resourceManager,
+ resourceAssemblyWrapper.Assembly,
+ baseName),
+ baseName,
+ resourceNamesCache,
+ logger)
+ {
+ }
+
+ ///
+ /// Intended for testing purposes only.
+ ///
+ public ResourceManagerStringLocalizer(
+ ResourceManager resourceManager,
+ IResourceStringProvider resourceStringProvider,
+ string baseName,
+ IResourceNamesCache resourceNamesCache,
+ ILogger logger)
+ {
+ if (resourceManager == null)
+ {
+ throw new ArgumentNullException(nameof(resourceManager));
+ }
+
+ if (resourceStringProvider == null)
+ {
+ throw new ArgumentNullException(nameof(resourceStringProvider));
+ }
+
+ if (baseName == null)
+ {
+ throw new ArgumentNullException(nameof(baseName));
+ }
+
+ if (resourceNamesCache == null)
+ {
+ throw new ArgumentNullException(nameof(resourceNamesCache));
+ }
+
+ if (logger == null)
+ {
+ throw new ArgumentNullException(nameof(logger));
+ }
+
+ _resourceStringProvider = resourceStringProvider;
+ _resourceManager = resourceManager;
+ _resourceBaseName = baseName;
+ _resourceNamesCache = resourceNamesCache;
+ _logger = logger;
+ }
+
+ ///
+ public virtual LocalizedString this[string name]
+ {
+ get
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ var value = GetStringSafely(name, null);
+
+ return new LocalizedString(name, value ?? name, resourceNotFound: value == null, searchedLocation: _resourceBaseName);
+ }
+ }
+
+ ///
+ public virtual LocalizedString this[string name, params object[] arguments]
+ {
+ get
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ var format = GetStringSafely(name, null);
+ var value = string.Format(format ?? name, arguments);
+
+ return new LocalizedString(name, value, resourceNotFound: format == null, searchedLocation: _resourceBaseName);
+ }
+ }
+
+ ///
+ /// Creates a new for a specific .
+ ///
+ /// The to use.
+ /// A culture-specific .
+ public IStringLocalizer WithCulture(CultureInfo culture)
+ {
+ return culture == null
+ ? new ResourceManagerStringLocalizer(
+ _resourceManager,
+ _resourceStringProvider,
+ _resourceBaseName,
+ _resourceNamesCache,
+ _logger)
+ : new ResourceManagerWithCultureStringLocalizer(
+ _resourceManager,
+ _resourceStringProvider,
+ _resourceBaseName,
+ _resourceNamesCache,
+ culture,
+ _logger);
+ }
+
+ ///
+ public virtual IEnumerable GetAllStrings(bool includeParentCultures) =>
+ GetAllStrings(includeParentCultures, CultureInfo.CurrentUICulture);
+
+ ///
+ /// Returns all strings in the specified culture.
+ ///
+ ///
+ /// The to get strings for.
+ /// The strings.
+ protected IEnumerable GetAllStrings(bool includeParentCultures, CultureInfo culture)
+ {
+ if (culture == null)
+ {
+ throw new ArgumentNullException(nameof(culture));
+ }
+
+ var resourceNames = includeParentCultures
+ ? GetResourceNamesFromCultureHierarchy(culture)
+ : _resourceStringProvider.GetAllResourceStrings(culture, true);
+
+ foreach (var name in resourceNames)
+ {
+ var value = GetStringSafely(name, culture);
+ yield return new LocalizedString(name, value ?? name, resourceNotFound: value == null, searchedLocation: _resourceBaseName);
+ }
+ }
+
+ ///
+ /// Gets a resource string from the and returns null instead of
+ /// throwing exceptions if a match isn't found.
+ ///
+ /// The name of the string resource.
+ /// The to get the string for.
+ /// The resource string, or null if none was found.
+ protected string GetStringSafely(string name, CultureInfo culture)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ var keyCulture = culture ?? CultureInfo.CurrentUICulture;
+
+ var cacheKey = $"name={name}&culture={keyCulture.Name}";
+
+ _logger.SearchedLocation(name, _resourceBaseName, keyCulture);
+
+ if (_missingManifestCache.ContainsKey(cacheKey))
+ {
+ return null;
+ }
+
+ try
+ {
+ return culture == null ? _resourceManager.GetString(name) : _resourceManager.GetString(name, culture);
+ }
+ catch (MissingManifestResourceException)
+ {
+ _missingManifestCache.TryAdd(cacheKey, null);
+ return null;
+ }
+ }
+
+ private IEnumerable GetResourceNamesFromCultureHierarchy(CultureInfo startingCulture)
+ {
+ var currentCulture = startingCulture;
+ var resourceNames = new HashSet();
+
+ var hasAnyCultures = false;
+
+ while (true)
+ {
+
+ var cultureResourceNames = _resourceStringProvider.GetAllResourceStrings(currentCulture, false);
+
+ if (cultureResourceNames != null)
+ {
+ foreach (var resourceName in cultureResourceNames)
+ {
+ resourceNames.Add(resourceName);
+ }
+ hasAnyCultures = true;
+ }
+
+ if (currentCulture == currentCulture.Parent)
+ {
+ // currentCulture begat currentCulture, probably time to leave
+ break;
+ }
+
+ currentCulture = currentCulture.Parent;
+ }
+
+ if (!hasAnyCultures)
+ {
+ throw new MissingManifestResourceException(Resources.Localization_MissingManifest_Parent);
+ }
+
+ return resourceNames;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Localization/Localization/src/ResourceManagerStringLocalizerFactory.cs b/src/Localization/Localization/src/ResourceManagerStringLocalizerFactory.cs
new file mode 100644
index 0000000000..2eb737eaa7
--- /dev/null
+++ b/src/Localization/Localization/src/ResourceManagerStringLocalizerFactory.cs
@@ -0,0 +1,270 @@
+// 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.Collections.Concurrent;
+using System.IO;
+using System.Reflection;
+using System.Resources;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Localization
+{
+ ///
+ /// An that creates instances of .
+ ///
+ ///
+ /// offers multiple ways to set the relative path of
+ /// resources to be used. They are, in order of precedence:
+ /// -> -> the project root.
+ ///
+ public class ResourceManagerStringLocalizerFactory : IStringLocalizerFactory
+ {
+ private readonly IResourceNamesCache _resourceNamesCache = new ResourceNamesCache();
+ private readonly ConcurrentDictionary _localizerCache =
+ new ConcurrentDictionary();
+ private readonly string _resourcesRelativePath;
+ private readonly ILoggerFactory _loggerFactory;
+
+ ///
+ /// Creates a new .
+ ///
+ /// The .
+ /// The .
+ public ResourceManagerStringLocalizerFactory(
+ IOptions localizationOptions,
+ ILoggerFactory loggerFactory)
+ {
+ if (localizationOptions == null)
+ {
+ throw new ArgumentNullException(nameof(localizationOptions));
+ }
+
+ if (loggerFactory == null)
+ {
+ throw new ArgumentNullException(nameof(loggerFactory));
+ }
+
+ _resourcesRelativePath = localizationOptions.Value.ResourcesPath ?? string.Empty;
+ _loggerFactory = loggerFactory;
+
+ if (!string.IsNullOrEmpty(_resourcesRelativePath))
+ {
+ _resourcesRelativePath = _resourcesRelativePath.Replace(Path.AltDirectorySeparatorChar, '.')
+ .Replace(Path.DirectorySeparatorChar, '.') + ".";
+ }
+ }
+
+ ///
+ /// Gets the resource prefix used to look up the resource.
+ ///
+ /// The type of the resource to be looked up.
+ /// The prefix for resource lookup.
+ protected virtual string GetResourcePrefix(TypeInfo typeInfo)
+ {
+ if (typeInfo == null)
+ {
+ throw new ArgumentNullException(nameof(typeInfo));
+ }
+
+ return GetResourcePrefix(typeInfo, GetRootNamespace(typeInfo.Assembly), GetResourcePath(typeInfo.Assembly));
+ }
+
+ ///
+ /// Gets the resource prefix used to look up the resource.
+ ///
+ /// The type of the resource to be looked up.
+ /// The base namespace of the application.
+ /// The folder containing all resources.
+ /// The prefix for resource lookup.
+ ///
+ /// For the type "Sample.Controllers.Home" if there's a resourceRelativePath return
+ /// "Sample.Resourcepath.Controllers.Home" if there isn't one then it would return "Sample.Controllers.Home".
+ ///
+ protected virtual string GetResourcePrefix(TypeInfo typeInfo, string baseNamespace, string resourcesRelativePath)
+ {
+ if (typeInfo == null)
+ {
+ throw new ArgumentNullException(nameof(typeInfo));
+ }
+
+ if (string.IsNullOrEmpty(baseNamespace))
+ {
+ throw new ArgumentNullException(nameof(baseNamespace));
+ }
+
+ if (string.IsNullOrEmpty(resourcesRelativePath))
+ {
+ return typeInfo.FullName;
+ }
+ else
+ {
+ // This expectation is defined by dotnet's automatic resource storage.
+ // We have to conform to "{RootNamespace}.{ResourceLocation}.{FullTypeName - AssemblyName}".
+ var assemblyName = new AssemblyName(typeInfo.Assembly.FullName).Name;
+ return baseNamespace + "." + resourcesRelativePath + TrimPrefix(typeInfo.FullName, assemblyName + ".");
+ }
+ }
+
+ ///
+ /// Gets the resource prefix used to look up the resource.
+ ///
+ /// The name of the resource to be looked up
+ /// The base namespace of the application.
+ /// The prefix for resource lookup.
+ protected virtual string GetResourcePrefix(string baseResourceName, string baseNamespace)
+ {
+ if (string.IsNullOrEmpty(baseResourceName))
+ {
+ throw new ArgumentNullException(nameof(baseResourceName));
+ }
+
+ if (string.IsNullOrEmpty(baseNamespace))
+ {
+ throw new ArgumentNullException(nameof(baseNamespace));
+ }
+
+ var assemblyName = new AssemblyName(baseNamespace);
+ var assembly = Assembly.Load(assemblyName);
+ var rootNamespace = GetRootNamespace(assembly);
+ var resourceLocation = GetResourcePath(assembly);
+ var locationPath = rootNamespace + "." + resourceLocation;
+
+ baseResourceName = locationPath + TrimPrefix(baseResourceName, baseNamespace + ".");
+
+ return baseResourceName;
+ }
+
+ ///
+ /// Creates a using the and
+ /// of the specified .
+ ///
+ /// The .
+ /// The .
+ public IStringLocalizer Create(Type resourceSource)
+ {
+ if (resourceSource == null)
+ {
+ throw new ArgumentNullException(nameof(resourceSource));
+ }
+
+ var typeInfo = resourceSource.GetTypeInfo();
+
+ var baseName = GetResourcePrefix(typeInfo);
+
+ var assembly = typeInfo.Assembly;
+
+ return _localizerCache.GetOrAdd(baseName, _ => CreateResourceManagerStringLocalizer(assembly, baseName));
+ }
+
+ ///
+ /// Creates a .
+ ///
+ /// The base name of the resource to load strings from.
+ /// The location to load resources from.
+ /// The .
+ public IStringLocalizer Create(string baseName, string location)
+ {
+ if (baseName == null)
+ {
+ throw new ArgumentNullException(nameof(baseName));
+ }
+
+ if (location == null)
+ {
+ throw new ArgumentNullException(nameof(location));
+ }
+
+ return _localizerCache.GetOrAdd($"B={baseName},L={location}", _ =>
+ {
+ var assemblyName = new AssemblyName(location);
+ var assembly = Assembly.Load(assemblyName);
+ baseName = GetResourcePrefix(baseName, location);
+
+ return CreateResourceManagerStringLocalizer(assembly, baseName);
+ });
+ }
+
+ /// Creates a for the given input.
+ /// The assembly to create a for.
+ /// The base name of the resource to search for.
+ /// A for the given and .
+ /// This method is virtual for testing purposes only.
+ protected virtual ResourceManagerStringLocalizer CreateResourceManagerStringLocalizer(
+ Assembly assembly,
+ string baseName)
+ {
+ return new ResourceManagerStringLocalizer(
+ new ResourceManager(baseName, assembly),
+ assembly,
+ baseName,
+ _resourceNamesCache,
+ _loggerFactory.CreateLogger());
+ }
+
+ ///
+ /// Gets the resource prefix used to look up the resource.
+ ///
+ /// The general location of the resource.
+ /// The base name of the resource.
+ /// The location of the resource within .
+ /// The resource prefix used to look up the resource.
+ 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 + ".");
+ }
+
+ /// Gets a from the provided .
+ /// The assembly to get a from.
+ /// The associated with the given .
+ /// This method is protected and virtual for testing purposes only.
+ protected virtual ResourceLocationAttribute GetResourceLocationAttribute(Assembly assembly)
+ {
+ return assembly.GetCustomAttribute();
+ }
+
+ /// Gets a from the provided .
+ /// The assembly to get a from.
+ /// The associated with the given .
+ /// This method is protected and virtual for testing purposes only.
+ protected virtual RootNamespaceAttribute GetRootNamespaceAttribute(Assembly assembly)
+ {
+ return assembly.GetCustomAttribute();
+ }
+
+ private string GetRootNamespace(Assembly assembly)
+ {
+ var rootNamespaceAttribute = GetRootNamespaceAttribute(assembly);
+
+ return rootNamespaceAttribute?.RootNamespace ??
+ new AssemblyName(assembly.FullName).Name;
+ }
+
+ 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))
+ {
+ return name.Substring(prefix.Length);
+ }
+
+ return name;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Localization/Localization/src/ResourceManagerWithCultureStringLocalizer.cs b/src/Localization/Localization/src/ResourceManagerWithCultureStringLocalizer.cs
new file mode 100644
index 0000000000..65b6ae242c
--- /dev/null
+++ b/src/Localization/Localization/src/ResourceManagerWithCultureStringLocalizer.cs
@@ -0,0 +1,164 @@
+// 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.Collections.Generic;
+using System.Globalization;
+using System.Reflection;
+using System.Resources;
+using Microsoft.Extensions.Localization.Internal;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Extensions.Localization
+{
+ ///
+ /// An that uses the and
+ /// to provide localized strings for a specific .
+ ///
+ public class ResourceManagerWithCultureStringLocalizer : ResourceManagerStringLocalizer
+ {
+ private readonly string _resourceBaseName;
+ private readonly CultureInfo _culture;
+
+ ///
+ /// Creates a new .
+ ///
+ /// The to read strings from.
+ /// The that can find the resources.
+ /// The base name of the embedded resource that contains the strings.
+ /// Cache of the list of strings for a given resource assembly name.
+ /// The specific to use.
+ /// The .
+ internal ResourceManagerWithCultureStringLocalizer(
+ ResourceManager resourceManager,
+ IResourceStringProvider resourceStringProvider,
+ string baseName,
+ IResourceNamesCache resourceNamesCache,
+ CultureInfo culture,
+ ILogger logger)
+ : base(resourceManager, resourceStringProvider, baseName, resourceNamesCache, logger)
+ {
+ if (resourceManager == null)
+ {
+ throw new ArgumentNullException(nameof(resourceManager));
+ }
+
+ if (resourceStringProvider == null)
+ {
+ throw new ArgumentNullException(nameof(resourceStringProvider));
+ }
+
+ if (baseName == null)
+ {
+ throw new ArgumentNullException(nameof(baseName));
+ }
+
+ if (resourceNamesCache == null)
+ {
+ throw new ArgumentNullException(nameof(resourceNamesCache));
+ }
+
+ if (culture == null)
+ {
+ throw new ArgumentNullException(nameof(culture));
+ }
+
+ if (logger == null)
+ {
+ throw new ArgumentNullException(nameof(logger));
+ }
+
+ _resourceBaseName = baseName;
+ _culture = culture;
+ }
+
+ ///
+ /// Creates a new .
+ ///
+ /// The to read strings from.
+ /// The that contains the strings as embedded resources.
+ /// The base name of the embedded resource that contains the strings.
+ /// Cache of the list of strings for a given resource assembly name.
+ /// The specific to use.
+ /// The .
+ public ResourceManagerWithCultureStringLocalizer(
+ ResourceManager resourceManager,
+ Assembly resourceAssembly,
+ string baseName,
+ IResourceNamesCache resourceNamesCache,
+ CultureInfo culture,
+ ILogger logger)
+ : base(resourceManager, resourceAssembly, baseName, resourceNamesCache, logger)
+ {
+ if (resourceManager == null)
+ {
+ throw new ArgumentNullException(nameof(resourceManager));
+ }
+
+ if (resourceAssembly == null)
+ {
+ throw new ArgumentNullException(nameof(resourceAssembly));
+ }
+
+ if (baseName == null)
+ {
+ throw new ArgumentNullException(nameof(baseName));
+ }
+
+ if (resourceNamesCache == null)
+ {
+ throw new ArgumentNullException(nameof(resourceNamesCache));
+ }
+
+ if (culture == null)
+ {
+ throw new ArgumentNullException(nameof(culture));
+ }
+
+ if (logger == null)
+ {
+ throw new ArgumentNullException(nameof(logger));
+ }
+
+ _resourceBaseName = baseName;
+ _culture = culture;
+ }
+
+ ///
+ public override LocalizedString this[string name]
+ {
+ get
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ var value = GetStringSafely(name, _culture);
+
+ return new LocalizedString(name, value ?? name, resourceNotFound: value == null, searchedLocation: _resourceBaseName);
+ }
+ }
+
+ ///
+ public override LocalizedString this[string name, params object[] arguments]
+ {
+ get
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ var format = GetStringSafely(name, _culture);
+ var value = string.Format(_culture, format ?? name, arguments);
+
+ return new LocalizedString(name, value, resourceNotFound: format == null, searchedLocation: _resourceBaseName);
+ }
+ }
+
+ ///
+ public override IEnumerable GetAllStrings(bool includeParentCultures) =>
+ GetAllStrings(includeParentCultures, _culture);
+ }
+}
\ No newline at end of file
diff --git a/src/Localization/Localization/src/ResourceNamesCache.cs b/src/Localization/Localization/src/ResourceNamesCache.cs
new file mode 100644
index 0000000000..86e94c102a
--- /dev/null
+++ b/src/Localization/Localization/src/ResourceNamesCache.cs
@@ -0,0 +1,23 @@
+// 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.Collections.Concurrent;
+using System.Collections.Generic;
+
+namespace Microsoft.Extensions.Localization
+{
+ ///
+ /// An implementation of backed by a .
+ ///
+ public class ResourceNamesCache : IResourceNamesCache
+ {
+ private readonly ConcurrentDictionary> _cache = new ConcurrentDictionary>();
+
+ ///
+ public IList GetOrAdd(string name, Func> valueFactory)
+ {
+ return _cache.GetOrAdd(name, valueFactory);
+ }
+ }
+}
diff --git a/src/Localization/Localization/src/Resources.resx b/src/Localization/Localization/src/Resources.resx
new file mode 100644
index 0000000000..b679f04664
--- /dev/null
+++ b/src/Localization/Localization/src/Resources.resx
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ The manifest '{0}' was not found.
+
+
+ No manifests exist for the current culture.
+
+
\ No newline at end of file
diff --git a/src/Localization/Localization/src/RootNamespaceAttribute.cs b/src/Localization/Localization/src/RootNamespaceAttribute.cs
new file mode 100644
index 0000000000..f28b4ea1fd
--- /dev/null
+++ b/src/Localization/Localization/src/RootNamespaceAttribute.cs
@@ -0,0 +1,35 @@
+// 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
+{
+ ///
+ /// Provides the RootNamespace of an Assembly. The RootNamespace of the assembly is used by Localization to
+ /// determine the resource name to look for when RootNamespace differs from the AssemblyName.
+ ///
+ [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)]
+ public class RootNamespaceAttribute : Attribute
+ {
+ ///
+ /// Creates a new .
+ ///
+ /// The RootNamespace for this Assembly.
+ public RootNamespaceAttribute(string rootNamespace)
+ {
+ if (string.IsNullOrEmpty(rootNamespace))
+ {
+ throw new ArgumentNullException(nameof(rootNamespace));
+ }
+
+ RootNamespace = rootNamespace;
+ }
+
+ ///
+ /// The RootNamespace of this Assembly. The RootNamespace of the assembly is used by Localization to
+ /// determine the resource name to look for when RootNamespace differs from the AssemblyName.
+ ///
+ public string RootNamespace { get; }
+ }
+}
diff --git a/src/Localization/Localization/src/baseline.netcore.json b/src/Localization/Localization/src/baseline.netcore.json
new file mode 100644
index 0000000000..860db76899
--- /dev/null
+++ b/src/Localization/Localization/src/baseline.netcore.json
@@ -0,0 +1,687 @@
+{
+ "AssemblyIdentity": "Microsoft.Extensions.Localization, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
+ "Types": [
+ {
+ "Name": "Microsoft.Extensions.DependencyInjection.LocalizationServiceCollectionExtensions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "Abstract": true,
+ "Static": true,
+ "Sealed": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "AddLocalization",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "AddLocalization",
+ "Parameters": [
+ {
+ "Name": "services",
+ "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
+ },
+ {
+ "Name": "setupAction",
+ "Type": "System.Action"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection",
+ "Static": true,
+ "Extension": true,
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.Extensions.Localization.IResourceNamesCache",
+ "Visibility": "Public",
+ "Kind": "Interface",
+ "Abstract": true,
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GetOrAdd",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "valueFactory",
+ "Type": "System.Func>"
+ }
+ ],
+ "ReturnType": "System.Collections.Generic.IList",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.Extensions.Localization.LocalizationOptions",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ResourcesPath",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "set_ResourcesPath",
+ "Parameters": [
+ {
+ "Name": "value",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.Void",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.Extensions.Localization.ResourceLocationAttribute",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "System.Attribute",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_ResourceLocation",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "resourceLocation",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.Extensions.Localization.ResourceManagerStringLocalizer",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.Extensions.Localization.IStringLocalizer"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Item",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.Localization.LocalizedString",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizer",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Item",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "arguments",
+ "Type": "System.Object[]",
+ "IsParams": true
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.Localization.LocalizedString",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizer",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "WithCulture",
+ "Parameters": [
+ {
+ "Name": "culture",
+ "Type": "System.Globalization.CultureInfo"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.Localization.IStringLocalizer",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizer",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetAllStrings",
+ "Parameters": [
+ {
+ "Name": "includeParentCultures",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Collections.Generic.IEnumerable",
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizer",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetAllStrings",
+ "Parameters": [
+ {
+ "Name": "includeParentCultures",
+ "Type": "System.Boolean"
+ },
+ {
+ "Name": "culture",
+ "Type": "System.Globalization.CultureInfo"
+ }
+ ],
+ "ReturnType": "System.Collections.Generic.IEnumerable",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetStringSafely",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "culture",
+ "Type": "System.Globalization.CultureInfo"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "resourceManager",
+ "Type": "System.Resources.ResourceManager"
+ },
+ {
+ "Name": "resourceAssembly",
+ "Type": "System.Reflection.Assembly"
+ },
+ {
+ "Name": "baseName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "resourceNamesCache",
+ "Type": "Microsoft.Extensions.Localization.IResourceNamesCache"
+ },
+ {
+ "Name": "logger",
+ "Type": "Microsoft.Extensions.Logging.ILogger"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "resourceManager",
+ "Type": "System.Resources.ResourceManager"
+ },
+ {
+ "Name": "resourceAssemblyWrapper",
+ "Type": "Microsoft.Extensions.Localization.Internal.AssemblyWrapper"
+ },
+ {
+ "Name": "baseName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "resourceNamesCache",
+ "Type": "Microsoft.Extensions.Localization.IResourceNamesCache"
+ },
+ {
+ "Name": "logger",
+ "Type": "Microsoft.Extensions.Logging.ILogger"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "resourceManager",
+ "Type": "System.Resources.ResourceManager"
+ },
+ {
+ "Name": "resourceStringProvider",
+ "Type": "Microsoft.Extensions.Localization.Internal.IResourceStringProvider"
+ },
+ {
+ "Name": "baseName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "resourceNamesCache",
+ "Type": "Microsoft.Extensions.Localization.IResourceNamesCache"
+ },
+ {
+ "Name": "logger",
+ "Type": "Microsoft.Extensions.Logging.ILogger"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.Extensions.Localization.ResourceManagerStringLocalizerFactory",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.Extensions.Localization.IStringLocalizerFactory"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GetResourcePrefix",
+ "Parameters": [
+ {
+ "Name": "typeInfo",
+ "Type": "System.Reflection.TypeInfo"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetResourcePrefix",
+ "Parameters": [
+ {
+ "Name": "typeInfo",
+ "Type": "System.Reflection.TypeInfo"
+ },
+ {
+ "Name": "baseNamespace",
+ "Type": "System.String"
+ },
+ {
+ "Name": "resourcesRelativePath",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetResourcePrefix",
+ "Parameters": [
+ {
+ "Name": "baseResourceName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "baseNamespace",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Create",
+ "Parameters": [
+ {
+ "Name": "resourceSource",
+ "Type": "System.Type"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.Localization.IStringLocalizer",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizerFactory",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "Create",
+ "Parameters": [
+ {
+ "Name": "baseName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "location",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.Localization.IStringLocalizer",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizerFactory",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "CreateResourceManagerStringLocalizer",
+ "Parameters": [
+ {
+ "Name": "assembly",
+ "Type": "System.Reflection.Assembly"
+ },
+ {
+ "Name": "baseName",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.Localization.ResourceManagerStringLocalizer",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetResourcePrefix",
+ "Parameters": [
+ {
+ "Name": "location",
+ "Type": "System.String"
+ },
+ {
+ "Name": "baseName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "resourceLocation",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "System.String",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetResourceLocationAttribute",
+ "Parameters": [
+ {
+ "Name": "assembly",
+ "Type": "System.Reflection.Assembly"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.Localization.ResourceLocationAttribute",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetRootNamespaceAttribute",
+ "Parameters": [
+ {
+ "Name": "assembly",
+ "Type": "System.Reflection.Assembly"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.Localization.RootNamespaceAttribute",
+ "Virtual": true,
+ "Visibility": "Protected",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "localizationOptions",
+ "Type": "Microsoft.Extensions.Options.IOptions"
+ },
+ {
+ "Name": "loggerFactory",
+ "Type": "Microsoft.Extensions.Logging.ILoggerFactory"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.Extensions.Localization.ResourceManagerWithCultureStringLocalizer",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "Microsoft.Extensions.Localization.ResourceManagerStringLocalizer",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_Item",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.Localization.LocalizedString",
+ "Virtual": true,
+ "Override": true,
+ "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizer",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "get_Item",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "arguments",
+ "Type": "System.Object[]",
+ "IsParams": true
+ }
+ ],
+ "ReturnType": "Microsoft.Extensions.Localization.LocalizedString",
+ "Virtual": true,
+ "Override": true,
+ "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizer",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Method",
+ "Name": "GetAllStrings",
+ "Parameters": [
+ {
+ "Name": "includeParentCultures",
+ "Type": "System.Boolean"
+ }
+ ],
+ "ReturnType": "System.Collections.Generic.IEnumerable",
+ "Virtual": true,
+ "Override": true,
+ "ImplementedInterface": "Microsoft.Extensions.Localization.IStringLocalizer",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "resourceManager",
+ "Type": "System.Resources.ResourceManager"
+ },
+ {
+ "Name": "resourceAssembly",
+ "Type": "System.Reflection.Assembly"
+ },
+ {
+ "Name": "baseName",
+ "Type": "System.String"
+ },
+ {
+ "Name": "resourceNamesCache",
+ "Type": "Microsoft.Extensions.Localization.IResourceNamesCache"
+ },
+ {
+ "Name": "culture",
+ "Type": "System.Globalization.CultureInfo"
+ },
+ {
+ "Name": "logger",
+ "Type": "Microsoft.Extensions.Logging.ILogger"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.Extensions.Localization.ResourceNamesCache",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "ImplementedInterfaces": [
+ "Microsoft.Extensions.Localization.IResourceNamesCache"
+ ],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "GetOrAdd",
+ "Parameters": [
+ {
+ "Name": "name",
+ "Type": "System.String"
+ },
+ {
+ "Name": "valueFactory",
+ "Type": "System.Func>"
+ }
+ ],
+ "ReturnType": "System.Collections.Generic.IList",
+ "Sealed": true,
+ "Virtual": true,
+ "ImplementedInterface": "Microsoft.Extensions.Localization.IResourceNamesCache",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ },
+ {
+ "Name": "Microsoft.Extensions.Localization.RootNamespaceAttribute",
+ "Visibility": "Public",
+ "Kind": "Class",
+ "BaseType": "System.Attribute",
+ "ImplementedInterfaces": [],
+ "Members": [
+ {
+ "Kind": "Method",
+ "Name": "get_RootNamespace",
+ "Parameters": [],
+ "ReturnType": "System.String",
+ "Visibility": "Public",
+ "GenericParameter": []
+ },
+ {
+ "Kind": "Constructor",
+ "Name": ".ctor",
+ "Parameters": [
+ {
+ "Name": "rootNamespace",
+ "Type": "System.String"
+ }
+ ],
+ "Visibility": "Public",
+ "GenericParameter": []
+ }
+ ],
+ "GenericParameters": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/Localization/Localization/test/LocalizationServiceCollectionExtensionsTest.cs b/src/Localization/Localization/test/LocalizationServiceCollectionExtensionsTest.cs
new file mode 100644
index 0000000000..d78581655c
--- /dev/null
+++ b/src/Localization/Localization/test/LocalizationServiceCollectionExtensionsTest.cs
@@ -0,0 +1,68 @@
+// 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.Linq;
+using Microsoft.Extensions.Localization;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ public class LocalizationServiceCollectionExtensionsTest
+ {
+ [Fact]
+ public void AddLocalization_AddsNeededServices()
+ {
+ // Arrange
+ var collection = new ServiceCollection();
+
+ // Act
+ LocalizationServiceCollectionExtensions.AddLocalizationServices(collection);
+
+ // Assert
+ AssertContainsSingle(collection, typeof(IStringLocalizerFactory), typeof(ResourceManagerStringLocalizerFactory));
+ AssertContainsSingle(collection, typeof(IStringLocalizer<>), typeof(StringLocalizer<>));
+ }
+
+ [Fact]
+ public void AddLocalizationWithLocalizationOptions_AddsNeededServices()
+ {
+ // Arrange
+ var collection = new ServiceCollection();
+
+ // Act
+ LocalizationServiceCollectionExtensions.AddLocalizationServices(
+ collection,
+ options => options.ResourcesPath = "Resources");
+
+ AssertContainsSingle(collection, typeof(IStringLocalizerFactory), typeof(ResourceManagerStringLocalizerFactory));
+ AssertContainsSingle(collection, typeof(IStringLocalizer<>), typeof(StringLocalizer<>));
+ }
+
+ private void AssertContainsSingle(
+ IServiceCollection services,
+ Type serviceType,
+ Type implementationType)
+ {
+ var matches = services
+ .Where(sd =>
+ sd.ServiceType == serviceType &&
+ sd.ImplementationType == implementationType)
+ .ToArray();
+
+ if (matches.Length == 0)
+ {
+ Assert.True(
+ false,
+ $"Could not find an instance of {implementationType} registered as {serviceType}");
+ }
+ else if (matches.Length > 1)
+ {
+ Assert.True(
+ false,
+ $"Found multiple instances of {implementationType} registered as {serviceType}");
+ }
+ }
+ }
+}
diff --git a/src/Localization/Localization/test/Microsoft.Extensions.Localization.Tests.csproj b/src/Localization/Localization/test/Microsoft.Extensions.Localization.Tests.csproj
new file mode 100644
index 0000000000..e7a47a1a6e
--- /dev/null
+++ b/src/Localization/Localization/test/Microsoft.Extensions.Localization.Tests.csproj
@@ -0,0 +1,13 @@
+
+
+
+ $(StandardTestTfms)
+
+
+
+
+
+
+
+
+
diff --git a/src/Localization/Localization/test/ResourceManagerStringLocalizerFactoryTest.cs b/src/Localization/Localization/test/ResourceManagerStringLocalizerFactoryTest.cs
new file mode 100644
index 0000000000..7a18c0e4bd
--- /dev/null
+++ b/src/Localization/Localization/test/ResourceManagerStringLocalizerFactoryTest.cs
@@ -0,0 +1,296 @@
+// 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.IO;
+using System.Reflection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+// This namespace intentionally matches the default assembly namespace.
+namespace Microsoft.Extensions.Localization.Tests
+{
+ public class TestResourceManagerStringLocalizerFactory : ResourceManagerStringLocalizerFactory
+ {
+ private ResourceLocationAttribute _resourceLocationAttribute;
+
+ private RootNamespaceAttribute _rootNamespaceAttribute;
+
+ public Assembly Assembly { get; private set; }
+ public string BaseName { get; private set; }
+
+ public TestResourceManagerStringLocalizerFactory(
+ IOptions localizationOptions,
+ ResourceLocationAttribute resourceLocationAttribute,
+ RootNamespaceAttribute rootNamespaceAttribute,
+ ILoggerFactory loggerFactory)
+ : base(localizationOptions, loggerFactory)
+ {
+ _resourceLocationAttribute = resourceLocationAttribute;
+ _rootNamespaceAttribute = rootNamespaceAttribute;
+ }
+
+ protected override ResourceLocationAttribute GetResourceLocationAttribute(Assembly assembly)
+ {
+ return _resourceLocationAttribute;
+ }
+
+ protected override RootNamespaceAttribute GetRootNamespaceAttribute(Assembly assembly)
+ {
+ return _rootNamespaceAttribute;
+ }
+
+ 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 locOptions = new LocalizationOptions();
+ var options = new Mock>();
+ options.Setup(o => o.Value).Returns(locOptions);
+
+ var resourceLocationAttribute = new ResourceLocationAttribute(Path.Combine("My", "Resources"));
+ var loggerFactory = NullLoggerFactory.Instance;
+ var typeFactory = new TestResourceManagerStringLocalizerFactory(
+ options.Object,
+ resourceLocationAttribute,
+ rootNamespaceAttribute: null,
+ loggerFactory: loggerFactory);
+ var stringFactory = new TestResourceManagerStringLocalizerFactory(
+ options.Object,
+ resourceLocationAttribute,
+ rootNamespaceAttribute: null,
+ loggerFactory: loggerFactory);
+ 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()
+ {
+ // Arrange
+ var locOptions = new LocalizationOptions();
+ var options = new Mock>();
+ options.Setup(o => o.Value).Returns(locOptions);
+ var loggerFactory = NullLoggerFactory.Instance;
+ var factory = new ResourceManagerStringLocalizerFactory(localizationOptions: options.Object, loggerFactory: loggerFactory);
+
+ // Act
+ var result1 = factory.Create(typeof(ResourceManagerStringLocalizerFactoryTest));
+ var result2 = factory.Create(typeof(ResourceManagerStringLocalizerFactoryTest));
+
+ // Assert
+ Assert.Same(result1, result2);
+ }
+
+ [Fact]
+ public void Create_FromType_ReturnsNewResultForDifferentType()
+ {
+ // Arrange
+ var locOptions = new LocalizationOptions();
+ var options = new Mock>();
+ options.Setup(o => o.Value).Returns(locOptions);
+ var loggerFactory = NullLoggerFactory.Instance;
+ var factory = new ResourceManagerStringLocalizerFactory(localizationOptions: options.Object, loggerFactory: loggerFactory);
+
+ // Act
+ var result1 = factory.Create(typeof(ResourceManagerStringLocalizerFactoryTest));
+ var result2 = factory.Create(typeof(LocalizationOptions));
+
+ // Assert
+ Assert.NotSame(result1, result2);
+ }
+
+ [Fact]
+ public void Create_ResourceLocationAttribute_RootNamespaceIgnoredWhenNoLocation()
+ {
+ // Arrange
+ var locOptions = new LocalizationOptions();
+ var options = new Mock>();
+ options.Setup(o => o.Value).Returns(locOptions);
+ var loggerFactory = NullLoggerFactory.Instance;
+
+ var resourcePath = Path.Combine("My", "Resources");
+ var rootNamespace = "MyNamespace";
+ var rootNamespaceAttribute = new RootNamespaceAttribute(rootNamespace);
+
+ var typeFactory = new TestResourceManagerStringLocalizerFactory(
+ options.Object,
+ resourceLocationAttribute: null,
+ rootNamespaceAttribute: rootNamespaceAttribute,
+ loggerFactory: loggerFactory);
+
+ var type = typeof(ResourceManagerStringLocalizerFactoryTest);
+ // Act
+ typeFactory.Create(type);
+
+ // Assert
+ Assert.Equal($"Microsoft.Extensions.Localization.Tests.ResourceManagerStringLocalizerFactoryTest", typeFactory.BaseName);
+ }
+
+ [Fact]
+ public void Create_ResourceLocationAttribute_UsesRootNamespace()
+ {
+ // Arrange
+ var locOptions = new LocalizationOptions();
+ var options = new Mock>();
+ options.Setup(o => o.Value).Returns(locOptions);
+ var loggerFactory = NullLoggerFactory.Instance;
+
+ var resourcePath = Path.Combine("My", "Resources");
+ var rootNamespace = "MyNamespace";
+ var resourceLocationAttribute = new ResourceLocationAttribute(resourcePath);
+ var rootNamespaceAttribute = new RootNamespaceAttribute(rootNamespace);
+
+ var typeFactory = new TestResourceManagerStringLocalizerFactory(
+ options.Object,
+ resourceLocationAttribute,
+ rootNamespaceAttribute,
+ loggerFactory);
+
+ var type = typeof(ResourceManagerStringLocalizerFactoryTest);
+ // Act
+ typeFactory.Create(type);
+
+ // Assert
+ Assert.Equal($"MyNamespace.My.Resources.ResourceManagerStringLocalizerFactoryTest", typeFactory.BaseName);
+ }
+
+ [Fact]
+ public void Create_FromType_ResourcesPathDirectorySeperatorToDot()
+ {
+ // Arrange
+ var locOptions = new LocalizationOptions();
+ locOptions.ResourcesPath = Path.Combine("My", "Resources");
+ var options = new Mock>();
+ options.Setup(o => o.Value).Returns(locOptions);
+ var loggerFactory = NullLoggerFactory.Instance;
+ var factory = new TestResourceManagerStringLocalizerFactory(
+ options.Object,
+ resourceLocationAttribute: null,
+ rootNamespaceAttribute: null,
+ loggerFactory: loggerFactory);
+
+ // Act
+ factory.Create(typeof(ResourceManagerStringLocalizerFactoryTest));
+
+ // Assert
+ Assert.Equal("Microsoft.Extensions.Localization.Tests.My.Resources." + nameof(ResourceManagerStringLocalizerFactoryTest), factory.BaseName);
+ }
+
+ [Fact]
+ public void Create_FromNameLocation_ReturnsCachedResultForSameNameLocation()
+ {
+ // Arrange
+ var locOptions = new LocalizationOptions();
+ var options = new Mock>();
+ options.Setup(o => o.Value).Returns(locOptions);
+ var loggerFactory = NullLoggerFactory.Instance;
+ var factory = new ResourceManagerStringLocalizerFactory(localizationOptions: options.Object, loggerFactory: loggerFactory);
+ var location = typeof(ResourceManagerStringLocalizer).GetTypeInfo().Assembly.FullName;
+
+ // Act
+ var result1 = factory.Create("baseName", location);
+ var result2 = factory.Create("baseName", location);
+
+ // Assert
+ Assert.Same(result1, result2);
+ }
+
+ [Fact]
+ public void Create_FromNameLocation_ReturnsNewResultForDifferentName()
+ {
+ // Arrange
+ var locOptions = new LocalizationOptions();
+ var options = new Mock>();
+ options.Setup(o => o.Value).Returns(locOptions);
+ var loggerFactory = NullLoggerFactory.Instance;
+ var factory = new ResourceManagerStringLocalizerFactory(localizationOptions: options.Object, loggerFactory: loggerFactory);
+ var location = typeof(ResourceManagerStringLocalizer).GetTypeInfo().Assembly.FullName;
+
+ // Act
+ var result1 = factory.Create("baseName1", location);
+ var result2 = factory.Create("baseName2", location);
+
+ // Assert
+ Assert.NotSame(result1, result2);
+ }
+
+ [Fact]
+ public void Create_FromNameLocation_ReturnsNewResultForDifferentLocation()
+ {
+ // Arrange
+ var locOptions = new LocalizationOptions();
+ var options = new Mock>();
+ options.Setup(o => o.Value).Returns(locOptions);
+ var loggerFactory = NullLoggerFactory.Instance;
+ var factory = new ResourceManagerStringLocalizerFactory(localizationOptions: options.Object, loggerFactory: loggerFactory);
+ 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);
+ var result2 = factory.Create("baseName", location2);
+
+ // Assert
+ Assert.NotSame(result1, result2);
+ }
+
+ [Fact]
+ public void Create_FromNameLocation_ResourcesPathDirectorySeparatorToDot()
+ {
+ // Arrange
+ var locOptions = new LocalizationOptions();
+ locOptions.ResourcesPath = Path.Combine("My", "Resources");
+ var options = new Mock>();
+ options.Setup(o => o.Value).Returns(locOptions);
+ var loggerFactory = NullLoggerFactory.Instance;
+ var factory = new TestResourceManagerStringLocalizerFactory(
+ options.Object,
+ resourceLocationAttribute: null,
+ rootNamespaceAttribute: null,
+ loggerFactory: loggerFactory);
+
+ // Act
+ var result1 = factory.Create("baseName", location: "Microsoft.Extensions.Localization.Tests");
+
+ // Assert
+ Assert.Equal("Microsoft.Extensions.Localization.Tests.My.Resources.baseName", factory.BaseName);
+ }
+
+ [Fact]
+ public void Create_FromNameLocation_NullLocationThrows()
+ {
+ // Arrange
+ var locOptions = new LocalizationOptions();
+ var options = new Mock>();
+ options.Setup(o => o.Value).Returns(locOptions);
+ var loggerFactory = NullLoggerFactory.Instance;
+ var factory = new ResourceManagerStringLocalizerFactory(localizationOptions: options.Object, loggerFactory: loggerFactory);
+
+ // Act & Assert
+ Assert.Throws(() => factory.Create("baseName", location: null));
+ }
+ }
+}
diff --git a/src/Localization/Localization/test/ResourceManagerStringLocalizerTest.cs b/src/Localization/Localization/test/ResourceManagerStringLocalizerTest.cs
new file mode 100644
index 0000000000..ff7bfa9933
--- /dev/null
+++ b/src/Localization/Localization/test/ResourceManagerStringLocalizerTest.cs
@@ -0,0 +1,299 @@
+// 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.IO;
+using System.Linq;
+using System.Reflection;
+using System.Resources;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.Localization.Internal;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Testing;
+using Xunit;
+
+namespace Microsoft.Extensions.Localization
+{
+ public class ResourceManagerStringLocalizerTest
+ {
+ [Fact]
+ public void EnumeratorCachesCultureWalkForSameAssembly()
+ {
+ // Arrange
+ var resourceNamesCache = new ResourceNamesCache();
+ var baseName = "test";
+ var resourceAssembly = new TestAssemblyWrapper();
+ var resourceManager = new TestResourceManager(baseName, resourceAssembly);
+ var resourceStreamManager = new TestResourceStringProvider(
+ resourceNamesCache,
+ resourceManager,
+ resourceAssembly.Assembly,
+ baseName);
+ var logger = Logger;
+ var localizer1 = new ResourceManagerStringLocalizer(resourceManager,
+ resourceStreamManager,
+ baseName,
+ resourceNamesCache,
+ logger);
+ var localizer2 = new ResourceManagerStringLocalizer(resourceManager,
+ resourceStreamManager,
+ baseName,
+ resourceNamesCache,
+ logger);
+
+ // Act
+ for (var i = 0; i < 5; i++)
+ {
+ localizer1.GetAllStrings().ToList();
+ localizer2.GetAllStrings().ToList();
+ }
+
+ // Assert
+ var expectedCallCount = GetCultureInfoDepth(CultureInfo.CurrentUICulture);
+ Assert.Equal(expectedCallCount, resourceAssembly.ManifestResourceStreamCallCount);
+ }
+
+ [Fact]
+ public void EnumeratorCacheIsScopedByAssembly()
+ {
+ // Arrange
+ var resourceNamesCache = new ResourceNamesCache();
+ var baseName = "test";
+ var resourceAssembly1 = new TestAssemblyWrapper(typeof(ResourceManagerStringLocalizerTest));
+ var resourceAssembly2 = new TestAssemblyWrapper(typeof(ResourceManagerStringLocalizer));
+ var resourceManager1 = new TestResourceManager(baseName, resourceAssembly1);
+ var resourceManager2 = new TestResourceManager(baseName, resourceAssembly2);
+ var resourceStreamManager1 = new TestResourceStringProvider(resourceNamesCache, resourceManager1, resourceAssembly1.Assembly, baseName);
+ var resourceStreamManager2 = new TestResourceStringProvider(resourceNamesCache, resourceManager2, resourceAssembly2.Assembly, baseName);
+ var logger = Logger;
+ var localizer1 = new ResourceManagerStringLocalizer(
+ resourceManager1,
+ resourceStreamManager1,
+ baseName,
+ resourceNamesCache,
+ logger);
+ var localizer2 = new ResourceManagerStringLocalizer(
+ resourceManager2,
+ resourceStreamManager2,
+ baseName,
+ resourceNamesCache,
+ logger);
+
+ // Act
+ localizer1.GetAllStrings().ToList();
+ localizer2.GetAllStrings().ToList();
+
+ // Assert
+ var expectedCallCount = GetCultureInfoDepth(CultureInfo.CurrentUICulture);
+ Assert.Equal(expectedCallCount, resourceAssembly1.ManifestResourceStreamCallCount);
+ Assert.Equal(expectedCallCount, resourceAssembly2.ManifestResourceStreamCallCount);
+ }
+
+ [Fact]
+ public void GetString_PopulatesSearchedLocationOnLocalizedString()
+ {
+ // Arrange
+ var baseName = "Resources.TestResource";
+ var resourceNamesCache = new ResourceNamesCache();
+ var resourceAssembly = new TestAssemblyWrapper();
+ var resourceManager = new TestResourceManager(baseName, resourceAssembly);
+ var resourceStreamManager = new TestResourceStringProvider(resourceNamesCache, resourceManager, resourceAssembly.Assembly, baseName);
+ var logger = Logger;
+ var localizer = new ResourceManagerStringLocalizer(
+ resourceManager,
+ resourceStreamManager,
+ baseName,
+ resourceNamesCache,
+ logger);
+
+ // Act
+ var value = localizer["name"];
+
+ // Assert
+ Assert.Equal("Resources.TestResource", value.SearchedLocation);
+ }
+
+ [Fact]
+ [ReplaceCulture("en-US", "en-US")]
+ public void GetString_LogsLocationSearched()
+ {
+ // Arrange
+ var baseName = "Resources.TestResource";
+ var resourceNamesCache = new ResourceNamesCache();
+ var resourceAssembly = new TestAssemblyWrapper();
+ var resourceManager = new TestResourceManager(baseName, resourceAssembly);
+ var resourceStreamManager = new TestResourceStringProvider(resourceNamesCache, resourceManager, resourceAssembly.Assembly, baseName);
+ var logger = Logger;
+
+ var localizer = new ResourceManagerStringLocalizer(
+ resourceManager,
+ resourceStreamManager,
+ baseName,
+ resourceNamesCache,
+ logger);
+
+ // Act
+ var value = localizer["a key!"];
+
+ // Assert
+ var write = Assert.Single(Sink.Writes);
+ Assert.Equal("ResourceManagerStringLocalizer searched for 'a key!' in 'Resources.TestResource' with culture 'en-US'.", write.State.ToString());
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ResourceManagerStringLocalizer_GetAllStrings_ReturnsExpectedValue(bool includeParentCultures)
+ {
+ // Arrange
+ var baseName = "test";
+ var resourceNamesCache = new ResourceNamesCache();
+ var resourceAssembly = new TestAssemblyWrapper();
+ var resourceManager = new TestResourceManager(baseName, resourceAssembly);
+ var resourceStreamManager = new TestResourceStringProvider(resourceNamesCache, resourceManager, resourceAssembly.Assembly, baseName);
+ var logger = Logger;
+ var localizer = new ResourceManagerStringLocalizer(
+ resourceManager,
+ resourceStreamManager,
+ baseName,
+ resourceNamesCache,
+ logger);
+
+ // Act
+ // We have to access the result so it evaluates.
+ var strings = localizer.GetAllStrings(includeParentCultures).ToList();
+
+ // Assert
+ var value = Assert.Single(strings);
+ Assert.Equal("TestName", value.Value);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ResourceManagerStringLocalizer_GetAllStrings_MissingResourceThrows(bool includeParentCultures)
+ {
+ // Arrange
+ var resourceNamesCache = new ResourceNamesCache();
+ var baseName = "testington";
+ var resourceAssembly = new TestAssemblyWrapper();
+ resourceAssembly.HasResources = false;
+ var resourceManager = new TestResourceManager(baseName, resourceAssembly);
+ var logger = Logger;
+
+ var localizer = new ResourceManagerWithCultureStringLocalizer(
+ resourceManager,
+ resourceAssembly.Assembly,
+ baseName,
+ resourceNamesCache,
+ CultureInfo.CurrentCulture,
+ logger);
+
+ // Act & Assert
+ var exception = Assert.Throws(() =>
+ {
+ // We have to access the result so it evaluates.
+ localizer.GetAllStrings(includeParentCultures).ToArray();
+ });
+
+ var expectedTries = includeParentCultures ? 3 : 1;
+ var expected = includeParentCultures
+ ? "No manifests exist for the current culture."
+ : $"The manifest 'testington.{CultureInfo.CurrentCulture}.resources' was not found.";
+ Assert.Equal(expected, exception.Message);
+ Assert.Equal(expectedTries, resourceAssembly.ManifestResourceStreamCallCount);
+ }
+
+ private static Stream MakeResourceStream()
+ {
+ var stream = new MemoryStream();
+ var resourceWriter = new ResourceWriter(stream);
+ resourceWriter.AddResource("TestName", "value");
+ resourceWriter.Generate();
+ stream.Position = 0;
+ return stream;
+ }
+
+ private static int GetCultureInfoDepth(CultureInfo culture)
+ {
+ var result = 0;
+ var currentCulture = culture;
+
+ while (true)
+ {
+ result++;
+
+ if (currentCulture == currentCulture.Parent)
+ {
+ break;
+ }
+
+ currentCulture = currentCulture.Parent;
+ }
+
+ return result;
+ }
+
+
+ private TestSink Sink { get; } = new TestSink();
+
+ private ILogger Logger => new TestLoggerFactory(Sink, enabled: true).CreateLogger();
+
+ public class TestResourceManager : ResourceManager
+ {
+ private AssemblyWrapper _assemblyWrapper;
+
+ public TestResourceManager(string baseName, AssemblyWrapper assemblyWrapper)
+ : base(baseName, assemblyWrapper.Assembly)
+ {
+ _assemblyWrapper = assemblyWrapper;
+ }
+
+ public override string GetString(string name, CultureInfo culture) => null;
+
+ public override ResourceSet GetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents)
+ {
+ var resourceStream = _assemblyWrapper.GetManifestResourceStream(BaseName);
+
+ return resourceStream != null ? new ResourceSet(resourceStream) : null;
+ }
+ }
+
+ public class TestResourceStringProvider : ResourceManagerStringProvider
+ {
+ public TestResourceStringProvider(
+ IResourceNamesCache resourceCache,
+ TestResourceManager resourceManager,
+ Assembly assembly,
+ string resourceBaseName)
+ : base(resourceCache, resourceManager, assembly, resourceBaseName)
+ {
+ }
+ }
+
+ public class TestAssemblyWrapper : AssemblyWrapper
+ {
+ public TestAssemblyWrapper()
+ : this(typeof(TestAssemblyWrapper))
+ {
+ }
+
+ public TestAssemblyWrapper(Type type)
+ : base(type.GetTypeInfo().Assembly)
+ {
+ }
+
+ public bool HasResources { get; set; } = true;
+
+ public int ManifestResourceStreamCallCount { get; private set; }
+
+ public override Stream GetManifestResourceStream(string name)
+ {
+ ManifestResourceStreamCallCount++;
+
+ return HasResources ? MakeResourceStream() : null;
+ }
+ }
+ }
+}