From 0d3f3ed9f3c1ca889eeab4160d58b795ba76ed60 Mon Sep 17 00:00:00 2001 From: DiegoMarguerettaz-Laser Date: Fri, 18 Sep 2020 08:40:09 +0200 Subject: [PATCH] Issue #8318: Managing multiple scopes in localization concurrent dictionary. (#8334) * Manage multiple scopes in localization concurrent dictionary (issue 8318) Co-authored-by: matteo.piovanelli --- src/Orchard.Tests/Localization/TextTests.cs | 5 ++- src/Orchard/Localization/FormatForScope.cs | 16 +++++++ .../Localization/LocalizationModule.cs | 28 +++++++----- .../Localization/LocalizationUtilities.cs | 14 ++++-- .../Services/DefaultLocalizedStringManager.cs | 43 +++++++++++-------- .../Services/ILocalizedStringManager.cs | 7 ++- src/Orchard/Localization/Text.cs | 26 +++++------ src/Orchard/Orchard.Framework.csproj | 1 + 8 files changed, 92 insertions(+), 48 deletions(-) create mode 100644 src/Orchard/Localization/FormatForScope.cs diff --git a/src/Orchard.Tests/Localization/TextTests.cs b/src/Orchard.Tests/Localization/TextTests.cs index ecefd961e..628adc580 100644 --- a/src/Orchard.Tests/Localization/TextTests.cs +++ b/src/Orchard.Tests/Localization/TextTests.cs @@ -5,6 +5,7 @@ using Orchard.Localization; using Orchard.Localization.Services; using Orchard.Mvc; using Orchard.Tests.Stubs; +using System.Collections.Generic; using System.Web; namespace Orchard.Tests.Localization { @@ -17,8 +18,8 @@ namespace Orchard.Tests.Localization { public void Init() { var mockLocalizedManager = new Mock(); mockLocalizedManager - .Setup(x => x.GetLocalizedString(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns("foo {0}"); + .Setup(x => x.GetLocalizedString(new List { It.IsAny() }, It.IsAny(), It.IsAny())) + .Returns(new FormatForScope("foo {0}", null)); var builder = new ContainerBuilder(); builder.RegisterInstance(new StubCultureSelector("fr-CA")).As(); diff --git a/src/Orchard/Localization/FormatForScope.cs b/src/Orchard/Localization/FormatForScope.cs new file mode 100644 index 000000000..af633cde7 --- /dev/null +++ b/src/Orchard/Localization/FormatForScope.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Orchard.Localization { + public class FormatForScope { + public FormatForScope(string format, string scope) { + Scope = scope; + Format = format; + } + public string Scope { get; set; } + public string Format { get; set; } + } +} diff --git a/src/Orchard/Localization/LocalizationModule.cs b/src/Orchard/Localization/LocalizationModule.cs index 3c003ce55..248233ed8 100644 --- a/src/Orchard/Localization/LocalizationModule.cs +++ b/src/Orchard/Localization/LocalizationModule.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; using System.Reflection; using Autofac; using Autofac.Core; @@ -20,18 +22,24 @@ namespace Orchard.Localization { protected override void AttachToComponentRegistration(IComponentRegistry componentRegistry, IComponentRegistration registration) { var userProperty = FindUserProperty(registration.Activator.LimitType); - if (userProperty != null) { - var scope = registration.Activator.LimitType.FullName; + List scopes = new List(); + var type = registration.Activator.LimitType; + // we don't need this behavior on CLR types, so that's an optimization + while (!type.Namespace.Equals("System")) { + scopes.Add(type.FullName); + type = type.BaseType; + } - registration.Activated += (sender, e) => { - if (e.Instance.GetType().FullName != scope) { - return; - } - - var localizer = _localizerCache.GetOrAdd(scope, key => LocalizationUtilities.Resolve(e.Context, scope)); - userProperty.SetValue(e.Instance, localizer, null); - }; + foreach(var scope in scopes) { + registration.Activated += (sender, e) => { + if (e.Instance.GetType().FullName != scope) { + return; + } + var localizer = _localizerCache.GetOrAdd(scope, key => LocalizationUtilities.Resolve(e.Context, scopes)); + userProperty.SetValue(e.Instance, localizer, null); + }; + } } } diff --git a/src/Orchard/Localization/LocalizationUtilities.cs b/src/Orchard/Localization/LocalizationUtilities.cs index b7ed7638d..520d8ceb4 100644 --- a/src/Orchard/Localization/LocalizationUtilities.cs +++ b/src/Orchard/Localization/LocalizationUtilities.cs @@ -1,19 +1,25 @@ -using System.Web.Mvc; +using System.Collections.Generic; +using System.Web.Mvc; using Autofac; namespace Orchard.Localization { public class LocalizationUtilities { public static Localizer Resolve(WorkContext workContext, string scope) { - return workContext == null ? NullLocalizer.Instance : Resolve(workContext.Resolve(), scope); + return workContext == null ? NullLocalizer.Instance : Resolve(workContext.Resolve(), new List { scope }); } public static Localizer Resolve(ControllerContext controllerContext, string scope) { var workContext = controllerContext.GetWorkContext(); - return Resolve(workContext, scope); + return Resolve(workContext, scope ); } public static Localizer Resolve(IComponentContext context, string scope) { - var text = context.Resolve(new NamedParameter("scope", scope)); + var text = context.Resolve(new NamedParameter("scope", new List { scope })); + return text.Get; + } + + public static Localizer Resolve(IComponentContext context, IEnumerable scopes) { + var text = context.Resolve(new NamedParameter("scopes", scopes)); return text.Get; } } diff --git a/src/Orchard/Localization/Services/DefaultLocalizedStringManager.cs b/src/Orchard/Localization/Services/DefaultLocalizedStringManager.cs index 936183a8a..d4245f125 100644 --- a/src/Orchard/Localization/Services/DefaultLocalizedStringManager.cs +++ b/src/Orchard/Localization/Services/DefaultLocalizedStringManager.cs @@ -8,6 +8,8 @@ using Orchard.FileSystems.WebSite; using Orchard.Logging; using Orchard.Environment.Descriptor.Models; using System.Linq; +using System.Collections; +using System; namespace Orchard.Localization.Services { public class DefaultLocalizedStringManager : ILocalizedStringManager { @@ -47,28 +49,34 @@ namespace Orchard.Localization.Services { ILogger Logger { get; set; } public bool DisableMonitoring { get; set; } + public FormatForScope GetLocalizedString(IEnumerable scopes, string text, string cultureName) { + var culture = LoadCulture(cultureName); + foreach (var scope in scopes) { + string scopedKey = (scope + "|" + text).ToLowerInvariant(); + if (culture.Translations.ContainsKey(scopedKey)) { + return new FormatForScope(culture.Translations[scopedKey], scope); + } + } + string genericKey = ("|" + text).ToLowerInvariant(); + if (culture.Translations.ContainsKey(genericKey)) { + return new FormatForScope(culture.Translations[genericKey], null); + } + + foreach (var scope in scopes) { + string parent_text = GetParentTranslation(scope, text, cultureName); + if (!parent_text.Equals(text)) { + return new FormatForScope(parent_text, scope); + } + } + return new FormatForScope(text, scopes.FirstOrDefault()); + } + // This will translate a string into a string in the target cultureName. // The scope portion is optional, it amounts to the location of the file containing // the string in case it lives in a view, or the namespace name if the string lives in a binary. // If the culture doesn't have a translation for the string, it will fallback to the // parent culture as defined in the .net culture hierarchy. e.g. fr-FR will fallback to fr. // In case it's not found anywhere, the text is returned as is. - public string GetLocalizedString(string scope, string text, string cultureName) { - var culture = LoadCulture(cultureName); - - string scopedKey = (scope + "|" + text).ToLowerInvariant(); - if (culture.Translations.ContainsKey(scopedKey)) { - return culture.Translations[scopedKey]; - } - - string genericKey = ("|" + text).ToLowerInvariant(); - if (culture.Translations.ContainsKey(genericKey)) { - return culture.Translations[genericKey]; - } - - return GetParentTranslation(scope, text, cultureName); - } - private string GetParentTranslation(string scope, string text, string cultureName) { string scopedKey = (scope + "|" + text).ToLowerInvariant(); string genericKey = ("|" + text).ToLowerInvariant(); @@ -143,8 +151,7 @@ namespace Orchard.Localization.Services { } foreach (var theme in _extensionManager.AvailableExtensions()) { - if (DefaultExtensionTypes.IsTheme(theme.ExtensionType) && _shellDescriptor.Features.Any(x => x.Name == theme.Id)) - { + if (DefaultExtensionTypes.IsTheme(theme.ExtensionType) && _shellDescriptor.Features.Any(x => x.Name == theme.Id)) { string themePath = string.Format(ThemesLocalizationFilePathFormat, theme.VirtualPath, culture); text = _webSiteFolder.ReadFile(themePath); diff --git a/src/Orchard/Localization/Services/ILocalizedStringManager.cs b/src/Orchard/Localization/Services/ILocalizedStringManager.cs index 52af84791..748e1211c 100644 --- a/src/Orchard/Localization/Services/ILocalizedStringManager.cs +++ b/src/Orchard/Localization/Services/ILocalizedStringManager.cs @@ -1,5 +1,8 @@ -namespace Orchard.Localization.Services { +using System.Collections.Generic; + + +namespace Orchard.Localization.Services { public interface ILocalizedStringManager : IDependency { - string GetLocalizedString(string scope, string text, string cultureName); + FormatForScope GetLocalizedString(IEnumerable scopes, string text, string cultureName); } } diff --git a/src/Orchard/Localization/Text.cs b/src/Orchard/Localization/Text.cs index d291ca7f9..eeb2c07b8 100644 --- a/src/Orchard/Localization/Text.cs +++ b/src/Orchard/Localization/Text.cs @@ -4,43 +4,45 @@ using Orchard.Localization.Services; using Orchard.Logging; using System.Web; using System.Linq; +using System.Collections.Generic; namespace Orchard.Localization { public class Text : IText { - private readonly string _scope; + private readonly IEnumerable _scopes; private readonly IWorkContextAccessor _workContextAccessor; private readonly ILocalizedStringManager _localizedStringManager; - public Text(string scope, IWorkContextAccessor workContextAccessor, ILocalizedStringManager localizedStringManager) { - _scope = scope; + public Text(IEnumerable scopes, IWorkContextAccessor workContextAccessor, ILocalizedStringManager localizedStringManager) { + _scopes = scopes; _workContextAccessor = workContextAccessor; _localizedStringManager = localizedStringManager; Logger = NullLogger.Instance; } + public ILogger Logger { get; set; } public LocalizedString Get(string textHint, params object[] args) { - Logger.Debug("{0} localizing '{1}'", _scope, textHint); - + Logger.Debug("{0} localizing '{1}'", _scopes.FirstOrDefault(), textHint); + string scope = null; var workContext = _workContextAccessor.GetContext(); - + if (workContext != null) { var currentCulture = workContext.CurrentCulture; - var localizedFormat = _localizedStringManager.GetLocalizedString(_scope, textHint, currentCulture); - + FormatForScope localizedFormatScope = _localizedStringManager.GetLocalizedString(_scopes, textHint, currentCulture); + scope = localizedFormatScope.Scope; // localization arguments are HTML-encoded unless they implement IHtmlString return args.Length == 0 - ? new LocalizedString(localizedFormat, _scope, textHint, args) + ? new LocalizedString(localizedFormatScope.Format, scope, textHint, args) : new LocalizedString( - String.Format(GetFormatProvider(currentCulture), localizedFormat, args.Select(Encode).ToArray()), - _scope, + String.Format(GetFormatProvider(currentCulture), localizedFormatScope.Format, args.Select(Encode).ToArray()), + scope, textHint, args); } - return new LocalizedString(textHint, _scope, textHint, args); + return new LocalizedString(textHint, scope, textHint, args); } private static IFormatProvider GetFormatProvider(string currentCulture) { diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index c9bf45368..84b071215 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -185,6 +185,7 @@ +