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; + } + } + } +}