diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Pages/Shared/Components/AbpPageToolbar/Button/AbpPageToolbarButtonViewComponent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Pages/Shared/Components/AbpPageToolbar/Button/AbpPageToolbarButtonViewComponent.cs index 097fad5f40..56ae47f0c8 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Pages/Shared/Components/AbpPageToolbar/Button/AbpPageToolbarButtonViewComponent.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Pages/Shared/Components/AbpPageToolbar/Button/AbpPageToolbarButtonViewComponent.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Button; @@ -15,7 +16,7 @@ public class AbpPageToolbarButtonViewComponent : AbpViewComponent StringLocalizerFactory = stringLocalizerFactory; } - public IViewComponentResult Invoke( + public async Task InvokeAsync( ILocalizableString text, string name, string icon, @@ -31,11 +32,11 @@ public class AbpPageToolbarButtonViewComponent : AbpViewComponent return View( "~/Pages/Shared/Components/AbpPageToolbar/Button/Default.cshtml", new AbpPageToolbarButtonViewModel( - text.Localize(StringLocalizerFactory), + await text.LocalizeAsync(StringLocalizerFactory), name, icon, id, - busyText?.Localize(StringLocalizerFactory), + busyText == null ? null : await busyText.LocalizeAsync(StringLocalizerFactory), iconType, type, size, diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs index 6d58172a16..37c509a285 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs @@ -229,7 +229,7 @@ public class AbpApplicationConfigurationAppService : ApplicationService, IAbpApp { var dictionary = new Dictionary(); - var localizer = StringLocalizerFactory.CreateByResourceNameOrNull(resourceName); + var localizer = await StringLocalizerFactory.CreateByResourceNameOrNullAsync(resourceName); if (localizer != null) { foreach (var localizedString in await localizer.GetAllStringsAsync()) diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/UiNotificationAlert.razor.cs b/framework/src/Volo.Abp.BlazoriseUI/Components/UiNotificationAlert.razor.cs index a1a079fe27..359ea1a422 100644 --- a/framework/src/Volo.Abp.BlazoriseUI/Components/UiNotificationAlert.razor.cs +++ b/framework/src/Volo.Abp.BlazoriseUI/Components/UiNotificationAlert.razor.cs @@ -5,6 +5,7 @@ using Blazorise.Snackbar; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Localization; using Volo.Abp.AspNetCore.Components.Notifications; +using Volo.Abp.Localization; namespace Volo.Abp.BlazoriseUI.Components; @@ -54,13 +55,15 @@ public partial class UiNotificationAlert : ComponentBase, IDisposable Title = e.Title; Options = e.Options; - var okButtonText = Options?.OkButtonText?.Localize(StringLocalizerFactory); + var okButtonText = Options?.OkButtonText == null + ? null + : await Options.OkButtonText.LocalizeAsync(StringLocalizerFactory); await SnackbarStack.PushAsync(Message, Title, GetSnackbarColor(e.NotificationType), (options) => - { - options.CloseButtonIcon = IconName.Times; - options.ActionButtonText = okButtonText; - }); + { + options.CloseButtonIcon = IconName.Times; + options.ActionButtonText = okButtonText; + }); } public virtual void Dispose() diff --git a/framework/src/Volo.Abp.Localization.Abstractions/Microsoft/Extensions/Localization/AbpStringLocalizerFactoryExtensions.cs b/framework/src/Volo.Abp.Localization.Abstractions/Microsoft/Extensions/Localization/AbpStringLocalizerFactoryExtensions.cs index ccd9718de9..8aa2515a54 100644 --- a/framework/src/Volo.Abp.Localization.Abstractions/Microsoft/Extensions/Localization/AbpStringLocalizerFactoryExtensions.cs +++ b/framework/src/Volo.Abp.Localization.Abstractions/Microsoft/Extensions/Localization/AbpStringLocalizerFactoryExtensions.cs @@ -1,4 +1,5 @@ -using JetBrains.Annotations; +using System.Threading.Tasks; +using JetBrains.Annotations; using Volo.Abp; namespace Microsoft.Extensions.Localization; @@ -34,4 +35,32 @@ public static class AbpStringLocalizerFactoryExtensions return localizer; } + + [ItemCanBeNull] + public static async Task CreateByResourceNameOrNullAsync( + this IStringLocalizerFactory localizerFactory, + string resourceName) + { + var abpLocalizerFactory = localizerFactory as IAbpStringLocalizerFactory; + if (abpLocalizerFactory == null) + { + return null; + } + + return await abpLocalizerFactory.CreateByResourceNameOrNullAsync(resourceName); + } + + [NotNull] + public async static Task CreateByResourceNameAsync( + this IStringLocalizerFactory localizerFactory, + string resourceName) + { + var localizer = await localizerFactory.CreateByResourceNameOrNullAsync(resourceName); + if (localizer == null) + { + throw new AbpException("Couldn't find a localizer with given resource name: " + resourceName); + } + + return localizer; + } } diff --git a/framework/src/Volo.Abp.Localization.Abstractions/Microsoft/Extensions/Localization/IAbpStringLocalizerFactory.cs b/framework/src/Volo.Abp.Localization.Abstractions/Microsoft/Extensions/Localization/IAbpStringLocalizerFactory.cs index d9ae96c324..8f6699a13d 100644 --- a/framework/src/Volo.Abp.Localization.Abstractions/Microsoft/Extensions/Localization/IAbpStringLocalizerFactory.cs +++ b/framework/src/Volo.Abp.Localization.Abstractions/Microsoft/Extensions/Localization/IAbpStringLocalizerFactory.cs @@ -1,4 +1,5 @@ -using JetBrains.Annotations; +using System.Threading.Tasks; +using JetBrains.Annotations; namespace Microsoft.Extensions.Localization; @@ -9,4 +10,7 @@ public interface IAbpStringLocalizerFactory [CanBeNull] IStringLocalizer CreateByResourceNameOrNull([NotNull] string resourceName); + + [ItemCanBeNull] + Task CreateByResourceNameOrNullAsync([NotNull] string resourceName); } diff --git a/framework/src/Volo.Abp.Localization.Abstractions/Volo/Abp/Localization/IAsyncLocalizableString.cs b/framework/src/Volo.Abp.Localization.Abstractions/Volo/Abp/Localization/IAsyncLocalizableString.cs new file mode 100644 index 0000000000..b8389d546f --- /dev/null +++ b/framework/src/Volo.Abp.Localization.Abstractions/Volo/Abp/Localization/IAsyncLocalizableString.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.Localization; + +namespace Volo.Abp.Localization; + +public interface IAsyncLocalizableString +{ + Task LocalizeAsync(IStringLocalizerFactory stringLocalizerFactory); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Localization.Abstractions/Volo/Abp/Localization/ILocalizableString.cs b/framework/src/Volo.Abp.Localization.Abstractions/Volo/Abp/Localization/ILocalizableString.cs index 2aecd696ac..d04e61a257 100644 --- a/framework/src/Volo.Abp.Localization.Abstractions/Volo/Abp/Localization/ILocalizableString.cs +++ b/framework/src/Volo.Abp.Localization.Abstractions/Volo/Abp/Localization/ILocalizableString.cs @@ -5,4 +5,4 @@ namespace Volo.Abp.Localization; public interface ILocalizableString { LocalizedString Localize(IStringLocalizerFactory stringLocalizerFactory); -} +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Localization.Abstractions/Volo/Abp/Localization/LocalizableString.cs b/framework/src/Volo.Abp.Localization.Abstractions/Volo/Abp/Localization/LocalizableString.cs index ef2027b415..009e4ec445 100644 --- a/framework/src/Volo.Abp.Localization.Abstractions/Volo/Abp/Localization/LocalizableString.cs +++ b/framework/src/Volo.Abp.Localization.Abstractions/Volo/Abp/Localization/LocalizableString.cs @@ -1,10 +1,11 @@ using System; +using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.Localization; namespace Volo.Abp.Localization; -public class LocalizableString : ILocalizableString +public class LocalizableString : ILocalizableString, IAsyncLocalizableString { [CanBeNull] public string ResourceName { get; } @@ -54,6 +55,29 @@ public class LocalizableString : ILocalizableString return result; } + + public async Task LocalizeAsync(IStringLocalizerFactory stringLocalizerFactory) + { + var localizer = await CreateStringLocalizerOrNullAsync(stringLocalizerFactory); + if (localizer == null) + { + throw new AbpException($"Set {nameof(ResourceName)} or configure the default localization resource type (in the AbpLocalizationOptions)!"); + } + + var result = localizer[Name]; + + if (result.ResourceNotFound && ResourceName != null) + { + /* Search in the default resource if not found in the provided resource */ + localizer = stringLocalizerFactory.CreateDefaultOrNull(); + if (localizer != null) + { + result = localizer[Name]; + } + } + + return result; + } private IStringLocalizer CreateStringLocalizerOrNull(IStringLocalizerFactory stringLocalizerFactory) { @@ -73,6 +97,25 @@ public class LocalizableString : ILocalizableString return stringLocalizerFactory.CreateDefaultOrNull(); } + + private async Task CreateStringLocalizerOrNullAsync(IStringLocalizerFactory stringLocalizerFactory) + { + if (ResourceType != null) + { + return stringLocalizerFactory.Create(ResourceType); + } + + if (ResourceName != null) + { + var localizerByName = await stringLocalizerFactory.CreateByResourceNameOrNullAsync(ResourceName); + if (localizerByName != null) + { + return localizerByName; + } + } + + return stringLocalizerFactory.CreateDefaultOrNull(); + } public static LocalizableString Create([NotNull] string name) { diff --git a/framework/src/Volo.Abp.Localization.Abstractions/Volo/Abp/Localization/LocalizableStringExtensions.cs b/framework/src/Volo.Abp.Localization.Abstractions/Volo/Abp/Localization/LocalizableStringExtensions.cs new file mode 100644 index 0000000000..b7cff0819f --- /dev/null +++ b/framework/src/Volo.Abp.Localization.Abstractions/Volo/Abp/Localization/LocalizableStringExtensions.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.Localization; + +namespace Volo.Abp.Localization; + +public static class LocalizableStringExtensions +{ + public static async Task LocalizeAsync( + this ILocalizableString localizableString, + IStringLocalizerFactory stringLocalizerFactory) + { + if (localizableString is IAsyncLocalizableString asyncLocalizableString) + { + return await asyncLocalizableString.LocalizeAsync(stringLocalizerFactory); + } + + return localizableString.Localize(stringLocalizerFactory); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpStringLocalizerFactory.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpStringLocalizerFactory.cs index c346d9ee51..b89a54abd9 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpStringLocalizerFactory.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpStringLocalizerFactory.cs @@ -2,11 +2,14 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; using Volo.Abp.Localization.External; +using Volo.Abp.Threading; namespace Volo.Abp.Localization; @@ -17,6 +20,7 @@ public class AbpStringLocalizerFactory : IStringLocalizerFactory, IAbpStringLoca protected IServiceProvider ServiceProvider { get; } protected IExternalLocalizationStore ExternalLocalizationStore { get; } protected ConcurrentDictionary LocalizerCache { get; } + protected SemaphoreSlim LocalizerCacheSemaphore { get; } = new(1, 1); public AbpStringLocalizerFactory( ResourceManagerStringLocalizerFactory innerFactory, @@ -33,6 +37,11 @@ public class AbpStringLocalizerFactory : IStringLocalizerFactory, IAbpStringLoca } public virtual IStringLocalizer Create(Type resourceType) + { + return Create(resourceType, lockCache: true); + } + + private IStringLocalizer Create(Type resourceType, bool lockCache) { var resource = AbpLocalizationOptions.Resources.GetOrNull(resourceType); if (resource == null) @@ -40,10 +49,17 @@ public class AbpStringLocalizerFactory : IStringLocalizerFactory, IAbpStringLoca return InnerFactory.Create(resourceType); } - return CreateInternal(resource.ResourceName, resource); + return CreateInternal(resource.ResourceName, resource, lockCache); } public IStringLocalizer CreateByResourceNameOrNull(string resourceName) + { + return CreateByResourceNameOrNullInternal(resourceName, lockCache: true); + } + + private IStringLocalizer CreateByResourceNameOrNullInternal( + string resourceName, + bool lockCache) { var resource = AbpLocalizationOptions.Resources.GetOrDefault(resourceName); if (resource == null) @@ -55,23 +71,102 @@ public class AbpStringLocalizerFactory : IStringLocalizerFactory, IAbpStringLoca } } - return CreateInternal(resourceName, resource); + return CreateInternal(resourceName, resource, lockCache); + } + + public Task CreateByResourceNameOrNullAsync(string resourceName) + { + return CreateByResourceNameOrNullInternalAsync(resourceName, lockCache: true); } - private IStringLocalizer CreateInternal(string resourceName, LocalizationResourceBase resource) + private async Task CreateByResourceNameOrNullInternalAsync( + string resourceName, + bool lockCache) + { + var resource = AbpLocalizationOptions.Resources.GetOrDefault(resourceName); + if (resource == null) + { + resource = await ExternalLocalizationStore.GetResourceOrNullAsync(resourceName); + if (resource == null) + { + return null; + } + } + + return await CreateInternalAsync(resourceName, resource, lockCache); + } + + private IStringLocalizer CreateInternal( + string resourceName, + LocalizationResourceBase resource, + bool lockCache) { if (LocalizerCache.TryGetValue(resourceName, out var cacheItem)) { return cacheItem.Localizer; } - lock (LocalizerCache) + IStringLocalizer GetOrCreateLocalizer() { + // Double check + if (LocalizerCache.TryGetValue(resourceName, out var cacheItem2)) + { + return cacheItem2.Localizer; + } + return LocalizerCache.GetOrAdd( resourceName, _ => CreateStringLocalizerCacheItem(resource) ).Localizer; } + + if (lockCache) + { + using (LocalizerCacheSemaphore.Lock()) + { + return GetOrCreateLocalizer(); + } + } + else + { + return GetOrCreateLocalizer(); + } + } + + private async Task CreateInternalAsync( + string resourceName, + LocalizationResourceBase resource, + bool lockCache) + { + if (LocalizerCache.TryGetValue(resourceName, out var cacheItem)) + { + return cacheItem.Localizer; + } + + async Task GetOrCreateLocalizerAsync() + { + // Double check + if (LocalizerCache.TryGetValue(resourceName, out var cacheItem2)) + { + return cacheItem2.Localizer; + } + + var newCacheItem = await CreateStringLocalizerCacheItemAsync(resource); + LocalizerCache[resourceName] = newCacheItem; + return newCacheItem.Localizer; + } + + if (lockCache) + { + using (await LocalizerCacheSemaphore.LockAsync()) + { + return await GetOrCreateLocalizerAsync(); + } + } + else + { + return await GetOrCreateLocalizerAsync(); + } } private StringLocalizerCacheItem CreateStringLocalizerCacheItem(LocalizationResourceBase resource) @@ -97,13 +192,51 @@ public class AbpStringLocalizerFactory : IStringLocalizerFactory, IAbpStringLoca resource, resource .BaseResourceNames - .Select(CreateByResourceNameOrNull) + .Select(x => CreateByResourceNameOrNullInternal(x, lockCache: false)) .Where(x => x != null) .ToList(), AbpLocalizationOptions ) ); } + + private async Task CreateStringLocalizerCacheItemAsync(LocalizationResourceBase resource) + { + foreach (var globalContributorType in AbpLocalizationOptions.GlobalContributors) + { + resource.Contributors.Add( + Activator + .CreateInstance(globalContributorType) + .As() + ); + } + + var context = new LocalizationResourceInitializationContext(resource, ServiceProvider); + + foreach (var contributor in resource.Contributors) + { + contributor.Initialize(context); + } + + var baseLocalizers = new List(); + + foreach (var baseResourceName in resource.BaseResourceNames) + { + var baseLocalizer = await CreateByResourceNameOrNullInternalAsync(baseResourceName, lockCache: false); + if (baseLocalizer != null) + { + baseLocalizers.Add(baseLocalizer); + } + } + + return new StringLocalizerCacheItem( + new AbpDictionaryBasedStringLocalizer( + resource, + baseLocalizers, + AbpLocalizationOptions + ) + ); + } public virtual IStringLocalizer Create(string baseName, string location) { diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/External/IExternalLocalizationStore.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/External/IExternalLocalizationStore.cs index fa03a6bbff..c04160eea1 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/External/IExternalLocalizationStore.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/External/IExternalLocalizationStore.cs @@ -8,5 +8,8 @@ public interface IExternalLocalizationStore [CanBeNull] LocalizationResourceBase GetResourceOrNull([NotNull] string resourceName); + [ItemCanBeNull] + Task GetResourceOrNullAsync([NotNull] string resourceName); + Task GetResourceNamesAsync(); } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/External/NullExternalLocalizationStore.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/External/NullExternalLocalizationStore.cs index 0c55919db2..a87bf74d71 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/External/NullExternalLocalizationStore.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/External/NullExternalLocalizationStore.cs @@ -11,6 +11,11 @@ public class NullExternalLocalizationStore : IExternalLocalizationStore, ISingle return null; } + public Task GetResourceOrNullAsync(string resourceName) + { + return Task.FromResult(null); + } + public Task GetResourceNamesAsync() { return Task.FromResult(Array.Empty()); diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpStringLocalizerFactory_Tests.cs b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpStringLocalizerFactory_Tests.cs index 352a7ea3a3..8cb0fcb833 100644 --- a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpStringLocalizerFactory_Tests.cs +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpStringLocalizerFactory_Tests.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Localization; +using System.Threading.Tasks; +using Microsoft.Extensions.Localization; using Shouldly; using Volo.Abp.DynamicProxy; using Volo.Abp.Testing; @@ -32,6 +33,17 @@ public class AbpStringLocalizerFactory_Tests : AbpIntegratedTest _factory.CreateByResourceName("UnknownResourceName") ); } + + [Fact] + public async Task Should_Throw_Exception_For_Unknown_Resource_Names_Async() + { + await Assert.ThrowsAsync( + async () => await _factory.CreateByResourceNameAsync("UnknownResourceName") + ); + } } \ No newline at end of file