diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/WebAssemblyCachedApplicationConfigurationClient.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/WebAssemblyCachedApplicationConfigurationClient.cs index 523a770a17..c3ba260a88 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/WebAssemblyCachedApplicationConfigurationClient.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/WebAssemblyCachedApplicationConfigurationClient.cs @@ -9,26 +9,43 @@ namespace Volo.Abp.AspNetCore.Components.WebAssembly; public class WebAssemblyCachedApplicationConfigurationClient : ICachedApplicationConfigurationClient, ITransientDependency { - protected AbpApplicationConfigurationClientProxy ApplicationConfigurationAppService { get; } + protected AbpApplicationConfigurationClientProxy ApplicationConfigurationClientProxy { get; } + + protected AbpApplicationLocalizationClientProxy ApplicationLocalizationClientProxy { get; } protected ApplicationConfigurationCache Cache { get; } protected ICurrentTenantAccessor CurrentTenantAccessor { get; } public WebAssemblyCachedApplicationConfigurationClient( - AbpApplicationConfigurationClientProxy applicationConfigurationAppService, + AbpApplicationConfigurationClientProxy applicationConfigurationClientProxy, ApplicationConfigurationCache cache, - ICurrentTenantAccessor currentTenantAccessor) + ICurrentTenantAccessor currentTenantAccessor, + AbpApplicationLocalizationClientProxy applicationLocalizationClientProxy) { - ApplicationConfigurationAppService = applicationConfigurationAppService; + ApplicationConfigurationClientProxy = applicationConfigurationClientProxy; Cache = cache; CurrentTenantAccessor = currentTenantAccessor; + ApplicationLocalizationClientProxy = applicationLocalizationClientProxy; } public virtual async Task InitializeAsync() { - var configurationDto = await ApplicationConfigurationAppService.GetAsync(); + var configurationDto = await ApplicationConfigurationClientProxy.GetAsync( + new ApplicationConfigurationRequestOptions { + IncludeLocalizationResources = false + } + ); + + var localizationDto = await ApplicationLocalizationClientProxy.GetAsync( + new ApplicationLocalizationRequestDto { + CultureName = configurationDto.Localization.CurrentCulture.Name, + OnlyDynamics = true + } + ); + configurationDto.Localization.Resources = localizationDto.Resources; + Cache.Set(configurationDto); CurrentTenantAccessor.Current = new BasicTenantInfo( diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/ClientProxies/AbpApplicationConfigurationClientProxy.Generated.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/ClientProxies/AbpApplicationConfigurationClientProxy.Generated.cs index 198cfce711..2d57dc66ab 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/ClientProxies/AbpApplicationConfigurationClientProxy.Generated.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/ClientProxies/AbpApplicationConfigurationClientProxy.Generated.cs @@ -15,8 +15,11 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ClientProxies; [ExposeServices(typeof(IAbpApplicationConfigurationAppService), typeof(AbpApplicationConfigurationClientProxy))] public partial class AbpApplicationConfigurationClientProxy : ClientProxyBase, IAbpApplicationConfigurationAppService { - public virtual async Task GetAsync() + public virtual async Task GetAsync(ApplicationConfigurationRequestOptions options) { - return await RequestAsync(nameof(GetAsync)); + return await RequestAsync(nameof(GetAsync), new ClientProxyRequestTypeValue + { + { typeof(ApplicationConfigurationRequestOptions), options } + }); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/ClientProxies/AbpApplicationLocalizationClientProxy.Generated.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/ClientProxies/AbpApplicationLocalizationClientProxy.Generated.cs new file mode 100644 index 0000000000..74db054d18 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/ClientProxies/AbpApplicationLocalizationClientProxy.Generated.cs @@ -0,0 +1,25 @@ +// This file is automatically generated by ABP framework to use MVC Controllers from CSharp +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Http.Client; +using Volo.Abp.Http.Modeling; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Http.Client.ClientProxying; +using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; + +// ReSharper disable once CheckNamespace +namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ClientProxies; + +[Dependency(ReplaceServices = true)] +[ExposeServices(typeof(IAbpApplicationLocalizationAppService), typeof(AbpApplicationLocalizationClientProxy))] +public partial class AbpApplicationLocalizationClientProxy : ClientProxyBase, IAbpApplicationLocalizationAppService +{ + public virtual async Task GetAsync(ApplicationLocalizationRequestDto input) + { + return await RequestAsync(nameof(GetAsync), new ClientProxyRequestTypeValue + { + { typeof(ApplicationLocalizationRequestDto), input } + }); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/ClientProxies/AbpApplicationLocalizationClientProxy.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/ClientProxies/AbpApplicationLocalizationClientProxy.cs new file mode 100644 index 0000000000..11bef4cecf --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/ClientProxies/AbpApplicationLocalizationClientProxy.cs @@ -0,0 +1,7 @@ +// This file is part of AbpApplicationLocalizationClientProxy, you can customize it here +// ReSharper disable once CheckNamespace +namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ClientProxies; + +public partial class AbpApplicationLocalizationClientProxy +{ +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/ClientProxies/abp-generate-proxy.json b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/ClientProxies/abp-generate-proxy.json index bf15515078..ce18e3eefd 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/ClientProxies/abp-generate-proxy.json +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/ClientProxies/abp-generate-proxy.json @@ -7,6 +7,8 @@ "Pages.Abp.MultiTenancy.AbpTenantController": { "controllerName": "AbpTenant", "controllerGroupName": "AbpTenant", + "isRemoteService": true, + "apiVersion": null, "type": "Pages.Abp.MultiTenancy.AbpTenantController", "interfaces": [ { @@ -93,6 +95,8 @@ "Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.AbpApplicationConfigurationController": { "controllerName": "AbpApplicationConfiguration", "controllerGroupName": "AbpApplicationConfiguration", + "isRemoteService": true, + "apiVersion": null, "type": "Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.AbpApplicationConfigurationController", "interfaces": [ { @@ -100,14 +104,36 @@ } ], "actions": { - "GetAsync": { - "uniqueName": "GetAsync", + "GetAsyncByOptions": { + "uniqueName": "GetAsyncByOptions", "name": "GetAsync", "httpMethod": "GET", "url": "api/abp/application-configuration", "supportedVersions": [], - "parametersOnMethod": [], - "parameters": [], + "parametersOnMethod": [ + { + "name": "options", + "typeAsString": "Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ApplicationConfigurationRequestOptions, Volo.Abp.AspNetCore.Mvc.Contracts", + "type": "Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ApplicationConfigurationRequestOptions", + "typeSimple": "Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ApplicationConfigurationRequestOptions", + "isOptional": false, + "defaultValue": null + } + ], + "parameters": [ + { + "nameOnMethod": "options", + "name": "IncludeLocalizationResources", + "jsonName": null, + "type": "System.Boolean", + "typeSimple": "boolean", + "isOptional": false, + "defaultValue": null, + "constraintTypes": null, + "bindingSourceId": "ModelBinding", + "descriptorName": "options" + } + ], "returnValue": { "type": "Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ApplicationConfigurationDto", "typeSimple": "Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ApplicationConfigurationDto" @@ -117,9 +143,74 @@ } } }, + "Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.AbpApplicationLocalizationController": { + "controllerName": "AbpApplicationLocalization", + "controllerGroupName": "AbpApplicationLocalization", + "isRemoteService": true, + "apiVersion": null, + "type": "Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.AbpApplicationLocalizationController", + "interfaces": [ + { + "type": "Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.IAbpApplicationLocalizationAppService" + } + ], + "actions": { + "GetAsyncByInput": { + "uniqueName": "GetAsyncByInput", + "name": "GetAsync", + "httpMethod": "GET", + "url": "api/abp/application-localization", + "supportedVersions": [], + "parametersOnMethod": [ + { + "name": "input", + "typeAsString": "Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ApplicationLocalizationRequestDto, Volo.Abp.AspNetCore.Mvc.Contracts", + "type": "Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ApplicationLocalizationRequestDto", + "typeSimple": "Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ApplicationLocalizationRequestDto", + "isOptional": false, + "defaultValue": null + } + ], + "parameters": [ + { + "nameOnMethod": "input", + "name": "CultureName", + "jsonName": null, + "type": "System.String", + "typeSimple": "string", + "isOptional": false, + "defaultValue": null, + "constraintTypes": null, + "bindingSourceId": "ModelBinding", + "descriptorName": "input" + }, + { + "nameOnMethod": "input", + "name": "OnlyDynamics", + "jsonName": null, + "type": "System.Boolean", + "typeSimple": "boolean", + "isOptional": false, + "defaultValue": null, + "constraintTypes": null, + "bindingSourceId": "ModelBinding", + "descriptorName": "input" + } + ], + "returnValue": { + "type": "Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ApplicationLocalizationDto", + "typeSimple": "Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ApplicationLocalizationDto" + }, + "allowAnonymous": null, + "implementFrom": "Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.IAbpApplicationLocalizationAppService" + } + } + }, "Volo.Abp.AspNetCore.Mvc.ApiExploring.AbpApiDefinitionController": { "controllerName": "AbpApiDefinition", "controllerGroupName": "AbpApiDefinition", + "isRemoteService": true, + "apiVersion": null, "type": "Volo.Abp.AspNetCore.Mvc.ApiExploring.AbpApiDefinitionController", "interfaces": [], "actions": { diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteExternalLocalizationStore.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteExternalLocalizationStore.cs new file mode 100644 index 0000000000..c5548acc62 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteExternalLocalizationStore.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Localization; +using Volo.Abp.Localization.External; + +namespace Volo.Abp.AspNetCore.Mvc.Client; + +public class RemoteExternalLocalizationStore : IExternalLocalizationStore, ITransientDependency +{ + protected ICachedApplicationConfigurationClient ConfigurationClient { get; } + protected AbpLocalizationOptions LocalizationOptions { get; } + + public RemoteExternalLocalizationStore( + ICachedApplicationConfigurationClient configurationClient, + IOptions localizationOptions) + { + ConfigurationClient = configurationClient; + LocalizationOptions = localizationOptions.Value; + } + + public virtual LocalizationResourceBase GetResourceOrNull(string resourceName) + { + var configurationDto = ConfigurationClient.Get(); + return CreateLocalizationResourceFromConfigurationOrNull(resourceName, configurationDto); + } + + public virtual async Task GetResourceOrNullAsync(string resourceName) + { + var configurationDto = await ConfigurationClient.GetAsync(); + return CreateLocalizationResourceFromConfigurationOrNull(resourceName, configurationDto); + } + + public virtual async Task GetResourceNamesAsync() + { + var configurationDto = await ConfigurationClient.GetAsync(); + return configurationDto + .Localization + .Resources + .Keys + .Where(x => !LocalizationOptions.Resources.ContainsKey(x)) + .ToArray(); +; } + + public virtual async Task GetResourcesAsync() + { + var configurationDto = await ConfigurationClient.GetAsync(); + var resources = new List(); + + foreach (var resource in configurationDto.Localization.Resources) + { + if (LocalizationOptions.Resources.ContainsKey(resource.Key)) + { + continue; + } + + resources.Add(CreateNonTypedLocalizationResource(resource.Key, resource.Value)); + } + + return resources.ToArray(); + } + + protected virtual LocalizationResourceBase CreateLocalizationResourceFromConfigurationOrNull( + string resourceName, + ApplicationConfigurationDto configurationDto) + { + var resourceDto = configurationDto.Localization.Resources.GetOrDefault(resourceName); + + if (resourceDto == null) + { + return null; + } + + return CreateNonTypedLocalizationResource(resourceName, resourceDto); + } + + protected virtual NonTypedLocalizationResource CreateNonTypedLocalizationResource( + string resourceName, + ApplicationLocalizationResourceDto resourceDto) + { + return new NonTypedLocalizationResource(resourceName) + .AddBaseResources(resourceDto.BaseResources); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteLocalizationContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteLocalizationContributor.cs index c01fc6a1af..e6e180c5f2 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteLocalizationContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteLocalizationContributor.cs @@ -1,15 +1,20 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; using Volo.Abp.Localization; namespace Volo.Abp.AspNetCore.Mvc.Client; public class RemoteLocalizationContributor : ILocalizationResourceContributor { - private LocalizationResource _resource; + public bool IsDynamic => true; + + private LocalizationResourceBase _resource; private ICachedApplicationConfigurationClient _applicationConfigurationClient; private ILogger _logger; @@ -21,50 +26,137 @@ public class RemoteLocalizationContributor : ILocalizationResourceContributor ?? NullLogger.Instance; } - public LocalizedString GetOrNull(string cultureName, string name) + public virtual LocalizedString GetOrNull(string cultureName, string name) + { + /* cultureName is not used because remote localization can only + * be done in the current culture. */ + + return GetOrNullInternal(_resource.ResourceName, name); + } + + protected virtual LocalizedString GetOrNullInternal(string resourceName, string name) { - var resource = GetResourceOrNull(); + var resource = GetResourceOrNull(resourceName); if (resource == null) { return null; } - var value = resource.GetOrDefault(name); - if (value == null) + var value = resource.Texts.GetOrDefault(name); + if (value != null) { - return null; + return new LocalizedString(name, value); + } + + foreach (var baseResource in resource.BaseResources) + { + value = GetOrNullInternal(baseResource, name); + if (value != null) + { + return new LocalizedString(name, value); + } + } + + return null; + } + + public virtual void Fill(string cultureName, Dictionary dictionary) + { + /* cultureName is not used because remote localization can only + * be done in the current culture. */ + + FillInternal(_resource.ResourceName, dictionary); + } + + protected virtual void FillInternal(string resourceName, Dictionary dictionary) + { + var resource = GetResourceOrNull(resourceName); + if (resource == null) + { + return; + } + + foreach (var baseResource in resource.BaseResources) + { + FillInternal(baseResource, dictionary); } - return new LocalizedString(name, value); + foreach (var keyValue in resource.Texts) + { + dictionary[keyValue.Key] = new LocalizedString(keyValue.Key, keyValue.Value); + } } - public void Fill(string cultureName, Dictionary dictionary) + public virtual async Task FillAsync(string cultureName, Dictionary dictionary) + { + /* cultureName is not used because remote localization can only + * be done in the current culture. */ + + await FillInternalAsync(_resource.ResourceName, dictionary); + } + + protected virtual async Task FillInternalAsync(string resourceName, Dictionary dictionary) { - var resource = GetResourceOrNull(); + var resource = await GetResourceOrNullAsync(resourceName); if (resource == null) { return; } + + foreach (var baseResource in resource.BaseResources) + { + await FillInternalAsync(baseResource, dictionary); + } - foreach (var keyValue in resource) + foreach (var keyValue in resource.Texts) { dictionary[keyValue.Key] = new LocalizedString(keyValue.Key, keyValue.Value); } } - private Dictionary GetResourceOrNull() + public virtual Task> GetSupportedCulturesAsync() + { + /* This contributor does not know all the supported cultures by the + remote localization resource, and it is not needed on the client side */ + return Task.FromResult((IEnumerable)Array.Empty()); + } + + protected virtual ApplicationLocalizationResourceDto GetResourceOrNull(string resourceName) { var applicationConfigurationDto = _applicationConfigurationClient.Get(); + return GetResourceOrNull(applicationConfigurationDto, resourceName); + } + + protected virtual async Task GetResourceOrNullAsync(string resourceName) + { + var applicationConfigurationDto = await _applicationConfigurationClient.GetAsync(); + return GetResourceOrNull(applicationConfigurationDto, resourceName); + } - var resource = applicationConfigurationDto + protected virtual ApplicationLocalizationResourceDto GetResourceOrNull( + ApplicationConfigurationDto applicationConfigurationDto, + string resourceName) + { + var resource = applicationConfigurationDto.Localization.Resources.GetOrDefault(resourceName); + if (resource != null) + { + return resource; + } + + var legacyResource = applicationConfigurationDto .Localization.Values - .GetOrDefault(_resource.ResourceName); + .GetOrDefault(resourceName); - if (resource == null) + if (legacyResource != null) { - _logger.LogWarning($"Could not find the localization resource {_resource.ResourceName} on the remote server!"); + return new ApplicationLocalizationResourceDto + { + Texts = legacyResource, + BaseResources = Array.Empty() + }; } - return resource; + _logger.LogWarning($"Could not find the localization resource {resourceName} on the remote server!"); + return null; } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs index 7bb2e67b49..405d43906d 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs @@ -15,6 +15,7 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu { protected IHttpContextAccessor HttpContextAccessor { get; } protected AbpApplicationConfigurationClientProxy ApplicationConfigurationAppService { get; } + protected AbpApplicationLocalizationClientProxy ApplicationLocalizationClientProxy { get; } protected ICurrentUser CurrentUser { get; } protected IDistributedCache Cache { get; } @@ -22,11 +23,13 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu IDistributedCache cache, AbpApplicationConfigurationClientProxy applicationConfigurationAppService, ICurrentUser currentUser, - IHttpContextAccessor httpContextAccessor) + IHttpContextAccessor httpContextAccessor, + AbpApplicationLocalizationClientProxy applicationLocalizationClientProxy) { ApplicationConfigurationAppService = applicationConfigurationAppService; CurrentUser = currentUser; HttpContextAccessor = httpContextAccessor; + ApplicationLocalizationClientProxy = applicationLocalizationClientProxy; Cache = cache; } @@ -42,7 +45,7 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu configuration = await Cache.GetOrAddAsync( cacheKey, - async () => await ApplicationConfigurationAppService.GetAsync(), + async () => await GetRemoteConfigurationAsync(), () => new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(300) //TODO: Should be configurable. @@ -57,6 +60,27 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu return configuration; } + private async Task GetRemoteConfigurationAsync() + { + var config = await ApplicationConfigurationAppService.GetAsync( + new ApplicationConfigurationRequestOptions + { + IncludeLocalizationResources = false + } + ); + + var localizationDto = await ApplicationLocalizationClientProxy.GetAsync( + new ApplicationLocalizationRequestDto { + CultureName = config.Localization.CurrentCulture.Name, + OnlyDynamics = true + } + ); + + config.Localization.Resources = localizationDto.Resources; + + return config; + } + public ApplicationConfigurationDto Get() { var cacheKey = CreateCacheKey(); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationRequestOptions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationRequestOptions.cs new file mode 100644 index 0000000000..a01534775a --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationRequestOptions.cs @@ -0,0 +1,9 @@ +namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; + +public class ApplicationConfigurationRequestOptions +{ + /// + /// Set to true to fill the Values property in . + /// + public bool IncludeLocalizationResources { get; set; } = true; +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationLocalizationConfigurationDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationLocalizationConfigurationDto.cs index f92c9d7bb9..b1c0c16e76 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationLocalizationConfigurationDto.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationLocalizationConfigurationDto.cs @@ -7,8 +7,23 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; [Serializable] public class ApplicationLocalizationConfigurationDto { + /// + /// This is not filled if is false. + /// public Dictionary> Values { get; set; } + /// + /// This property will never be filled by the application configuration endpoint + /// (by AbpApplicationConfigurationAppService). However, it is here to be filled + /// using the application localization endpoint (AbpApplicationLocalizationAppService). + /// This is an ugly design, but it is the best solution for backward-compability and + /// simple implementation. + /// + /// It's client's responsibility to fill this property + /// using the application localization endpoint. + /// + public Dictionary Resources { get; set; } = new(); + public List Languages { get; set; } public CurrentCultureDto CurrentCulture { get; set; } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationLocalizationDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationLocalizationDto.cs new file mode 100644 index 0000000000..d301490bed --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationLocalizationDto.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; + +[Serializable] +public class ApplicationLocalizationDto +{ + public Dictionary Resources { get; set; } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationLocalizationRequestDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationLocalizationRequestDto.cs new file mode 100644 index 0000000000..535d572b3f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationLocalizationRequestDto.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; + +public class ApplicationLocalizationRequestDto +{ + [Required] + public string CultureName { get; set; } + + public bool OnlyDynamics { get; set; } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationLocalizationResourceDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationLocalizationResourceDto.cs new file mode 100644 index 0000000000..a7200423d2 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationLocalizationResourceDto.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; + +[Serializable] +public class ApplicationLocalizationResourceDto +{ + public Dictionary Texts { get; set; } + + public string[] BaseResources { get; set; } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentCultureDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentCultureDto.cs index 8f1896e31f..83f1e0c803 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentCultureDto.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentCultureDto.cs @@ -1,5 +1,8 @@ -namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; +using System; +namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; + +[Serializable] public class CurrentCultureDto { public string DisplayName { get; set; } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/IAbpApplicationConfigurationAppService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/IAbpApplicationConfigurationAppService.cs index 9c8fd3d414..ae4f22cc06 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/IAbpApplicationConfigurationAppService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/IAbpApplicationConfigurationAppService.cs @@ -5,5 +5,5 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; public interface IAbpApplicationConfigurationAppService : IApplicationService { - Task GetAsync(); -} + Task GetAsync(ApplicationConfigurationRequestOptions options); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/IAbpApplicationLocalizationAppService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/IAbpApplicationLocalizationAppService.cs new file mode 100644 index 0000000000..af72c8f7ee --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/IAbpApplicationLocalizationAppService.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; + +public interface IAbpApplicationLocalizationAppService : IApplicationService +{ + Task GetAsync(ApplicationLocalizationRequestDto input); +} \ No newline at end of file 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 7bcc0b3cd9..85c9b86a3d 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 @@ -17,6 +17,7 @@ using Volo.Abp.Data; using Volo.Abp.Features; using Volo.Abp.GlobalFeatures; using Volo.Abp.Localization; +using Volo.Abp.Localization.External; using Volo.Abp.MultiTenancy; using Volo.Abp.Settings; using Volo.Abp.Timing; @@ -82,7 +83,7 @@ public class AbpApplicationConfigurationAppService : ApplicationService, IAbpApp _multiTenancyOptions = multiTenancyOptions.Value; } - public virtual async Task GetAsync() + public virtual async Task GetAsync(ApplicationConfigurationRequestOptions options) { //TODO: Optimize & cache..? @@ -93,7 +94,7 @@ public class AbpApplicationConfigurationAppService : ApplicationService, IAbpApp Auth = await GetAuthConfigAsync(), Features = await GetFeaturesConfigAsync(), GlobalFeatures = await GetGlobalFeaturesConfigAsync(), - Localization = await GetLocalizationConfigAsync(), + Localization = await GetLocalizationConfigAsync(options), CurrentUser = GetCurrentUser(), Setting = await GetSettingConfigAsync(), MultiTenancy = GetMultiTenancy(), @@ -205,26 +206,42 @@ public class AbpApplicationConfigurationAppService : ApplicationService, IAbpApp return authConfig; } - protected virtual async Task GetLocalizationConfigAsync() + protected virtual async Task GetLocalizationConfigAsync( + ApplicationConfigurationRequestOptions options) { var localizationConfig = new ApplicationLocalizationConfigurationDto(); localizationConfig.Languages.AddRange(await _languageProvider.GetLanguagesAsync()); - foreach (var resource in _localizationOptions.Resources.Values) + if (options.IncludeLocalizationResources) { - var dictionary = new Dictionary(); - - var localizer = (IStringLocalizer) _serviceProvider.GetRequiredService( - typeof(IStringLocalizer<>).MakeGenericType(resource.ResourceType) - ); - - foreach (var localizedString in localizer.GetAllStrings()) + var resourceNames = _localizationOptions + .Resources + .Values + .Select(x => x.ResourceName) + .Union( + await LazyServiceProvider + .LazyGetRequiredService() + .GetResourceNamesAsync() + ); + + foreach (var resourceName in resourceNames) { - dictionary[localizedString.Name] = localizedString.Value; - } + var dictionary = new Dictionary(); + + var localizer = await StringLocalizerFactory + .CreateByResourceNameOrNullAsync(resourceName); + + if (localizer != null) + { + foreach (var localizedString in await localizer.GetAllStringsAsync()) + { + dictionary[localizedString.Name] = localizedString.Value; + } + } - localizationConfig.Values[resource.ResourceName] = dictionary; + localizationConfig.Values[resourceName] = dictionary; + } } localizationConfig.CurrentCulture = GetCurrentCultureInfo(); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationController.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationController.cs index 6c410a8a7b..427aaf65a6 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationController.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationController.cs @@ -21,9 +21,10 @@ public class AbpApplicationConfigurationController : AbpControllerBase, IAbpAppl } [HttpGet] - public virtual async Task GetAsync() + public virtual async Task GetAsync( + ApplicationConfigurationRequestOptions options) { _antiForgeryManager.SetCookie(); - return await _applicationConfigurationAppService.GetAsync(); + return await _applicationConfigurationAppService.GetAsync(options); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationScriptController.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationScriptController.cs index 92a4bdef0d..7fafee261c 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationScriptController.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationScriptController.cs @@ -17,14 +17,14 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; [ApiExplorerSettings(IgnoreApi = true)] public class AbpApplicationConfigurationScriptController : AbpController { - private readonly IAbpApplicationConfigurationAppService _configurationAppService; + private readonly AbpApplicationConfigurationAppService _configurationAppService; private readonly IJsonSerializer _jsonSerializer; private readonly AbpAspNetCoreMvcOptions _options; private readonly IJavascriptMinifier _javascriptMinifier; private readonly IAbpAntiForgeryManager _antiForgeryManager; public AbpApplicationConfigurationScriptController( - IAbpApplicationConfigurationAppService configurationAppService, + AbpApplicationConfigurationAppService configurationAppService, IJsonSerializer jsonSerializer, IOptions options, IJavascriptMinifier javascriptMinifier, @@ -41,7 +41,13 @@ public class AbpApplicationConfigurationScriptController : AbpController [Produces(MimeTypes.Application.Javascript, MimeTypes.Text.Plain)] public async Task Get() { - var script = CreateAbpExtendScript(await _configurationAppService.GetAsync()); + var script = CreateAbpExtendScript( + await _configurationAppService.GetAsync( + new ApplicationConfigurationRequestOptions { + IncludeLocalizationResources = false + } + ) + ); _antiForgeryManager.SetCookie(); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationLocalizationAppService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationLocalizationAppService.cs new file mode 100644 index 0000000000..48d7f949c3 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationLocalizationAppService.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using Volo.Abp.Application.Services; +using Volo.Abp.Localization; +using Volo.Abp.Localization.External; + +namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; + +public class AbpApplicationLocalizationAppService : + ApplicationService, + IAbpApplicationLocalizationAppService +{ + protected IExternalLocalizationStore ExternalLocalizationStore { get; } + protected AbpLocalizationOptions LocalizationOptions { get; } + + public AbpApplicationLocalizationAppService( + IExternalLocalizationStore externalLocalizationStore, + IOptions localizationOptions) + { + ExternalLocalizationStore = externalLocalizationStore; + LocalizationOptions = localizationOptions.Value; + } + + public async Task GetAsync(ApplicationLocalizationRequestDto input) + { + using (CultureHelper.Use(input.CultureName)) + { + var resources = LocalizationOptions + .Resources + .Values + .Union( + await ExternalLocalizationStore.GetResourcesAsync() + ).ToArray(); + + var localizationConfig = new ApplicationLocalizationDto { + Resources = new Dictionary(resources.Length) + }; + + foreach (var resource in resources) + { + var dictionary = new Dictionary(); + var localizer = await StringLocalizerFactory.CreateByResourceNameOrNullAsync(resource.ResourceName); + if (localizer != null) + { + Dictionary staticLocalizedStrings = null; + + if (input.OnlyDynamics) + { + staticLocalizedStrings = (await localizer.GetAllStringsAsync( + includeParentCultures: true, + includeBaseLocalizers: false, + includeDynamicContributors: false + )).ToDictionary(x => x.Name); + } + + var localizedStringsWithDynamics = await localizer.GetAllStringsAsync( + includeParentCultures: true, + includeBaseLocalizers: false, + includeDynamicContributors: true + ); + + foreach (var localizedString in localizedStringsWithDynamics) + { + if (input.OnlyDynamics) + { + var staticLocalizedString = staticLocalizedStrings.GetOrDefault(localizedString.Name); + if (staticLocalizedString != null && + localizedString.Value == staticLocalizedString.Value) + { + continue; + } + } + + dictionary[localizedString.Name] = localizedString.Value; + } + } + + localizationConfig.Resources[resource.ResourceName] = + new ApplicationLocalizationResourceDto { + Texts = dictionary, + BaseResources = resource.BaseResourceNames.ToArray() + }; + } + + return localizationConfig; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationLocalizationController.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationLocalizationController.cs new file mode 100644 index 0000000000..a056746022 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationLocalizationController.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; + +[Area("abp")] +[RemoteService(Name = "abp")] +[Route("api/abp/application-localization")] +public class AbpApplicationLocalizationController: AbpControllerBase, IAbpApplicationLocalizationAppService +{ + private readonly IAbpApplicationLocalizationAppService _localizationAppService; + + public AbpApplicationLocalizationController(IAbpApplicationLocalizationAppService localizationAppService) + { + _localizationAppService = localizationAppService; + } + + [HttpGet] + public virtual async Task GetAsync(ApplicationLocalizationRequestDto input) + { + return await _localizationAppService.GetAsync(input); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs index 8acfcefd3d..0f9b05830d 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ObjectExtending/CachedObjectExtensionsDtoService.cs @@ -204,9 +204,7 @@ public class CachedObjectExtensionsDtoService : ICachedObjectExtensionsDtoServic { return new LocalizableStringDto( localizableStringInstance.Name, - localizableStringInstance.ResourceType != null - ? LocalizationResourceNameAttribute.GetName(localizableStringInstance.ResourceType) - : null + localizableStringInstance.ResourceName ); } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpApplicationLocalizationScriptController.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpApplicationLocalizationScriptController.cs new file mode 100644 index 0000000000..d8da85f871 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpApplicationLocalizationScriptController.cs @@ -0,0 +1,69 @@ +using System; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; +using Volo.Abp.Auditing; +using Volo.Abp.Http; +using Volo.Abp.Json; +using Volo.Abp.Localization; +using Volo.Abp.Minify.Scripts; + +namespace Volo.Abp.AspNetCore.Mvc.Localization; + +[Area("Abp")] +[Route("Abp/ApplicationLocalizationScript")] +[DisableAuditing] +[RemoteService(false)] +[ApiExplorerSettings(IgnoreApi = true)] +public class AbpApplicationLocalizationScriptController : AbpController +{ + protected AbpApplicationLocalizationAppService LocalizationAppService { get; } + protected AbpAspNetCoreMvcOptions Options { get; } + protected IJsonSerializer JsonSerializer { get; } + protected IJavascriptMinifier JavascriptMinifier { get; } + + public AbpApplicationLocalizationScriptController( + AbpApplicationLocalizationAppService localizationAppService, + IOptions options, + IJsonSerializer jsonSerializer, + IJavascriptMinifier javascriptMinifier) + { + LocalizationAppService = localizationAppService; + JsonSerializer = jsonSerializer; + JavascriptMinifier = javascriptMinifier; + Options = options.Value; + } + + [HttpGet] + [Produces(MimeTypes.Application.Javascript, MimeTypes.Text.Plain)] + public async Task GetAsync(ApplicationLocalizationRequestDto input) + { + var script = CreateScript( + await LocalizationAppService.GetAsync(input) + ); + + return Content( + Options.MinifyGeneratedScript == true + ? JavascriptMinifier.Minify(script) + : script, + MimeTypes.Application.Javascript + ); + } + + private string CreateScript(ApplicationLocalizationDto localizationDto) + { + var script = new StringBuilder(); + + script.AppendLine("(function(){"); + script.AppendLine(); + script.AppendLine( + $"$.extend(true, abp.localization, {JsonSerializer.Serialize(localizationDto, indented: true)})"); + script.AppendLine(); + script.Append("})();"); + + return script.ToString(); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpLanguagesController.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpLanguagesController.cs index dc44f12124..9f5d6aa530 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpLanguagesController.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpLanguagesController.cs @@ -3,12 +3,14 @@ using Microsoft.AspNetCore.Mvc; using System; using System.Threading.Tasks; using Microsoft.AspNetCore.RequestLocalization; +using Volo.Abp.Auditing; using Volo.Abp.Localization; namespace Volo.Abp.AspNetCore.Mvc.Localization; [Area("Abp")] [Route("Abp/Languages/[action]")] +[DisableAuditing] [RemoteService(false)] [ApiExplorerSettings(IgnoreApi = true)] public class AbpLanguagesController : AbpController diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ProxyScripting/ServiceProxyGenerationModel.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ProxyScripting/ServiceProxyGenerationModel.cs index 20ac2e4cb8..3501882e3a 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ProxyScripting/ServiceProxyGenerationModel.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ProxyScripting/ServiceProxyGenerationModel.cs @@ -5,7 +5,7 @@ using Volo.Abp.Http.ProxyScripting.Generators.JQuery; namespace Volo.Abp.AspNetCore.Mvc.ProxyScripting; -public class ServiceProxyGenerationModel //: TODO: IShouldNormalize +public class ServiceProxyGenerationModel { public string Type { get; set; } diff --git a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/AbpRequestLocalizationMiddleware.cs b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/AbpRequestLocalizationMiddleware.cs index 1324df0865..6fd02a0470 100644 --- a/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/AbpRequestLocalizationMiddleware.cs +++ b/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/RequestLocalization/AbpRequestLocalizationMiddleware.cs @@ -27,7 +27,9 @@ public class AbpRequestLocalizationMiddleware : IMiddleware, ITransientDependenc { var middleware = new RequestLocalizationMiddleware( next, - new OptionsWrapper(await _requestLocalizationOptionsProvider.GetLocalizationOptionsAsync()), + new OptionsWrapper( + await _requestLocalizationOptionsProvider.GetLocalizationOptionsAsync() + ), _loggerFactory ); 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.Caching/Volo/Abp/Caching/DistributedCache.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs index c51ec76fc1..a72a369655 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs @@ -21,26 +21,115 @@ namespace Volo.Abp.Caching; /// /// The type of cache item being cached. public class DistributedCache : - DistributedCache, IDistributedCache where TCacheItem : class { - public DistributedCache( - IOptions distributedCacheOption, - IDistributedCache cache, - ICancellationTokenProvider cancellationTokenProvider, - IDistributedCacheSerializer serializer, - IDistributedCacheKeyNormalizer keyNormalizer, - IServiceScopeFactory serviceScopeFactory, - IUnitOfWorkManager unitOfWorkManager) : base( - distributedCacheOption: distributedCacheOption, - cache: cache, - cancellationTokenProvider: cancellationTokenProvider, - serializer: serializer, - keyNormalizer: keyNormalizer, - serviceScopeFactory: serviceScopeFactory, - unitOfWorkManager: unitOfWorkManager) + public IDistributedCache InternalCache { get; } + + public DistributedCache(IDistributedCache internalCache) + { + InternalCache = internalCache; + } + + public TCacheItem Get(string key, bool? hideErrors = null, bool considerUow = false) + { + return InternalCache.Get(key, hideErrors, considerUow); + } + + public KeyValuePair[] GetMany(IEnumerable keys, bool? hideErrors = null, bool considerUow = false) + { + return InternalCache.GetMany(keys, hideErrors, considerUow); + } + + public Task[]> GetManyAsync(IEnumerable keys, bool? hideErrors = null, bool considerUow = false, CancellationToken token = default) + { + return InternalCache.GetManyAsync(keys, hideErrors, considerUow, token); + } + + public Task GetAsync(string key, bool? hideErrors = null, bool considerUow = false, CancellationToken token = default) + { + return InternalCache.GetAsync(key, hideErrors, considerUow, token); + } + + public TCacheItem GetOrAdd(string key, Func factory, Func optionsFactory = null, bool? hideErrors = null, bool considerUow = false) + { + return InternalCache.GetOrAdd(key, factory, optionsFactory, hideErrors, considerUow); + } + + public Task GetOrAddAsync(string key, Func> factory, Func optionsFactory = null, bool? hideErrors = null, bool considerUow = false, CancellationToken token = default) + { + return InternalCache.GetOrAddAsync(key, factory, optionsFactory, hideErrors, considerUow, token); + } + + public KeyValuePair[] GetOrAddMany(IEnumerable keys, Func, List>> factory, Func optionsFactory = null, bool? hideErrors = null, bool considerUow = false) + { + return InternalCache.GetOrAddMany(keys, factory, optionsFactory, hideErrors, considerUow); + } + + public Task[]> GetOrAddManyAsync(IEnumerable keys, Func, Task>>> factory, Func optionsFactory = null, bool? hideErrors = null, bool considerUow = false, CancellationToken token = default) + { + return InternalCache.GetOrAddManyAsync(keys, factory, optionsFactory, hideErrors, considerUow, token); + } + + public void Set(string key, TCacheItem value, DistributedCacheEntryOptions options = null, bool? hideErrors = null, bool considerUow = false) + { + InternalCache.Set(key, value, options, hideErrors, considerUow); + } + + public Task SetAsync(string key, TCacheItem value, DistributedCacheEntryOptions options = null, bool? hideErrors = null, bool considerUow = false, CancellationToken token = default) + { + return InternalCache.SetAsync(key, value, options, hideErrors, considerUow, token); + } + + public void SetMany(IEnumerable> items, DistributedCacheEntryOptions options = null, bool? hideErrors = null, bool considerUow = false) + { + InternalCache.SetMany(items, options, hideErrors, considerUow); + } + + public Task SetManyAsync(IEnumerable> items, DistributedCacheEntryOptions options = null, bool? hideErrors = null, bool considerUow = false, CancellationToken token = default) + { + return InternalCache.SetManyAsync(items, options, hideErrors, considerUow, token); + } + + public void Refresh(string key, bool? hideErrors = null) + { + InternalCache.Refresh(key, hideErrors); + } + + public Task RefreshAsync(string key, bool? hideErrors = null, CancellationToken token = default) + { + return InternalCache.RefreshAsync(key, hideErrors, token); + } + + public void RefreshMany(IEnumerable keys, bool? hideErrors = null) + { + InternalCache.RefreshMany(keys, hideErrors); + } + + public Task RefreshManyAsync(IEnumerable keys, bool? hideErrors = null, CancellationToken token = default) + { + return InternalCache.RefreshManyAsync(keys, hideErrors, token); + } + + public void Remove(string key, bool? hideErrors = null, bool considerUow = false) + { + InternalCache.Remove(key, hideErrors, considerUow); + } + + public Task RemoveAsync(string key, bool? hideErrors = null, bool considerUow = false, CancellationToken token = default) + { + return InternalCache.RemoveAsync(key, hideErrors, considerUow, token); + } + + public void RemoveMany(IEnumerable keys, bool? hideErrors = null, bool considerUow = false) + { + InternalCache.RemoveMany(keys, hideErrors, considerUow); + } + + public Task RemoveManyAsync(IEnumerable keys, bool? hideErrors = null, bool considerUow = false, + CancellationToken token = default) { + return InternalCache.RemoveManyAsync(keys, hideErrors, considerUow, token); } } diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/IDistributedCache.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/IDistributedCache.cs index 21a2842e15..ab3ca560a8 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/IDistributedCache.cs +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/IDistributedCache.cs @@ -14,6 +14,7 @@ namespace Volo.Abp.Caching; public interface IDistributedCache : IDistributedCache where TCacheItem : class { + IDistributedCache InternalCache { get; } } /// diff --git a/framework/src/Volo.Abp.Core/System/AbpObjectExtensions.cs b/framework/src/Volo.Abp.Core/System/AbpObjectExtensions.cs index 2d19fa8bc2..24ece0e52e 100644 --- a/framework/src/Volo.Abp.Core/System/AbpObjectExtensions.cs +++ b/framework/src/Volo.Abp.Core/System/AbpObjectExtensions.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Globalization; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; namespace System; @@ -17,6 +18,7 @@ public static class AbpObjectExtensions /// Type to be casted /// Object to cast /// Casted object + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T As(this object obj) where T : class { diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Localization/CultureHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Localization/CultureHelper.cs index dded0d794e..74a62116a4 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Localization/CultureHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Localization/CultureHelper.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Linq; using JetBrains.Annotations; namespace Volo.Abp.Localization; @@ -59,4 +60,45 @@ public static class CultureHelper { return new CultureInfo(cultureName).Parent.Name; } + + public static bool IsCompatibleCulture( + string sourceCultureName, + string targetCultureName) + { + if (sourceCultureName == targetCultureName) + { + return true; + } + + if (sourceCultureName.StartsWith("zh") && targetCultureName.StartsWith("zh")) + { + var culture = new CultureInfo(targetCultureName); + do + { + if (culture.Name == sourceCultureName) + { + return true; + } + + culture = new CultureInfo(culture.Name).Parent; + } while (!culture.Equals(CultureInfo.InvariantCulture)); + } + + if (sourceCultureName.Contains("-")) + { + return false; + } + + if (!targetCultureName.Contains("-")) + { + return false; + } + + if (sourceCultureName == GetBaseCultureName(targetCultureName)) + { + return true; + } + + return false; + } } diff --git a/framework/src/Volo.Abp.Http.Client/Microsoft/Extensions/DependencyInjection/ServiceCollectionHttpClientProxyExtensions.cs b/framework/src/Volo.Abp.Http.Client/Microsoft/Extensions/DependencyInjection/ServiceCollectionHttpClientProxyExtensions.cs index 7bc239813d..57233ac972 100644 --- a/framework/src/Volo.Abp.Http.Client/Microsoft/Extensions/DependencyInjection/ServiceCollectionHttpClientProxyExtensions.cs +++ b/framework/src/Volo.Abp.Http.Client/Microsoft/Extensions/DependencyInjection/ServiceCollectionHttpClientProxyExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Net.Http; using System.Reflection; using Castle.DynamicProxy; using JetBrains.Annotations; @@ -196,6 +197,16 @@ public static class ServiceCollectionHttpClientProxyExtensions { clientBuildAction(remoteServiceConfigurationName, provider, client); } + }).ConfigurePrimaryHttpMessageHandler((provider) => + { + var handler = new HttpClientHandler { UseCookies = false }; + + foreach (var handlerAction in preOptions.ProxyClientHandlerActions) + { + handlerAction(remoteServiceConfigurationName, provider, handler); + } + + return handler; }); foreach (var clientBuildAction in preOptions.ProxyClientBuildActions) diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientBuilderOptions.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientBuilderOptions.cs index 240f35fe0a..b2ffae98a5 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientBuilderOptions.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientBuilderOptions.cs @@ -12,11 +12,14 @@ public class AbpHttpClientBuilderOptions internal HashSet ConfiguredProxyClients { get; } public List> ProxyClientActions { get; } + + public List> ProxyClientHandlerActions { get; } public AbpHttpClientBuilderOptions() { ProxyClientBuildActions = new List>(); ConfiguredProxyClients = new HashSet(); ProxyClientActions = new List>(); + ProxyClientHandlerActions = new List>(); } } 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 896fdb6fa0..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,10 +1,66 @@ -namespace Microsoft.Extensions.Localization; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Volo.Abp; + +namespace Microsoft.Extensions.Localization; public static class AbpStringLocalizerFactoryExtensions { + [CanBeNull] public static IStringLocalizer CreateDefaultOrNull(this IStringLocalizerFactory localizerFactory) { - return (localizerFactory as IAbpStringLocalizerFactoryWithDefaultResourceSupport) + return (localizerFactory as IAbpStringLocalizerFactory) ?.CreateDefaultOrNull(); } + + [CanBeNull] + public static IStringLocalizer CreateByResourceNameOrNull( + this IStringLocalizerFactory localizerFactory, + string resourceName) + { + return (localizerFactory as IAbpStringLocalizerFactory) + ?.CreateByResourceNameOrNull(resourceName); + } + + [NotNull] + public static IStringLocalizer CreateByResourceName( + this IStringLocalizerFactory localizerFactory, + string resourceName) + { + var localizer = localizerFactory.CreateByResourceNameOrNull(resourceName); + if (localizer == null) + { + throw new AbpException("Couldn't find a localizer with given resource name: " + resourceName); + } + + 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 new file mode 100644 index 0000000000..8f6699a13d --- /dev/null +++ b/framework/src/Volo.Abp.Localization.Abstractions/Microsoft/Extensions/Localization/IAbpStringLocalizerFactory.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Microsoft.Extensions.Localization; + +public interface IAbpStringLocalizerFactory +{ + [CanBeNull] + IStringLocalizer CreateDefaultOrNull(); + + [CanBeNull] + IStringLocalizer CreateByResourceNameOrNull([NotNull] string resourceName); + + [ItemCanBeNull] + Task CreateByResourceNameOrNullAsync([NotNull] string resourceName); +} diff --git a/framework/src/Volo.Abp.Localization.Abstractions/Microsoft/Extensions/Localization/IAbpStringLocalizerFactoryWithDefaultResourceSupport.cs b/framework/src/Volo.Abp.Localization.Abstractions/Microsoft/Extensions/Localization/IAbpStringLocalizerFactoryWithDefaultResourceSupport.cs deleted file mode 100644 index 0ab20aec4b..0000000000 --- a/framework/src/Volo.Abp.Localization.Abstractions/Microsoft/Extensions/Localization/IAbpStringLocalizerFactoryWithDefaultResourceSupport.cs +++ /dev/null @@ -1,9 +0,0 @@ -using JetBrains.Annotations; - -namespace Microsoft.Extensions.Localization; - -public interface IAbpStringLocalizerFactoryWithDefaultResourceSupport -{ - [CanBeNull] - IStringLocalizer CreateDefaultOrNull(); -} 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 88eb8a399d..a98f207a55 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,11 +1,15 @@ 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; } + [CanBeNull] public Type ResourceType { get; } @@ -16,22 +20,53 @@ public class LocalizableString : ILocalizableString { Name = Check.NotNullOrEmpty(name, nameof(name)); ResourceType = resourceType; + + if (resourceType != null) + { + ResourceName = LocalizationResourceNameAttribute.GetName(resourceType); + } + } + + public LocalizableString([NotNull] string name, [CanBeNull] string resourceName = null) + { + Name = Check.NotNullOrEmpty(name, nameof(name)); + ResourceName = resourceName; } public LocalizedString Localize(IStringLocalizerFactory stringLocalizerFactory) { - var localizer = ResourceType != null - ? stringLocalizerFactory.Create(ResourceType) - : stringLocalizerFactory.CreateDefaultOrNull(); + var localizer = CreateStringLocalizerOrNull(stringLocalizerFactory); + if (localizer == null) + { + return new LocalizedString(Name, Name, resourceNotFound: true); + } + + 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; + } + + public async Task LocalizeAsync(IStringLocalizerFactory stringLocalizerFactory) + { + var localizer = await CreateStringLocalizerOrNullAsync(stringLocalizerFactory); if (localizer == null) { - throw new AbpException($"Set {nameof(ResourceType)} or configure the default localization resource type (in the AbpLocalizationOptions)!"); + throw new AbpException($"Set {nameof(ResourceName)} or configure the default localization resource type (in the AbpLocalizationOptions)!"); } var result = localizer[Name]; - if (result.ResourceNotFound && ResourceType != null) + if (result.ResourceNotFound && ResourceName != null) { /* Search in the default resource if not found in the provided resource */ localizer = stringLocalizerFactory.CreateDefaultOrNull(); @@ -44,8 +79,56 @@ public class LocalizableString : ILocalizableString return result; } + private IStringLocalizer CreateStringLocalizerOrNull(IStringLocalizerFactory stringLocalizerFactory) + { + if (ResourceType != null) + { + return stringLocalizerFactory.Create(ResourceType); + } + + if (ResourceName != null) + { + var localizerByName = stringLocalizerFactory.CreateByResourceNameOrNull(ResourceName); + if (localizerByName != null) + { + return localizerByName; + } + } + + 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) { - return new LocalizableString(typeof(TResource), name); + return Create(typeof(TResource), name); + } + + public static LocalizableString Create(Type resourceType,[NotNull] string name) + { + return new LocalizableString(resourceType, name); + } + + public static LocalizableString Create([NotNull] string name, [CanBeNull] string resourceName = null) + { + return new LocalizableString(name, resourceName); } } 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.csproj b/framework/src/Volo.Abp.Localization/Volo.Abp.Localization.csproj index 64352257f3..53e71d1c38 100644 --- a/framework/src/Volo.Abp.Localization/Volo.Abp.Localization.csproj +++ b/framework/src/Volo.Abp.Localization/Volo.Abp.Localization.csproj @@ -22,6 +22,8 @@ + + diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs index 61621c2479..cf5c1ebb82 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs @@ -4,13 +4,14 @@ using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Resources; +using System.Threading.Tasks; using Microsoft.Extensions.Localization; namespace Volo.Abp.Localization; -public class AbpDictionaryBasedStringLocalizer : IStringLocalizer, IStringLocalizerSupportsInheritance +public class AbpDictionaryBasedStringLocalizer : IAbpStringLocalizer { - public LocalizationResource Resource { get; } + public LocalizationResourceBase Resource { get; } public List BaseLocalizers { get; } @@ -20,7 +21,10 @@ public class AbpDictionaryBasedStringLocalizer : IStringLocalizer, IStringLocali public virtual LocalizedString this[string name, params object[] arguments] => GetLocalizedStringFormatted(name, arguments); - public AbpDictionaryBasedStringLocalizer(LocalizationResource resource, List baseLocalizers, AbpLocalizationOptions abpLocalizationOptions) + public AbpDictionaryBasedStringLocalizer( + LocalizationResourceBase resource, + List baseLocalizers, + AbpLocalizationOptions abpLocalizationOptions) { Resource = resource; BaseLocalizers = baseLocalizers; @@ -34,16 +38,46 @@ public class AbpDictionaryBasedStringLocalizer : IStringLocalizer, IStringLocali includeParentCultures ); } + + public async Task> GetAllStringsAsync(bool includeParentCultures) + { + return await GetAllStringsAsync( + CultureInfo.CurrentUICulture.Name, + includeParentCultures + ); + } - public IEnumerable GetAllStrings(bool includeParentCultures, bool includeBaseLocalizers) + public IEnumerable GetAllStrings( + bool includeParentCultures, + bool includeBaseLocalizers, + bool includeDynamicContributors) { return GetAllStrings( CultureInfo.CurrentUICulture.Name, includeParentCultures, - includeBaseLocalizers + includeBaseLocalizers, + includeDynamicContributors + ); + } + + public async Task> GetAllStringsAsync( + bool includeParentCultures, + bool includeBaseLocalizers, + bool includeDynamicContributors) + { + return await GetAllStringsAsync( + CultureInfo.CurrentUICulture.Name, + includeParentCultures, + includeBaseLocalizers, + includeDynamicContributors ); } + public Task> GetSupportedCulturesAsync() + { + return Resource.Contributors.GetSupportedCulturesAsync(); + } + protected virtual LocalizedString GetLocalizedStringFormatted(string name, params object[] arguments) { return GetLocalizedStringFormatted(name, CultureInfo.CurrentUICulture.Name, arguments); @@ -84,7 +118,10 @@ public class AbpDictionaryBasedStringLocalizer : IStringLocalizer, IStringLocali return value; } - protected virtual LocalizedString GetLocalizedStringOrNull(string name, string cultureName, bool tryDefaults = true) + protected virtual LocalizedString GetLocalizedStringOrNull( + string name, + string cultureName, + bool tryDefaults = true) { //Try to get from original dictionary (with country code) var strOriginal = Resource.Contributors.GetOrNull(cultureName, name); @@ -131,7 +168,67 @@ public class AbpDictionaryBasedStringLocalizer : IStringLocalizer, IStringLocali protected virtual IReadOnlyList GetAllStrings( string cultureName, bool includeParentCultures = true, - bool includeBaseLocalizers = true) + bool includeBaseLocalizers = true, + bool includeDynamicContributors = true) + { + //TODO: Can be optimized (example: if it's already default dictionary, skip overriding) + + var allStrings = new Dictionary(); + + if (includeBaseLocalizers) + { + foreach (var baseLocalizer in BaseLocalizers.Select(l => l)) + { + using (CultureHelper.Use(CultureInfo.GetCultureInfo(cultureName))) + { + //TODO: Try/catch is a workaround here! + try + { + var baseLocalizedString = baseLocalizer.GetAllStrings( + includeParentCultures, + includeBaseLocalizers, // Always true, I know! + includeDynamicContributors + ); + + foreach (var localizedString in baseLocalizedString) + { + allStrings[localizedString.Name] = localizedString; + } + } + catch (MissingManifestResourceException) + { + + } + } + } + } + + if (includeParentCultures) + { + //Fill all strings from default culture + if (!Resource.DefaultCultureName.IsNullOrEmpty()) + { + Resource.Contributors.Fill(Resource.DefaultCultureName, allStrings, includeDynamicContributors); + } + + //Overwrite all strings from the language based on country culture + if (cultureName.Contains("-")) + { + Resource.Contributors.Fill(CultureHelper.GetBaseCultureName(cultureName), allStrings, includeDynamicContributors); + } + } + + //Overwrite all strings from the original culture + Resource.Contributors.Fill(cultureName, allStrings, includeDynamicContributors); + + return allStrings.Values.ToImmutableList(); + } + + protected virtual async Task> GetAllStringsAsync( + string cultureName, + bool includeParentCultures = true, + bool includeBaseLocalizers = true, + bool includeDynamicContributors = true) { //TODO: Can be optimized (example: if it's already default dictionary, skip overriding) @@ -146,7 +243,12 @@ public class AbpDictionaryBasedStringLocalizer : IStringLocalizer, IStringLocali //TODO: Try/catch is a workaround here! try { - var baseLocalizedString = baseLocalizer.GetAllStrings(includeParentCultures); + var baseLocalizedString = await baseLocalizer.GetAllStringsAsync( + includeParentCultures, + includeBaseLocalizers, // Always true, I know! + includeDynamicContributors + ); + foreach (var localizedString in baseLocalizedString) { allStrings[localizedString.Name] = localizedString; @@ -165,23 +267,35 @@ public class AbpDictionaryBasedStringLocalizer : IStringLocalizer, IStringLocali //Fill all strings from default culture if (!Resource.DefaultCultureName.IsNullOrEmpty()) { - Resource.Contributors.Fill(Resource.DefaultCultureName, allStrings); + await Resource.Contributors.FillAsync( + Resource.DefaultCultureName, + allStrings, + includeDynamicContributors + ); } //Overwrite all strings from the language based on country culture if (cultureName.Contains("-")) { - Resource.Contributors.Fill(CultureHelper.GetBaseCultureName(cultureName), allStrings); + await Resource.Contributors.FillAsync( + CultureHelper.GetBaseCultureName(cultureName), + allStrings, + includeDynamicContributors + ); } } //Overwrite all strings from the original culture - Resource.Contributors.Fill(cultureName, allStrings); + await Resource.Contributors.FillAsync( + cultureName, + allStrings, + includeDynamicContributors + ); return allStrings.Values.ToImmutableList(); } - public class CultureWrapperStringLocalizer : IStringLocalizer, IStringLocalizerSupportsInheritance + public class CultureWrapperStringLocalizer : IAbpStringLocalizer { private readonly string _cultureName; private readonly AbpDictionaryBasedStringLocalizer _innerLocalizer; @@ -201,9 +315,28 @@ public class AbpDictionaryBasedStringLocalizer : IStringLocalizer, IStringLocali return _innerLocalizer.GetAllStrings(_cultureName, includeParentCultures); } - public IEnumerable GetAllStrings(bool includeParentCultures, bool includeBaseLocalizers) + public IEnumerable GetAllStrings(bool includeParentCultures, bool includeBaseLocalizers, bool includeDynamicContributors) + { + return _innerLocalizer.GetAllStrings(_cultureName, includeParentCultures, includeBaseLocalizers, includeDynamicContributors); + } + + public Task> GetAllStringsAsync(bool includeParentCultures) + { + return _innerLocalizer.GetAllStringsAsync(includeParentCultures); + } + + public Task> GetAllStringsAsync(bool includeParentCultures, bool includeBaseLocalizers, bool includeDynamicContributors) + { + return _innerLocalizer.GetAllStringsAsync( + includeParentCultures, + includeBaseLocalizers, + includeDynamicContributors + ); + } + + public Task> GetSupportedCulturesAsync() { - return _innerLocalizer.GetAllStrings(_cultureName, includeParentCultures, includeBaseLocalizers); + return _innerLocalizer.GetSupportedCulturesAsync(); } } } diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpLocalizationModule.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpLocalizationModule.cs index 4ce8c32f88..85be669593 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpLocalizationModule.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpLocalizationModule.cs @@ -1,6 +1,7 @@ using Volo.Abp.Localization.Resources.AbpLocalization; using Volo.Abp.Modularity; using Volo.Abp.Settings; +using Volo.Abp.Threading; using Volo.Abp.VirtualFileSystem; namespace Volo.Abp.Localization; @@ -8,7 +9,8 @@ namespace Volo.Abp.Localization; [DependsOn( typeof(AbpVirtualFileSystemModule), typeof(AbpSettingsModule), - typeof(AbpLocalizationAbstractionsModule) + typeof(AbpLocalizationAbstractionsModule), + typeof(AbpThreadingModule) )] public class AbpLocalizationModule : AbpModule { diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpStringLocalizerExtensions.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpStringLocalizerExtensions.cs index 8201043c6e..9b3bb2c201 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpStringLocalizerExtensions.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpStringLocalizerExtensions.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Reflection; +using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.Localization; using Volo.Abp.DynamicProxy; @@ -39,14 +41,16 @@ public static class AbpStringLocalizerExtensions public static IEnumerable GetAllStrings( this IStringLocalizer stringLocalizer, bool includeParentCultures, - bool includeBaseLocalizers) + bool includeBaseLocalizers, + bool includeDynamicContributors) { - var internalLocalizer = (ProxyHelper.UnProxy(stringLocalizer) as IStringLocalizer).GetInternalLocalizer(); - if (internalLocalizer is IStringLocalizerSupportsInheritance stringLocalizerSupportsInheritance) + var internalLocalizer = ((IStringLocalizer)ProxyHelper.UnProxy(stringLocalizer)).GetInternalLocalizer(); + if (internalLocalizer is IAbpStringLocalizer abpStringLocalizer) { - return stringLocalizerSupportsInheritance.GetAllStrings( + return abpStringLocalizer.GetAllStrings( includeParentCultures, - includeBaseLocalizers + includeBaseLocalizers, + includeDynamicContributors ); } @@ -54,4 +58,59 @@ public static class AbpStringLocalizerExtensions includeParentCultures ); } + + public static async Task> GetAllStringsAsync( + this IStringLocalizer stringLocalizer, + bool includeParentCultures, + bool includeBaseLocalizers, + bool includeDynamicContributors) + { + var internalLocalizer = ((IStringLocalizer)ProxyHelper.UnProxy(stringLocalizer)).GetInternalLocalizer(); + if (internalLocalizer is IAbpStringLocalizer abpStringLocalizer) + { + return await abpStringLocalizer.GetAllStringsAsync( + includeParentCultures, + includeBaseLocalizers, + includeDynamicContributors + ); + } + + return stringLocalizer.GetAllStrings( + includeParentCultures + ); + } + + public static async Task> GetSupportedCulturesAsync(this IStringLocalizer localizer) + { + var internalLocalizer = ((IStringLocalizer)ProxyHelper.UnProxy(localizer)).GetInternalLocalizer(); + if (internalLocalizer is IAbpStringLocalizer abpStringLocalizer) + { + return await abpStringLocalizer.GetSupportedCulturesAsync(); + } + + return Array.Empty(); + } + + public static Task> GetAllStringsAsync( + this IStringLocalizer localizer) + { + return localizer.GetAllStringsAsync(includeParentCultures: true); + } + + public static Task> GetAllStringsAsync( + this IStringLocalizer localizer, + bool includeParentCultures) + { + Check.NotNull(localizer, nameof(localizer)); + + var internalLocalizer = ((IStringLocalizer)ProxyHelper.UnProxy(localizer)).GetInternalLocalizer(); + if (internalLocalizer is IAbpStringLocalizer abpStringLocalizer) + { + return abpStringLocalizer.GetAllStringsAsync(includeParentCultures: includeParentCultures); + } + + return Task.FromResult( + localizer.GetAllStrings(includeParentCultures: true) + ); + } } 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 3f47fd779d..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,60 +2,182 @@ 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; -public class AbpStringLocalizerFactory : IStringLocalizerFactory, IAbpStringLocalizerFactoryWithDefaultResourceSupport +public class AbpStringLocalizerFactory : IStringLocalizerFactory, IAbpStringLocalizerFactory { protected internal AbpLocalizationOptions AbpLocalizationOptions { get; } protected ResourceManagerStringLocalizerFactory InnerFactory { get; } protected IServiceProvider ServiceProvider { get; } - protected ConcurrentDictionary LocalizerCache { get; } + protected IExternalLocalizationStore ExternalLocalizationStore { get; } + protected ConcurrentDictionary LocalizerCache { get; } + protected SemaphoreSlim LocalizerCacheSemaphore { get; } = new(1, 1); - //TODO: It's better to use decorator pattern for IStringLocalizerFactory instead of getting ResourceManagerStringLocalizerFactory as a dependency. public AbpStringLocalizerFactory( ResourceManagerStringLocalizerFactory innerFactory, IOptions abpLocalizationOptions, - IServiceProvider serviceProvider) + IServiceProvider serviceProvider, + IExternalLocalizationStore externalLocalizationStore) { InnerFactory = innerFactory; ServiceProvider = serviceProvider; + ExternalLocalizationStore = externalLocalizationStore; AbpLocalizationOptions = abpLocalizationOptions.Value; - LocalizerCache = new ConcurrentDictionary(); + LocalizerCache = new ConcurrentDictionary(); } public virtual IStringLocalizer Create(Type resourceType) { - var resource = AbpLocalizationOptions.Resources.GetOrDefault(resourceType); + return Create(resourceType, lockCache: true); + } + + private IStringLocalizer Create(Type resourceType, bool lockCache) + { + var resource = AbpLocalizationOptions.Resources.GetOrNull(resourceType); if (resource == null) { return InnerFactory.Create(resourceType); } - if (LocalizerCache.TryGetValue(resourceType, out var cacheItem)) + 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) + { + resource = ExternalLocalizationStore.GetResourceOrNull(resourceName); + if (resource == null) + { + return null; + } + } + + return CreateInternal(resourceName, resource, lockCache); + } + + public Task CreateByResourceNameOrNullAsync(string resourceName) + { + return CreateByResourceNameOrNullInternalAsync(resourceName, lockCache: true); + } + + 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( - resourceType, + 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; + } - private StringLocalizerCacheItem CreateStringLocalizerCacheItem(LocalizationResource resource) + 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) { - foreach (var globalContributor in AbpLocalizationOptions.GlobalContributors) + foreach (var globalContributorType in AbpLocalizationOptions.GlobalContributors) { - resource.Contributors.Add((ILocalizationResourceContributor)Activator.CreateInstance(globalContributor)); + resource.Contributors.Add( + Activator + .CreateInstance(globalContributorType) + .As() + ); } var context = new LocalizationResourceInitializationContext(resource, ServiceProvider); @@ -68,7 +190,49 @@ public class AbpStringLocalizerFactory : IStringLocalizerFactory, IAbpStringLoca return new StringLocalizerCacheItem( new AbpDictionaryBasedStringLocalizer( resource, - resource.BaseResourceTypes.Select(Create).ToList(), + resource + .BaseResourceNames + .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 ) ); @@ -76,8 +240,6 @@ public class AbpStringLocalizerFactory : IStringLocalizerFactory, IAbpStringLoca public virtual IStringLocalizer Create(string baseName, string location) { - //TODO: Investigate when this is called? - return InnerFactory.Create(baseName, location); } @@ -106,4 +268,4 @@ public class AbpStringLocalizerFactory : IStringLocalizerFactory, IAbpStringLoca return Create(AbpLocalizationOptions.DefaultResourceType); } -} +} \ No newline at end of file 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 new file mode 100644 index 0000000000..cd48bb1bd9 --- /dev/null +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/External/IExternalLocalizationStore.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Volo.Abp.Localization.External; + +public interface IExternalLocalizationStore +{ + [CanBeNull] + LocalizationResourceBase GetResourceOrNull([NotNull] string resourceName); + + [ItemCanBeNull] + Task GetResourceOrNullAsync([NotNull] string resourceName); + + Task GetResourceNamesAsync(); + + Task GetResourcesAsync(); +} \ 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 new file mode 100644 index 0000000000..508f947981 --- /dev/null +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/External/NullExternalLocalizationStore.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Localization.External; + +public class NullExternalLocalizationStore : IExternalLocalizationStore, ISingletonDependency +{ + public LocalizationResourceBase GetResourceOrNull(string resourceName) + { + return null; + } + + public Task GetResourceOrNullAsync(string resourceName) + { + return Task.FromResult(null); + } + + public Task GetResourceNamesAsync() + { + return Task.FromResult(Array.Empty()); + } + + public Task GetResourcesAsync() + { + return Task.FromResult(Array.Empty()); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/IAbpStringLocalizer.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/IAbpStringLocalizer.cs new file mode 100644 index 0000000000..59fbbcb7b4 --- /dev/null +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/IAbpStringLocalizer.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Localization; + +namespace Volo.Abp.Localization; + +public interface IAbpStringLocalizer : IStringLocalizer +{ + IEnumerable GetAllStrings( + bool includeParentCultures, + bool includeBaseLocalizers, + bool includeDynamicContributors + ); + + Task> GetAllStringsAsync( + bool includeParentCultures + ); + + Task> GetAllStringsAsync( + bool includeParentCultures, + bool includeBaseLocalizers, + bool includeDynamicContributors + ); + + Task> GetSupportedCulturesAsync(); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/ILocalizationResourceContributor.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/ILocalizationResourceContributor.cs index bfccb660a0..2a79d20821 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/ILocalizationResourceContributor.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/ILocalizationResourceContributor.cs @@ -1,13 +1,20 @@ using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.Extensions.Localization; namespace Volo.Abp.Localization; public interface ILocalizationResourceContributor { + bool IsDynamic { get; } + void Initialize(LocalizationResourceInitializationContext context); LocalizedString GetOrNull(string cultureName, string name); void Fill(string cultureName, Dictionary dictionary); + + Task FillAsync(string cultureName, Dictionary dictionary); + + Task> GetSupportedCulturesAsync(); } diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/IStringLocalizerSupportsInheritance.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/IStringLocalizerSupportsInheritance.cs deleted file mode 100644 index cf0bff3631..0000000000 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/IStringLocalizerSupportsInheritance.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; -using Microsoft.Extensions.Localization; - -namespace Volo.Abp.Localization; - -public interface IStringLocalizerSupportsInheritance -{ - IEnumerable GetAllStrings(bool includeParentCultures, bool includeBaseLocalizers); -} diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizableStringSerializer.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizableStringSerializer.cs index 4cfd6e2f32..da431bca65 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizableStringSerializer.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizableStringSerializer.cs @@ -17,7 +17,7 @@ public class LocalizableStringSerializer : ILocalizableStringSerializer, ITransi { if (localizableString is LocalizableString realLocalizableString) { - return $"L:{LocalizationResourceNameAttribute.GetName(realLocalizableString.ResourceType)},{realLocalizableString.Name}"; + return $"L:{realLocalizableString.ResourceName},{realLocalizableString.Name}"; } if (localizableString is FixedLocalizableString fixedLocalizableString) @@ -55,13 +55,8 @@ public class LocalizableStringSerializer : ILocalizableStringSerializer, ITransi { throw new AbpException("Invalid LocalizableString value: " + value); } - - var resourceType = LocalizationOptions.Resources.GetOrNull(resourceName)?.ResourceType; - - return new LocalizableString( - resourceType, - name - ); + + return LocalizableString.Create(name, resourceName); default: return new FixedLocalizableString(value); } diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResource.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResource.cs index a2202dc959..7a55f1f8a3 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResource.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResource.cs @@ -5,39 +5,21 @@ using JetBrains.Annotations; namespace Volo.Abp.Localization; -public class LocalizationResource +public class LocalizationResource : LocalizationResourceBase { [NotNull] public Type ResourceType { get; } - [NotNull] - public string ResourceName => LocalizationResourceNameAttribute.GetName(ResourceType); - - [CanBeNull] - public string DefaultCultureName { get; set; } - - [NotNull] - public LocalizationResourceContributorList Contributors { get; } - - [NotNull] - public List BaseResourceTypes { get; } - public LocalizationResource( [NotNull] Type resourceType, [CanBeNull] string defaultCultureName = null, [CanBeNull] ILocalizationResourceContributor initialContributor = null) + : base( + LocalizationResourceNameAttribute.GetName(resourceType), + defaultCultureName, + initialContributor) { ResourceType = Check.NotNull(resourceType, nameof(resourceType)); - DefaultCultureName = defaultCultureName; - - BaseResourceTypes = new List(); - Contributors = new LocalizationResourceContributorList(); - - if (initialContributor != null) - { - Contributors.Add(initialContributor); - } - AddBaseResourceTypes(); } @@ -51,8 +33,8 @@ public class LocalizationResource { foreach (var baseResourceType in descriptor.GetInheritedResourceTypes()) { - BaseResourceTypes.AddIfNotContains(baseResourceType); + BaseResourceNames.AddIfNotContains(LocalizationResourceNameAttribute.GetName(baseResourceType)); } } } -} +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceBase.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceBase.cs new file mode 100644 index 0000000000..a7ec4fb93b --- /dev/null +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceBase.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace Volo.Abp.Localization; + +public abstract class LocalizationResourceBase +{ + [NotNull] + public string ResourceName { get; } + + public List BaseResourceNames { get; } + + [CanBeNull] + public string DefaultCultureName { get; set; } + + [NotNull] + public LocalizationResourceContributorList Contributors { get; } + + public LocalizationResourceBase( + [NotNull] string resourceName, + [CanBeNull] string defaultCultureName = null, + [CanBeNull] ILocalizationResourceContributor initialContributor = null) + { + ResourceName = Check.NotNullOrWhiteSpace(resourceName, nameof(resourceName)); + DefaultCultureName = defaultCultureName; + + Contributors = new LocalizationResourceContributorList(); + BaseResourceNames = new(); + + if (initialContributor != null) + { + Contributors.Add(initialContributor); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceContributorList.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceContributorList.cs index d52fe78943..6df15920da 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceContributorList.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceContributorList.cs @@ -1,15 +1,24 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Microsoft.Extensions.Localization; namespace Volo.Abp.Localization; public class LocalizationResourceContributorList : List { - public LocalizedString GetOrNull(string cultureName, string name) + public LocalizedString GetOrNull( + string cultureName, + string name, + bool includeDynamicContributors = true) { foreach (var contributor in this.Select(x => x).Reverse()) { + if (!includeDynamicContributors && contributor.IsDynamic) + { + continue; + } + var localString = contributor.GetOrNull(cultureName, name); if (localString != null) { @@ -20,11 +29,47 @@ public class LocalizationResourceContributorList : List dictionary) + public void Fill( + string cultureName, + Dictionary dictionary, + bool includeDynamicContributors = true) { foreach (var contributor in this) { + if (!includeDynamicContributors && contributor.IsDynamic) + { + continue; + } + contributor.Fill(cultureName, dictionary); } } + + public async Task FillAsync( + string cultureName, + Dictionary dictionary, + bool includeDynamicContributors = true) + { + foreach (var contributor in this) + { + if (!includeDynamicContributors && contributor.IsDynamic) + { + continue; + } + + await contributor.FillAsync(cultureName, dictionary); + } + } + + internal async Task> GetSupportedCulturesAsync() + { + var cultures = new List(); + + foreach (var contributor in this) + { + cultures.AddRange(await contributor.GetSupportedCulturesAsync()); + } + + return cultures; + } } diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceDictionary.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceDictionary.cs index d672a2f17a..4a88eac199 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceDictionary.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceDictionary.cs @@ -4,9 +4,9 @@ using JetBrains.Annotations; namespace Volo.Abp.Localization; -public class LocalizationResourceDictionary : Dictionary +public class LocalizationResourceDictionary : Dictionary { - private readonly Dictionary _resourcesByNames = new(); + private readonly Dictionary _resourcesByTypes = new(); public LocalizationResource Add([CanBeNull] string defaultCultureName = null) { @@ -15,24 +15,41 @@ public class LocalizationResourceDictionary : Dictionary() + return resource; + } + + public LocalizationResourceBase Get() { var resourceType = typeof(TResource); - var resource = this.GetOrDefault(resourceType); + var resource = _resourcesByTypes.GetOrDefault(resourceType); if (resource == null) { throw new AbpException("Can not find a resource with given type: " + resourceType.AssemblyQualifiedName); @@ -41,9 +58,9 @@ public class LocalizationResourceDictionary : Dictionary( + [NotNull] this TLocalizationResource localizationResource, [NotNull] string virtualPath) + where TLocalizationResource : LocalizationResourceBase { Check.NotNull(localizationResource, nameof(localizationResource)); Check.NotNull(virtualPath, nameof(virtualPath)); @@ -21,16 +22,35 @@ public static class LocalizationResourceExtensions return localizationResource; } - public static LocalizationResource AddBaseTypes( - [NotNull] this LocalizationResource localizationResource, + public static TLocalizationResource AddBaseTypes( + [NotNull] this TLocalizationResource localizationResource, [NotNull] params Type[] types) + where TLocalizationResource : LocalizationResourceBase { Check.NotNull(localizationResource, nameof(localizationResource)); Check.NotNull(types, nameof(types)); foreach (var type in types) { - localizationResource.BaseResourceTypes.AddIfNotContains(type); + localizationResource + .BaseResourceNames + .AddIfNotContains(LocalizationResourceNameAttribute.GetName(type)); + } + + return localizationResource; + } + + public static TLocalizationResource AddBaseResources( + [NotNull] this TLocalizationResource localizationResource, + [NotNull] params string[] baseResourceNames) + where TLocalizationResource : LocalizationResourceBase + { + Check.NotNull(localizationResource, nameof(localizationResource)); + Check.NotNull(baseResourceNames, nameof(baseResourceNames)); + + foreach (var baseResourceName in baseResourceNames) + { + localizationResource.BaseResourceNames.AddIfNotContains(baseResourceName); } return localizationResource; diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceInitializationContext.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceInitializationContext.cs index 845938efb2..7f26466877 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceInitializationContext.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceInitializationContext.cs @@ -4,11 +4,11 @@ namespace Volo.Abp.Localization; public class LocalizationResourceInitializationContext { - public LocalizationResource Resource { get; } + public LocalizationResourceBase Resource { get; } public IServiceProvider ServiceProvider { get; } - public LocalizationResourceInitializationContext(LocalizationResource resource, IServiceProvider serviceProvider) + public LocalizationResourceInitializationContext(LocalizationResourceBase resource, IServiceProvider serviceProvider) { Resource = resource; ServiceProvider = serviceProvider; diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/NonTypedLocalizationResource.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/NonTypedLocalizationResource.cs new file mode 100644 index 0000000000..2a8300d522 --- /dev/null +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/NonTypedLocalizationResource.cs @@ -0,0 +1,17 @@ +using JetBrains.Annotations; + +namespace Volo.Abp.Localization; + +public class NonTypedLocalizationResource : LocalizationResourceBase +{ + public NonTypedLocalizationResource( + [NotNull] string resourceName, + [CanBeNull] string defaultCultureName = null, + [CanBeNull] ILocalizationResourceContributor initialContributor = null + ) : base( + resourceName, + defaultCultureName, + initialContributor) + { + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/VirtualFiles/VirtualFileLocalizationResourceContributorBase.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/VirtualFiles/VirtualFileLocalizationResourceContributorBase.cs index 1ac41dba06..15b45efaff 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/VirtualFiles/VirtualFileLocalizationResourceContributorBase.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/VirtualFiles/VirtualFileLocalizationResourceContributorBase.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Localization; @@ -11,6 +12,8 @@ namespace Volo.Abp.Localization.VirtualFiles; public abstract class VirtualFileLocalizationResourceContributorBase : ILocalizationResourceContributor { + public bool IsDynamic => false; + private readonly string _virtualPath; private IVirtualFileProvider _virtualFileProvider; private Dictionary _dictionaries; @@ -37,6 +40,17 @@ public abstract class VirtualFileLocalizationResourceContributorBase : ILocaliza GetDictionaries().GetOrDefault(cultureName)?.Fill(dictionary); } + public Task FillAsync(string cultureName, Dictionary dictionary) + { + Fill(cultureName, dictionary); + return Task.CompletedTask; + } + + public Task> GetSupportedCulturesAsync() + { + return Task.FromResult((IEnumerable)GetDictionaries().Keys); + } + private Dictionary GetDictionaries() { var dictionaries = _dictionaries; diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfigurationExtensions.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfigurationExtensions.cs index 8b9dca76be..085e486756 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfigurationExtensions.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/ExtensionPropertyConfigurationExtensions.cs @@ -8,20 +8,18 @@ public static class ExtensionPropertyConfigurationExtensions public static string GetLocalizationResourceNameOrNull( this ExtensionPropertyConfiguration property) { - var resourceType = property.GetLocalizationResourceTypeOrNull(); - if (resourceType == null) + if (property.DisplayName is LocalizableString localizableString) { - return null; + return localizableString.ResourceName; } - return LocalizationResourceNameAttribute.GetName(resourceType); + return null; } public static Type GetLocalizationResourceTypeOrNull( this ExtensionPropertyConfiguration property) { - if (property.DisplayName != null && - property.DisplayName is LocalizableString localizableString) + if (property.DisplayName is LocalizableString localizableString) { return localizableString.ResourceType; } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationBuilder_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationBuilder_Tests.cs index 5d3cb09749..56fc8a583a 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationBuilder_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationBuilder_Tests.cs @@ -12,7 +12,7 @@ public class ApplicationConfigurationBuilder_Tests : AspNetCoreMvcTestBase { var applicationConfigurationBuilder = GetRequiredService(); - var config = await applicationConfigurationBuilder.GetAsync(); + var config = await applicationConfigurationBuilder.GetAsync(new ApplicationConfigurationRequestOptions()); config.Auth.ShouldNotBeNull(); config.Localization.ShouldNotBeNull(); diff --git a/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_ConfigureOptions_Test.cs b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_ConfigureOptions_Test.cs index 12c8a16833..513423a781 100644 --- a/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_ConfigureOptions_Test.cs +++ b/framework/test/Volo.Abp.Caching.Tests/Volo/Abp/Caching/DistributedCache_ConfigureOptions_Test.cs @@ -34,7 +34,19 @@ public class DistributedCache_ConfigureOptions_Test : AbpIntegratedTest { var cache1 = GetRequiredService>(); var cache2 = GetRequiredService>(); - + + cache1.InternalCache.ShouldBe(cache2); + await cache1.SetAsync("john", new PersonCacheItem("John Doe")); var item1 = await cache1.GetAsync("john"); diff --git a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Localization/CultureHelper_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Localization/CultureHelper_Tests.cs new file mode 100644 index 0000000000..d55df57800 --- /dev/null +++ b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Localization/CultureHelper_Tests.cs @@ -0,0 +1,36 @@ +using Shouldly; +using Xunit; + +namespace Volo.Abp.Localization; + +public class CultureHelper_Tests +{ + [Fact] + public void IsCompatibleCulture() + { + CultureHelper.IsCompatibleCulture("tr", "tr").ShouldBeTrue(); + CultureHelper.IsCompatibleCulture("tr", "tr-TR").ShouldBeTrue(); + + CultureHelper.IsCompatibleCulture("en", "tr").ShouldBeFalse(); + CultureHelper.IsCompatibleCulture("en", "tr-TR").ShouldBeFalse(); + + CultureHelper.IsCompatibleCulture("en-US", "en").ShouldBeFalse(); + CultureHelper.IsCompatibleCulture("en-US", "en-GB").ShouldBeFalse(); + + CultureHelper.IsCompatibleCulture("zh", "zh-CN").ShouldBeTrue(); + CultureHelper.IsCompatibleCulture("zh", "zh-HK").ShouldBeTrue(); + CultureHelper.IsCompatibleCulture("zh", "zh-MO").ShouldBeTrue(); + CultureHelper.IsCompatibleCulture("zh", "zh-SG").ShouldBeTrue(); + CultureHelper.IsCompatibleCulture("zh", "zh-TW").ShouldBeTrue(); + CultureHelper.IsCompatibleCulture("zh", "zh-Hans").ShouldBeTrue(); + CultureHelper.IsCompatibleCulture("zh", "zh-Hant").ShouldBeTrue(); + CultureHelper.IsCompatibleCulture("zh-Hans", "zh-CN").ShouldBeTrue(); + CultureHelper.IsCompatibleCulture("zh-Hans", "zh-SG").ShouldBeTrue(); + CultureHelper.IsCompatibleCulture("zh-Hant", "zh-HK").ShouldBeTrue(); + CultureHelper.IsCompatibleCulture("zh-Hant", "zh-MO").ShouldBeTrue(); + CultureHelper.IsCompatibleCulture("zh-Hant", "zh-TW").ShouldBeTrue(); + + CultureHelper.IsCompatibleCulture("zh-Hans", "zh-HK").ShouldBeFalse(); + CultureHelper.IsCompatibleCulture("zh-Hant", "zh-SG").ShouldBeFalse(); + } +} diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalizationTestModule.cs b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalizationTestModule.cs index 340d091330..7e01a1c42c 100644 --- a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalizationTestModule.cs +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalizationTestModule.cs @@ -1,5 +1,4 @@ -using Volo.Abp.Localization.TestResources.Base.CountryNames; -using Volo.Abp.Localization.TestResources.Base.Validation; +using Volo.Abp.Localization.TestResources.Base.Validation; using Volo.Abp.Localization.TestResources.Source; using Volo.Abp.Modularity; using Volo.Abp.VirtualFileSystem; @@ -24,12 +23,13 @@ public class AbpLocalizationTestModule : AbpModule .AddVirtualJson("/Volo/Abp/Localization/TestResources/Base/Validation"); options.Resources - .Add("en") + .Add("LocalizationTestCountryNames") .AddVirtualJson("/Volo/Abp/Localization/TestResources/Base/CountryNames"); options.Resources .Add("en") - .AddVirtualJson("/Volo/Abp/Localization/TestResources/Source"); + .AddVirtualJson("/Volo/Abp/Localization/TestResources/Source") + .AddBaseResources("LocalizationTestCountryNames"); options.Resources .Get() diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalization_Tests.cs b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalization_Tests.cs index d643f163b9..ee0e929e32 100644 --- a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalization_Tests.cs +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalization_Tests.cs @@ -1,5 +1,6 @@ using System.Globalization; using System.Linq; +using System.Threading.Tasks; using Microsoft.Extensions.Localization; using Shouldly; using Volo.Abp.Localization.TestResources.Source; @@ -310,7 +311,7 @@ public class AbpLocalization_Tests : AbpIntegratedTest +{ + private readonly IStringLocalizerFactory _factory; + + public AbpStringLocalizerFactory_Tests() + { + _factory = GetRequiredService(); + } + + [Fact] + public void Factory_Type_Should_Be_AbpStringLocalizerFactory() + { + ProxyHelper.UnProxy(_factory).ShouldBeOfType(); + } + + [Fact] + public void Should_Create_Resource_By_Name() + { + using (CultureHelper.Use("en")) + { + var localizer = _factory.CreateByResourceNameOrNull("Test"); + localizer.ShouldNotBeNull(); + localizer["CarPlural"].Value.ShouldBe("Cars"); + } + } + + [Fact] + public async Task Should_Create_Resource_By_Name_Async() + { + using (CultureHelper.Use("en")) + { + var localizer = await _factory.CreateByResourceNameOrNullAsync("Test"); + localizer.ShouldNotBeNull(); + localizer["CarPlural"].Value.ShouldBe("Cars"); + } + } + + [Fact] + public void Should_Throw_Exception_For_Unknown_Resource_Names() + { + Assert.Throws( + () => _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 diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/LocalizableStringSerializer_Tests.cs b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/LocalizableStringSerializer_Tests.cs index d8fc1717d2..9261d19dbc 100644 --- a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/LocalizableStringSerializer_Tests.cs +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/LocalizableStringSerializer_Tests.cs @@ -59,7 +59,7 @@ public class LocalizableStringSerializer_Tests : AbpIntegratedTest(); - localizableString.ResourceType.ShouldBe(typeof(LocalizationTestResource)); + localizableString.ResourceName.ShouldBe("Test"); localizableString.Name.ShouldBe("Car"); Assert.Throws(() => diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/CountryNames/LocalizationTestCountryNamesResource.cs b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/CountryNames/LocalizationTestCountryNamesResource.cs deleted file mode 100644 index a70436cbc7..0000000000 --- a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/CountryNames/LocalizationTestCountryNamesResource.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Volo.Abp.Localization.TestResources.Base.CountryNames; - -public sealed class LocalizationTestCountryNamesResource -{ - -} diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/LocalizationTestResource.cs b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/LocalizationTestResource.cs index bc32e98bcc..95a1bbd32f 100644 --- a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/LocalizationTestResource.cs +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/LocalizationTestResource.cs @@ -1,11 +1,9 @@ -using Volo.Abp.Localization.TestResources.Base.CountryNames; -using Volo.Abp.Localization.TestResources.Base.Validation; +using Volo.Abp.Localization.TestResources.Base.Validation; namespace Volo.Abp.Localization.TestResources.Source; [InheritResource( - typeof(LocalizationTestValidationResource), - typeof(LocalizationTestCountryNamesResource) + typeof(LocalizationTestValidationResource) )] [LocalizationResourceName("Test")] public sealed class LocalizationTestResource diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml b/modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml index 5e2ca98ba7..02adac253b 100644 --- a/modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml @@ -96,6 +96,7 @@ + diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Application.cshtml b/modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Application.cshtml index 42f2b2ead2..4c91b8edfb 100644 --- a/modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Application.cshtml +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Application.cshtml @@ -59,7 +59,7 @@ @(await Component.InvokeAsync())
- @RenderSection("content_toolbar", false) + @await RenderSectionAsync("content_toolbar", false)
@await Component.InvokeLayoutHookAsync(LayoutHooks.PageContent.First, StandardLayouts.Application) @@ -69,6 +69,7 @@ + diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Empty.cshtml b/modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Empty.cshtml index 6d3461be43..aaef7898c3 100644 --- a/modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Empty.cshtml +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Empty.cshtml @@ -64,6 +64,7 @@ + diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor/Components/FeatureManagementModal.razor.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor/Components/FeatureManagementModal.razor.cs index e2cb9c160c..f63dc6f37f 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor/Components/FeatureManagementModal.razor.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor/Components/FeatureManagementModal.razor.cs @@ -194,8 +194,8 @@ public partial class FeatureManagementModal protected virtual IStringLocalizer CreateStringLocalizer(string resourceName) { - var resource = LocalizationOptions.Value.Resources.Values.FirstOrDefault(x => x.ResourceName == resourceName); - return HtmlLocalizerFactory.Create(resource != null ? resource.ResourceType : LocalizationOptions.Value.DefaultResourceType); + return StringLocalizerFactory.CreateByResourceNameOrNull(resourceName) ?? + StringLocalizerFactory.CreateDefaultOrNull(); } protected virtual Task ClosingModal(ModalClosingEventArgs eventArgs) diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor/Components/FeatureSettingGroup/FeatureSettingManagementComponent.razor.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor/Components/FeatureSettingGroup/FeatureSettingManagementComponent.razor.cs index 09b31b9609..2b5bee083d 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor/Components/FeatureSettingGroup/FeatureSettingManagementComponent.razor.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor/Components/FeatureSettingGroup/FeatureSettingManagementComponent.razor.cs @@ -10,9 +10,6 @@ namespace Volo.Abp.FeatureManagement.Blazor.Components.FeatureSettingGroup; public partial class FeatureSettingManagementComponent : AbpComponentBase { - [Inject] - protected IStringLocalizer L { get; set; } - [Inject] protected PermissionChecker PermissionChecker { get; set; } @@ -20,6 +17,11 @@ public partial class FeatureSettingManagementComponent : AbpComponentBase protected FeatureSettingViewModel Settings; + public FeatureSettingManagementComponent() + { + LocalizationResource = typeof(AbpFeatureManagementResource); + } + protected async override Task OnInitializedAsync() { Settings = new FeatureSettingViewModel @@ -32,9 +34,4 @@ public partial class FeatureSettingManagementComponent : AbpComponentBase { await FeatureManagementModal.OpenAsync(TenantFeatureValueProvider.ProviderName); } -} - -public class FeatureSettingViewModel -{ - public bool HasManageHostFeaturesPermission { get; set; } } \ No newline at end of file diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor/Components/FeatureSettingGroup/FeatureSettingViewModel.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor/Components/FeatureSettingGroup/FeatureSettingViewModel.cs new file mode 100644 index 0000000000..eaef1a96f3 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Blazor/Components/FeatureSettingGroup/FeatureSettingViewModel.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.FeatureManagement.Blazor.Components.FeatureSettingGroup; + +public class FeatureSettingViewModel +{ + public bool HasManageHostFeaturesPermission { get; set; } +} \ No newline at end of file diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml b/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml index 868211adcf..4ed0012a89 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml @@ -1,22 +1,21 @@ @page @using Microsoft.AspNetCore.Mvc.Localization -@using Microsoft.Extensions.Options +@using Microsoft.Extensions.Localization @using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal @using Volo.Abp.FeatureManagement.Localization @using Volo.Abp.Validation.StringValues @using Volo.Abp.FeatureManagement.Web.Pages.FeatureManagement -@using Volo.Abp.Localization @model FeatureManagementModal @inject IHtmlLocalizer L -@inject IHtmlLocalizerFactory HtmlLocalizerFactory -@inject IOptions LocalizationOptions +@inject IStringLocalizerFactory StringLocalizerFactory @{ Layout = null; IHtmlLocalizer CreateHtmlLocalizer(string resourceName) { - var resource = LocalizationOptions.Value.Resources.Values.FirstOrDefault(x => x.ResourceName == resourceName); - return HtmlLocalizerFactory.Create(resource != null ? resource.ResourceType : LocalizationOptions.Value.DefaultResourceType); + var localizer = StringLocalizerFactory.CreateByResourceNameOrNull(resourceName) ?? + StringLocalizerFactory.CreateDefaultOrNull(); + return new HtmlLocalizer(localizer); } }
diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Application.Contracts/Volo/Abp/PermissionManagement/PermissionGrantInfoDto.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Application.Contracts/Volo/Abp/PermissionManagement/PermissionGrantInfoDto.cs index 12e4010f99..b608a69b54 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Application.Contracts/Volo/Abp/PermissionManagement/PermissionGrantInfoDto.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Application.Contracts/Volo/Abp/PermissionManagement/PermissionGrantInfoDto.cs @@ -7,10 +7,6 @@ public class PermissionGrantInfoDto public string Name { get; set; } public string DisplayName { get; set; } - - public string DisplayNameKey { get; set; } - - public string DisplayNameResource { get; set; } public string ParentName { get; set; } diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Application/Volo/Abp/PermissionManagement/PermissionAppService.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Application/Volo/Abp/PermissionManagement/PermissionAppService.cs index fd0ccf0246..8579fa28ac 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Application/Volo/Abp/PermissionManagement/PermissionAppService.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Application/Volo/Abp/PermissionManagement/PermissionAppService.cs @@ -101,15 +101,9 @@ public class PermissionAppService : ApplicationService, IPermissionAppService private PermissionGrantInfoDto CreatePermissionGrantInfoDto(PermissionDefinition permission) { - var localizableDisplayName = permission.DisplayName as LocalizableString; - return new PermissionGrantInfoDto { Name = permission.Name, DisplayName = permission.DisplayName.Localize(StringLocalizerFactory), - DisplayNameKey = localizableDisplayName?.Name, - DisplayNameResource = localizableDisplayName?.ResourceType != null - ? LocalizationResourceNameAttribute.GetName(localizableDisplayName.ResourceType) - : null, ParentName = permission.Parent?.Name, AllowedProviders = permission.Providers, GrantedProviders = new List() diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/PermissionManagementModal.razor.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/PermissionManagementModal.razor.cs index 36358c1b95..b96a1c9d4d 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/PermissionManagementModal.razor.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/PermissionManagementModal.razor.cs @@ -84,8 +84,6 @@ public partial class PermissionManagementModal var result = await PermissionAppService.GetAsync(_providerName, _providerKey); - UpdateLocalizations(result); - _entityDisplayName = entityDisplayName ?? result.EntityDisplayName; _groups = result.Groups; @@ -252,47 +250,4 @@ public partial class PermissionManagementModal eventArgs.Cancel = eventArgs.CloseReason == CloseReason.FocusLostClosing; return Task.CompletedTask; } - - protected virtual void UpdateLocalizations(GetPermissionListResultDto result) - { - foreach (var group in result.Groups) - { - group.DisplayName = Localize( - group.DisplayNameKey, - group.DisplayNameResource, - group.DisplayName - ); - - foreach (var permission in group.Permissions) - { - permission.DisplayName = Localize( - permission.DisplayNameKey, - permission.DisplayNameResource, - permission.DisplayName - ); - } - } - } - - protected virtual string Localize(string key, string resourceName, string fallbackValue) - { - if (key.IsNullOrEmpty() || resourceName.IsNullOrEmpty()) - { - return fallbackValue; - } - - var resource = LocalizationOptions.Value.Resources.GetOrNull(resourceName); - if (resource == null) - { - return fallbackValue; - } - - var result = new LocalizableString(resource.ResourceType, key).Localize(StringLocalizerFactory); - if (result.ResourceNotFound) - { - return fallbackValue; - } - - return result.Value; - } } diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml.cs index 30057eea82..9d91fc573e 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -60,8 +59,6 @@ public class PermissionManagementModal : AbpPageModel var result = await PermissionAppService.GetAsync(ProviderName, ProviderKey); - UpdateLocalizations(result); - EntityDisplayName = !string.IsNullOrWhiteSpace(ProviderKeyDisplayName) ? ProviderKeyDisplayName : result.EntityDisplayName; @@ -86,49 +83,6 @@ public class PermissionManagementModal : AbpPageModel return Page(); } - private void UpdateLocalizations(GetPermissionListResultDto result) - { - foreach (var group in result.Groups) - { - group.DisplayName = Localize( - group.DisplayNameKey, - group.DisplayNameResource, - group.DisplayName - ); - - foreach (var permission in group.Permissions) - { - permission.DisplayName = Localize( - permission.DisplayNameKey, - permission.DisplayNameResource, - permission.DisplayName - ); - } - } - } - - private string Localize(string key, string resourceName, string fallbackValue) - { - if (key.IsNullOrEmpty() || resourceName.IsNullOrEmpty()) - { - return fallbackValue; - } - - var resource = LocalizationOptions.Resources.GetOrNull(resourceName); - if (resource == null) - { - return fallbackValue; - } - - var result = new LocalizableString(resource.ResourceType, key).Localize(StringLocalizerFactory); - if (result.ResourceNotFound) - { - return fallbackValue; - } - - return result.Value; - } - public virtual async Task OnPostAsync() { ValidateModel(); diff --git a/npm/packs/core/src/abp.js b/npm/packs/core/src/abp.js index 0348069bec..074d4f0f50 100644 --- a/npm/packs/core/src/abp.js +++ b/npm/packs/core/src/abp.js @@ -72,8 +72,61 @@ var abp = abp || {}; /* LOCALIZATION ***********************************************/ abp.localization = abp.localization || {}; - + abp.localization.internal = abp.localization.internal || {}; abp.localization.values = abp.localization.values || {}; + abp.localization.resources = abp.localization.resources || {}; + + abp.localization.internal.getResource = function (resourceName) { + var resource = abp.localization.resources[resourceName]; + if (resource) { + return resource; + } + + var legacySource = abp.localization.values[resourceName]; + if (legacySource) { + return { + texts: abp.localization.values[resourceName], + baseResources: [] + }; + } + + abp.log.warn('Could not find localization source: ' + resourceName); + return null; + }; + + abp.localization.internal.localize = function (key, sourceName) { + var resource = abp.localization.internal.getResource(sourceName); + if (!resource){ + return { + value: key, + found: false + }; + } + + var value = resource.texts[key]; + if (value === undefined) { + for (var i = 0; i < resource.baseResources.length; i++){ + var result = abp.localization.internal.localize(key, resource.baseResources[i]); + if (result.found){ + return result; + } + } + + return { + value: key, + found: false + }; + } + + var copiedArguments = Array.prototype.slice.call(arguments, 0); + copiedArguments.splice(1, 1); + copiedArguments[0] = value; + + return { + value: abp.utils.formatString.apply(this, copiedArguments), + found: true + }; + }; abp.localization.localize = function (key, sourceName) { if (sourceName === '_') { //A convention to suppress the localization @@ -86,22 +139,7 @@ var abp = abp || {}; return key; } - var source = abp.localization.values[sourceName]; - if (!source) { - abp.log.warn('Could not find localization source: ' + sourceName); - return key; - } - - var value = source[key]; - if (value == undefined) { - return key; - } - - var copiedArguments = Array.prototype.slice.call(arguments, 0); - copiedArguments.splice(1, 1); - copiedArguments[0] = value; - - return abp.utils.formatString.apply(this, copiedArguments); + return abp.localization.internal.localize(key, sourceName).value; }; abp.localization.isLocalized = function (key, sourceName) { @@ -114,17 +152,7 @@ var abp = abp || {}; return false; } - var source = abp.localization.values[sourceName]; - if (!source) { - return false; - } - - var value = source[key]; - if (value === undefined) { - return false; - } - - return true; + return abp.localization.internal.localize(key, sourceName).found; }; abp.localization.getResource = function (name) { @@ -687,7 +715,7 @@ var abp = abp || {}; } /** - * Escape HTML to help prevent XSS attacks. + * Escape HTML to help prevent XSS attacks. */ abp.utils.htmlEscape = function (html) { return typeof html === 'string' ? html.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') : html; @@ -759,7 +787,7 @@ var abp = abp || {}; return toUtc(date); } }; - + /* FEATURES *************************************************/ abp.features = abp.features || {}; @@ -774,7 +802,7 @@ var abp = abp || {}; abp.features.get = function (name) { return abp.features.values[name]; }; - + /* GLOBAL FEATURES *************************************************/ abp.globalFeatures = abp.globalFeatures || {};