// 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; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Reflection; using System.Resources; using Microsoft.Extensions.Localization.Internal; 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 AssemblyWrapper _resourceAssemblyWrapper; private readonly string _resourceBaseName; /// /// Creates a new . /// /// The to read strings from. /// The that contains the strings as embedded resources. /// The base name of the embedded resource in the that contains the strings. /// Cache of the list of strings for a given resource assembly name. public ResourceManagerStringLocalizer( ResourceManager resourceManager, Assembly resourceAssembly, string baseName, IResourceNamesCache resourceNamesCache) : this(resourceManager, new AssemblyWrapper(resourceAssembly), baseName, resourceNamesCache) { if (resourceAssembly == null) { throw new ArgumentNullException(nameof(resourceAssembly)); } } /// /// Intended for testing purposes only. /// public ResourceManagerStringLocalizer( ResourceManager resourceManager, AssemblyWrapper resourceAssemblyWrapper, string baseName, IResourceNamesCache resourceNamesCache) { if (resourceManager == null) { throw new ArgumentNullException(nameof(resourceManager)); } if (resourceAssemblyWrapper == null) { throw new ArgumentNullException(nameof(resourceAssemblyWrapper)); } if (baseName == null) { throw new ArgumentNullException(nameof(baseName)); } if (resourceNamesCache == null) { throw new ArgumentNullException(nameof(resourceNamesCache)); } _resourceAssemblyWrapper = resourceAssemblyWrapper; _resourceManager = resourceManager; _resourceBaseName = baseName; _resourceNamesCache = resourceNamesCache; } /// 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); } } /// 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); } } /// /// Creates a new for a specific . /// /// The to use. /// A culture-specific . public IStringLocalizer WithCulture(CultureInfo culture) { return culture == null ? new ResourceManagerStringLocalizer( _resourceManager, _resourceAssemblyWrapper.Assembly, _resourceBaseName, _resourceNamesCache) : new ResourceManagerWithCultureStringLocalizer( _resourceManager, _resourceAssemblyWrapper.Assembly, _resourceBaseName, _resourceNamesCache, culture); } /// public virtual IEnumerable GetAllStrings(bool includeAncestorCultures) => GetAllStrings(includeAncestorCultures, CultureInfo.CurrentUICulture); /// /// Returns all strings in the specified culture. /// /// /// The to get strings for. /// The strings. protected IEnumerable GetAllStrings(bool includeAncestorCultures, CultureInfo culture) { if (culture == null) { throw new ArgumentNullException(nameof(culture)); } var resourceNames = includeAncestorCultures ? GetResourceNamesFromCultureHierarchy(culture) : GetResourceNamesForCulture(culture); foreach (var name in resourceNames) { var value = GetStringSafely(name, culture); yield return new LocalizedString(name, value ?? name, resourceNotFound: value == null); } } /// /// 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 cacheKey = $"name={name}&culture={(culture ?? CultureInfo.CurrentUICulture).Name}"; 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(); while (true) { try { var cultureResourceNames = GetResourceNamesForCulture(currentCulture); foreach (var resourceName in cultureResourceNames) { resourceNames.Add(resourceName); } } catch (MissingManifestResourceException) { } if (currentCulture == currentCulture.Parent) { // currentCulture begat currentCulture, probably time to leave break; } currentCulture = currentCulture.Parent; } return resourceNames; } private IList GetResourceNamesForCulture(CultureInfo culture) { var resourceStreamName = _resourceBaseName; if (!string.IsNullOrEmpty(culture.Name)) { resourceStreamName += "." + culture.Name; } resourceStreamName += ".resources"; var cacheKey = $"assembly={_resourceAssemblyWrapper.FullName};resourceStreamName={resourceStreamName}"; var cultureResourceNames = _resourceNamesCache.GetOrAdd(cacheKey, _ => { var names = new List(); using (var cultureResourceStream = _resourceAssemblyWrapper.GetManifestResourceStream(resourceStreamName)) using (var resources = new ResourceReader(cultureResourceStream)) { foreach (DictionaryEntry entry in resources) { var resourceName = (string)entry.Key; names.Add(resourceName); } } return names; }); return cultureResourceNames; } } }