Include all strings from GetAllStrings (#264)

This commit is contained in:
Ryan Brandenburg 2016-07-15 10:01:02 -07:00 committed by GitHub
parent 2669c32641
commit e976c0fa22
5 changed files with 280 additions and 76 deletions

View File

@ -0,0 +1,149 @@
// 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.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Resources;
using System.Text;
namespace Microsoft.Extensions.Localization.Internal
{
public class AssemblyResourceStringProvider : IResourceStringProvider
{
private const string AssemblyElementDelimiter = ", ";
private static readonly string[] _assemblyElementDelimiterArray = new[] { AssemblyElementDelimiter };
private static readonly char[] _assemblyEqualDelimiter = new[] { '=' };
private readonly AssemblyWrapper _assembly;
private readonly string _resourceBaseName;
private readonly IResourceNamesCache _resourceNamesCache;
public AssemblyResourceStringProvider(
IResourceNamesCache resourceCache,
AssemblyWrapper resourceAssembly,
string resourceBaseName)
{
_resourceNamesCache = resourceCache;
_assembly = resourceAssembly;
_resourceBaseName = resourceBaseName;
}
private string GetResourceCacheKey(CultureInfo culture)
{
var assemblyName = ApplyCultureToAssembly(culture);
return $"Assembly={assemblyName};resourceName={_resourceBaseName}";
}
private string GetResourceName(CultureInfo culture)
{
var resourceStreamName = _resourceBaseName;
if (!string.IsNullOrEmpty(culture.Name))
{
resourceStreamName += "." + culture.Name;
}
resourceStreamName += ".resources";
return resourceStreamName;
}
private IList<string> ThrowOrNull(CultureInfo culture, bool throwOnMissing)
{
if (throwOnMissing)
{
throw new MissingManifestResourceException(
Resources.FormatLocalization_MissingManifest(GetResourceName(culture)));
}
return null;
}
public IList<string> GetAllResourceStrings(CultureInfo culture, bool throwOnMissing)
{
var cacheKey = GetResourceCacheKey(culture);
return _resourceNamesCache.GetOrAdd(cacheKey, _ =>
{
var assembly = GetAssembly(culture);
if (assembly == null)
{
return ThrowOrNull(culture, throwOnMissing);
}
var resourceStreamName = GetResourceName(culture);
using (var resourceStream = assembly.GetManifestResourceStream(resourceStreamName))
{
if (resourceStream == null)
{
return ThrowOrNull(culture, throwOnMissing);
}
using (var resources = new ResourceReader(resourceStream))
{
var names = new List<string>();
foreach (DictionaryEntry entry in resources)
{
var resourceName = (string)entry.Key;
names.Add(resourceName);
}
return names;
}
}
});
}
protected virtual AssemblyWrapper GetAssembly(CultureInfo culture)
{
var assemblyString = ApplyCultureToAssembly(culture);
Assembly assembly;
try
{
assembly = Assembly.Load(new AssemblyName(assemblyString));
}
catch (FileNotFoundException)
{
return null;
}
return new AssemblyWrapper(assembly);
}
// This is all a workaround for https://github.com/dotnet/coreclr/issues/6123
private string ApplyCultureToAssembly(CultureInfo culture)
{
var builder = new StringBuilder(_assembly.FullName);
var cultureName = string.IsNullOrEmpty(culture.Name) ? "neutral" : culture.Name;
var cultureString = $"Culture={cultureName}";
var cultureStartIndex = _assembly.FullName.IndexOf("Culture", StringComparison.OrdinalIgnoreCase);
if (cultureStartIndex < 0)
{
builder.Append(AssemblyElementDelimiter + cultureString);
}
else
{
var cultureEndIndex = _assembly.FullName.IndexOf(
AssemblyElementDelimiter,
cultureStartIndex,
StringComparison.Ordinal);
var cultureLength = cultureEndIndex - cultureStartIndex;
builder.Remove(cultureStartIndex, cultureLength);
builder.Insert(cultureStartIndex, cultureString);
}
var firstSplit = _assembly.FullName.IndexOf(AssemblyElementDelimiter);
if (firstSplit < 0)
{
//Index of end of Assembly name
firstSplit = _assembly.FullName.Length;
}
builder.Insert(firstSplit, ".resources");
return builder.ToString();
}
}
}

View File

@ -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<string> GetAllResourceStrings(CultureInfo culture, bool throwOnMissing);
}
}

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
@ -13,8 +12,8 @@ using Microsoft.Extensions.Localization.Internal;
namespace Microsoft.Extensions.Localization namespace Microsoft.Extensions.Localization
{ {
/// <summary> /// <summary>
/// An <see cref="IStringLocalizer"/> that uses the <see cref="System.Resources.ResourceManager"/> and /// An <see cref="IStringLocalizer"/> that uses the <see cref="ResourceManager"/> and
/// <see cref="System.Resources.ResourceReader"/> to provide localized strings. /// <see cref="ResourceReader"/> to provide localized strings.
/// </summary> /// </summary>
/// <remarks>This type is thread-safe.</remarks> /// <remarks>This type is thread-safe.</remarks>
public class ResourceManagerStringLocalizer : IStringLocalizer public class ResourceManagerStringLocalizer : IStringLocalizer
@ -22,22 +21,29 @@ namespace Microsoft.Extensions.Localization
private readonly ConcurrentDictionary<string, object> _missingManifestCache = new ConcurrentDictionary<string, object>(); private readonly ConcurrentDictionary<string, object> _missingManifestCache = new ConcurrentDictionary<string, object>();
private readonly IResourceNamesCache _resourceNamesCache; private readonly IResourceNamesCache _resourceNamesCache;
private readonly ResourceManager _resourceManager; private readonly ResourceManager _resourceManager;
private readonly AssemblyWrapper _resourceAssemblyWrapper; private readonly IResourceStringProvider _resourceStringProvider;
private readonly string _resourceBaseName; private readonly string _resourceBaseName;
/// <summary> /// <summary>
/// Creates a new <see cref="ResourceManagerStringLocalizer"/>. /// Creates a new <see cref="ResourceManagerStringLocalizer"/>.
/// </summary> /// </summary>
/// <param name="resourceManager">The <see cref="System.Resources.ResourceManager"/> to read strings from.</param> /// <param name="resourceManager">The <see cref="ResourceManager"/> to read strings from.</param>
/// <param name="resourceAssembly">The <see cref="Assembly"/> that contains the strings as embedded resources.</param> /// <param name="resourceAssembly">The <see cref="Assembly"/> that contains the strings as embedded resources.</param>
/// <param name="baseName">The base name of the embedded resource in the <see cref="Assembly"/> that contains the strings.</param> /// <param name="baseName">The base name of the embedded resource that contains the strings.</param>
/// <param name="resourceNamesCache">Cache of the list of strings for a given resource assembly name.</param> /// <param name="resourceNamesCache">Cache of the list of strings for a given resource assembly name.</param>
public ResourceManagerStringLocalizer( public ResourceManagerStringLocalizer(
ResourceManager resourceManager, ResourceManager resourceManager,
Assembly resourceAssembly, Assembly resourceAssembly,
string baseName, string baseName,
IResourceNamesCache resourceNamesCache) IResourceNamesCache resourceNamesCache)
: this(resourceManager, new AssemblyWrapper(resourceAssembly), baseName, resourceNamesCache) : this(
resourceManager,
new AssemblyResourceStringProvider(
resourceNamesCache,
new AssemblyWrapper(resourceAssembly),
baseName),
baseName,
resourceNamesCache)
{ {
if (resourceAssembly == null) if (resourceAssembly == null)
{ {
@ -50,7 +56,7 @@ namespace Microsoft.Extensions.Localization
/// </summary> /// </summary>
public ResourceManagerStringLocalizer( public ResourceManagerStringLocalizer(
ResourceManager resourceManager, ResourceManager resourceManager,
AssemblyWrapper resourceAssemblyWrapper, IResourceStringProvider resourceStringProvider,
string baseName, string baseName,
IResourceNamesCache resourceNamesCache) IResourceNamesCache resourceNamesCache)
{ {
@ -59,9 +65,9 @@ namespace Microsoft.Extensions.Localization
throw new ArgumentNullException(nameof(resourceManager)); throw new ArgumentNullException(nameof(resourceManager));
} }
if (resourceAssemblyWrapper == null) if (resourceStringProvider == null)
{ {
throw new ArgumentNullException(nameof(resourceAssemblyWrapper)); throw new ArgumentNullException(nameof(resourceStringProvider));
} }
if (baseName == null) if (baseName == null)
@ -74,7 +80,7 @@ namespace Microsoft.Extensions.Localization
throw new ArgumentNullException(nameof(resourceNamesCache)); throw new ArgumentNullException(nameof(resourceNamesCache));
} }
_resourceAssemblyWrapper = resourceAssemblyWrapper; _resourceStringProvider = resourceStringProvider;
_resourceManager = resourceManager; _resourceManager = resourceManager;
_resourceBaseName = baseName; _resourceBaseName = baseName;
_resourceNamesCache = resourceNamesCache; _resourceNamesCache = resourceNamesCache;
@ -121,12 +127,12 @@ namespace Microsoft.Extensions.Localization
return culture == null return culture == null
? new ResourceManagerStringLocalizer( ? new ResourceManagerStringLocalizer(
_resourceManager, _resourceManager,
_resourceAssemblyWrapper.Assembly, _resourceStringProvider,
_resourceBaseName, _resourceBaseName,
_resourceNamesCache) _resourceNamesCache)
: new ResourceManagerWithCultureStringLocalizer( : new ResourceManagerWithCultureStringLocalizer(
_resourceManager, _resourceManager,
_resourceAssemblyWrapper.Assembly, _resourceStringProvider,
_resourceBaseName, _resourceBaseName,
_resourceNamesCache, _resourceNamesCache,
culture); culture);
@ -151,14 +157,7 @@ namespace Microsoft.Extensions.Localization
var resourceNames = includeParentCultures var resourceNames = includeParentCultures
? GetResourceNamesFromCultureHierarchy(culture) ? GetResourceNamesFromCultureHierarchy(culture)
: GetResourceNamesForCulture(culture); : _resourceStringProvider.GetAllResourceStrings(culture, true);
if (resourceNames == null && !includeParentCultures)
{
var resourceStreamName = GetResourceStreamName(culture);
throw new MissingManifestResourceException(
Resources.FormatLocalization_MissingManifest(resourceStreamName));
}
foreach (var name in resourceNames) foreach (var name in resourceNames)
{ {
@ -209,7 +208,7 @@ namespace Microsoft.Extensions.Localization
while (true) while (true)
{ {
var cultureResourceNames = GetResourceNamesForCulture(currentCulture); var cultureResourceNames = _resourceStringProvider.GetAllResourceStrings(currentCulture, false);
if (cultureResourceNames != null) if (cultureResourceNames != null)
{ {
@ -236,49 +235,5 @@ namespace Microsoft.Extensions.Localization
return resourceNames; return resourceNames;
} }
private string GetResourceStreamName(CultureInfo culture)
{
var resourceStreamName = _resourceBaseName;
if (!string.IsNullOrEmpty(culture.Name))
{
resourceStreamName += "." + culture.Name;
}
resourceStreamName += ".resources";
return resourceStreamName;
}
private IList<string> GetResourceNamesForCulture(CultureInfo culture)
{
var resourceStreamName = GetResourceStreamName(culture);
var cacheKey = $"assembly={_resourceAssemblyWrapper.FullName};resourceStreamName={resourceStreamName}";
var cultureResourceNames = _resourceNamesCache.GetOrAdd(cacheKey, _ =>
{
using (var cultureResourceStream = _resourceAssemblyWrapper.GetManifestResourceStream(resourceStreamName))
{
if (cultureResourceStream == null)
{
return null;
}
using (var resources = new ResourceReader(cultureResourceStream))
{
var names = new List<string>();
foreach (DictionaryEntry entry in resources)
{
var resourceName = (string)entry.Key;
names.Add(resourceName);
}
return names;
}
}
});
return cultureResourceNames;
}
} }
} }

View File

@ -6,12 +6,13 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Reflection; using System.Reflection;
using System.Resources; using System.Resources;
using Microsoft.Extensions.Localization.Internal;
namespace Microsoft.Extensions.Localization namespace Microsoft.Extensions.Localization
{ {
/// <summary> /// <summary>
/// An <see cref="IStringLocalizer"/> that uses the <see cref="System.Resources.ResourceManager"/> and /// An <see cref="IStringLocalizer"/> that uses the <see cref="ResourceManager"/> and
/// <see cref="System.Resources.ResourceReader"/> to provide localized strings for a specific <see cref="CultureInfo"/>. /// <see cref="ResourceReader"/> to provide localized strings for a specific <see cref="CultureInfo"/>.
/// </summary> /// </summary>
public class ResourceManagerWithCultureStringLocalizer : ResourceManagerStringLocalizer public class ResourceManagerWithCultureStringLocalizer : ResourceManagerStringLocalizer
{ {
@ -20,9 +21,53 @@ namespace Microsoft.Extensions.Localization
/// <summary> /// <summary>
/// Creates a new <see cref="ResourceManagerWithCultureStringLocalizer"/>. /// Creates a new <see cref="ResourceManagerWithCultureStringLocalizer"/>.
/// </summary> /// </summary>
/// <param name="resourceManager">The <see cref="System.Resources.ResourceManager"/> to read strings from.</param> /// <param name="resourceManager">The <see cref="ResourceManager"/> to read strings from.</param>
/// <param name="resourceStringProvider">The <see cref="IResourceStringProvider"/> that can find the resources.</param>
/// <param name="baseName">The base name of the embedded resource that contains the strings.</param>
/// <param name="resourceNamesCache">Cache of the list of strings for a given resource assembly name.</param>
/// <param name="culture">The specific <see cref="CultureInfo"/> to use.</param>
internal ResourceManagerWithCultureStringLocalizer(
ResourceManager resourceManager,
IResourceStringProvider resourceStringProvider,
string baseName,
IResourceNamesCache resourceNamesCache,
CultureInfo culture)
: base(resourceManager, resourceStringProvider, baseName, resourceNamesCache)
{
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));
}
_culture = culture;
}
/// <summary>
/// Creates a new <see cref="ResourceManagerWithCultureStringLocalizer"/>.
/// </summary>
/// <param name="resourceManager">The <see cref="ResourceManager"/> to read strings from.</param>
/// <param name="resourceAssembly">The <see cref="Assembly"/> that contains the strings as embedded resources.</param> /// <param name="resourceAssembly">The <see cref="Assembly"/> that contains the strings as embedded resources.</param>
/// <param name="baseName">The base name of the embedded resource in the <see cref="Assembly"/> that contains the strings.</param> /// <param name="baseName">The base name of the embedded resource that contains the strings.</param>
/// <param name="resourceNamesCache">Cache of the list of strings for a given resource assembly name.</param> /// <param name="resourceNamesCache">Cache of the list of strings for a given resource assembly name.</param>
/// <param name="culture">The specific <see cref="CultureInfo"/> to use.</param> /// <param name="culture">The specific <see cref="CultureInfo"/> to use.</param>
public ResourceManagerWithCultureStringLocalizer( public ResourceManagerWithCultureStringLocalizer(

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -21,8 +22,15 @@ namespace Microsoft.Extensions.Localization.Tests
var baseName = "test"; var baseName = "test";
var resourceAssembly = new TestAssemblyWrapper(); var resourceAssembly = new TestAssemblyWrapper();
var resourceManager = new TestResourceManager(baseName, resourceAssembly.Assembly); var resourceManager = new TestResourceManager(baseName, resourceAssembly.Assembly);
var localizer1 = new ResourceManagerStringLocalizer(resourceManager, resourceAssembly, baseName, resourceNamesCache); var resourceStreamManager = new TestResourceStringProvider(resourceNamesCache, resourceAssembly, baseName);
var localizer2 = new ResourceManagerStringLocalizer(resourceManager, resourceAssembly, baseName, resourceNamesCache); var localizer1 = new ResourceManagerStringLocalizer(resourceManager,
resourceStreamManager,
baseName,
resourceNamesCache);
var localizer2 = new ResourceManagerStringLocalizer(resourceManager,
resourceStreamManager,
baseName,
resourceNamesCache);
// Act // Act
for (int i = 0; i < 5; i++) for (int i = 0; i < 5; i++)
@ -46,8 +54,19 @@ namespace Microsoft.Extensions.Localization.Tests
var resourceAssembly2 = new TestAssemblyWrapper("Assembly2"); var resourceAssembly2 = new TestAssemblyWrapper("Assembly2");
var resourceManager1 = new TestResourceManager(baseName, resourceAssembly1.Assembly); var resourceManager1 = new TestResourceManager(baseName, resourceAssembly1.Assembly);
var resourceManager2 = new TestResourceManager(baseName, resourceAssembly2.Assembly); var resourceManager2 = new TestResourceManager(baseName, resourceAssembly2.Assembly);
var localizer1 = new ResourceManagerStringLocalizer(resourceManager1, resourceAssembly1, baseName, resourceNamesCache); var resourceStreamManager1 = new TestResourceStringProvider(resourceNamesCache, resourceAssembly1, baseName);
var localizer2 = new ResourceManagerStringLocalizer(resourceManager2, resourceAssembly2, baseName, resourceNamesCache); var resourceStreamManager2 = new TestResourceStringProvider(resourceNamesCache, resourceAssembly2, baseName);
var localizer1 = new ResourceManagerStringLocalizer(
resourceManager1,
resourceStreamManager1,
baseName,
resourceNamesCache);
var localizer2 = new ResourceManagerStringLocalizer(
resourceManager2,
resourceStreamManager2,
baseName,
resourceNamesCache);
// Act // Act
localizer1.GetAllStrings().ToList(); localizer1.GetAllStrings().ToList();
@ -69,7 +88,12 @@ namespace Microsoft.Extensions.Localization.Tests
var resourceNamesCache = new ResourceNamesCache(); var resourceNamesCache = new ResourceNamesCache();
var resourceAssembly = new TestAssemblyWrapper(); var resourceAssembly = new TestAssemblyWrapper();
var resourceManager = new TestResourceManager(baseName, resourceAssembly.Assembly); var resourceManager = new TestResourceManager(baseName, resourceAssembly.Assembly);
var localizer = new ResourceManagerStringLocalizer(resourceManager, resourceAssembly, baseName, resourceNamesCache); var resourceStreamManager = new TestResourceStringProvider(resourceNamesCache, resourceAssembly, baseName);
var localizer = new ResourceManagerStringLocalizer(
resourceManager,
resourceStreamManager,
baseName,
resourceNamesCache);
// Act // Act
// We have to access the result so it evaluates. // We have to access the result so it evaluates.
@ -144,12 +168,30 @@ namespace Microsoft.Extensions.Localization.Tests
public TestResourceManager(string baseName, Assembly assembly) public TestResourceManager(string baseName, Assembly assembly)
: base(baseName, assembly) : base(baseName, assembly)
{ {
} }
public override string GetString(string name, CultureInfo culture) => null; public override string GetString(string name, CultureInfo culture) => null;
} }
public class TestResourceStringProvider : AssemblyResourceStringProvider
{
private TestAssemblyWrapper _assemblyWrapper;
public TestResourceStringProvider(
IResourceNamesCache resourceCache,
TestAssemblyWrapper assemblyWrapper,
string resourceBaseName)
: base(resourceCache, assemblyWrapper, resourceBaseName)
{
_assemblyWrapper = assemblyWrapper;
}
protected override AssemblyWrapper GetAssembly(CultureInfo culture)
{
return _assemblyWrapper;
}
}
public class TestAssemblyWrapper : AssemblyWrapper public class TestAssemblyWrapper : AssemblyWrapper
{ {
private readonly string _name; private readonly string _name;