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