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 b486536a1f..97ae0c868d 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 @@ -218,14 +218,13 @@ public class AbpApplicationConfigurationAppService : ApplicationService, IAbpApp foreach (var resource in _localizationOptions.Resources.Values) { var dictionary = new Dictionary(); - - var localizer = (IStringLocalizer) _serviceProvider.GetRequiredService( - typeof(IStringLocalizer<>).MakeGenericType(resource.ResourceType) - ); - - foreach (var localizedString in localizer.GetAllStrings()) + var localizer = StringLocalizerFactory.CreateByResourceNameOrNull(resource.ResourceName); + if (localizer != null) { - dictionary[localizedString.Name] = localizedString.Value; + foreach (var localizedString in localizer.GetAllStrings()) + { + dictionary[localizedString.Name] = localizedString.Value; + } } localizationConfig.Values[resource.ResourceName] = dictionary; diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpLocalizationScriptController.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpLocalizationScriptController.cs index cd95768c3f..f4e22e0f41 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpLocalizationScriptController.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Localization/AbpLocalizationScriptController.cs @@ -34,8 +34,8 @@ public class AbpLocalizationScriptController { foreach (var resource in LocalizationOptions.Resources.Values) { - var localizer = StringLocalizerFactory.Create(resource.ResourceType); - localizer.GetAllStrings(); + var localizer = StringLocalizerFactory.CreateByResourceNameOrNull(resource.ResourceName); + localizer?.GetAllStrings(); } } } 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.Localization.Abstractions/Microsoft/Extensions/Localization/AbpStringLocalizerFactoryExtensions.cs b/framework/src/Volo.Abp.Localization.Abstractions/Microsoft/Extensions/Localization/AbpStringLocalizerFactoryExtensions.cs index 534a7e0eb9..ccd9718de9 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,15 +1,18 @@ -using Volo.Abp; +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 IAbpStringLocalizerFactory) ?.CreateDefaultOrNull(); } + [CanBeNull] public static IStringLocalizer CreateByResourceNameOrNull( this IStringLocalizerFactory localizerFactory, string resourceName) @@ -18,6 +21,7 @@ public static class AbpStringLocalizerFactoryExtensions ?.CreateByResourceNameOrNull(resourceName); } + [NotNull] public static IStringLocalizer CreateByResourceName( this IStringLocalizerFactory localizerFactory, string resourceName) 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..ef2027b415 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 @@ -6,6 +6,9 @@ namespace Volo.Abp.Localization; public class LocalizableString : ILocalizableString { + [CanBeNull] + public string ResourceName { get; } + [CanBeNull] public Type ResourceType { get; } @@ -16,22 +19,30 @@ public class LocalizableString : ILocalizableString { Name = Check.NotNullOrEmpty(name, nameof(name)); ResourceType = resourceType; + + if (resourceType != null) + { + ResourceName = LocalizationResourceNameAttribute.GetName(resourceType); + } } - public LocalizedString Localize(IStringLocalizerFactory stringLocalizerFactory) + public LocalizableString([NotNull] string name, [CanBeNull] string resourceName = null) { - var localizer = ResourceType != null - ? stringLocalizerFactory.Create(ResourceType) - : stringLocalizerFactory.CreateDefaultOrNull(); + Name = Check.NotNullOrEmpty(name, nameof(name)); + ResourceName = resourceName; + } + public LocalizedString Localize(IStringLocalizerFactory stringLocalizerFactory) + { + var localizer = CreateStringLocalizerOrNull(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 +55,37 @@ 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(); + } + 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/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs index 61621c2479..102e5971d6 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpDictionaryBasedStringLocalizer.cs @@ -20,7 +20,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( + LocalizationResource resource, + List baseLocalizers, + AbpLocalizationOptions abpLocalizationOptions) { Resource = resource; BaseLocalizers = baseLocalizers; 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 5e8769ddab..63206a4d31 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpStringLocalizerFactory.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/AbpStringLocalizerFactory.cs @@ -14,19 +14,22 @@ public class AbpStringLocalizerFactory : IStringLocalizerFactory, IAbpStringLoca 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; } //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) @@ -37,7 +40,7 @@ public class AbpStringLocalizerFactory : IStringLocalizerFactory, IAbpStringLoca return InnerFactory.Create(resourceType); } - return CreateInternal(resourceType, resource); + return CreateInternal(resource.ResourceName, resource); } public IStringLocalizer CreateByResourceNameOrNull(string resourceName) @@ -45,15 +48,19 @@ public class AbpStringLocalizerFactory : IStringLocalizerFactory, IAbpStringLoca var resource = AbpLocalizationOptions.Resources.GetOrNull(resourceName); if (resource == null) { - return null; + resource = ExternalLocalizationStore.GetResourceOrNull(resourceName); + if (resource == null) + { + return null; + } } - return CreateInternal(resource.ResourceType, resource); + return CreateInternal(resourceName, resource); } - private IStringLocalizer CreateInternal(Type resourceType, LocalizationResource resource) + private IStringLocalizer CreateInternal(string resourceName, LocalizationResource resource) { - if (LocalizerCache.TryGetValue(resourceType, out var cacheItem)) + if (LocalizerCache.TryGetValue(resourceName, out var cacheItem)) { return cacheItem.Localizer; } @@ -61,7 +68,7 @@ public class AbpStringLocalizerFactory : IStringLocalizerFactory, IAbpStringLoca lock (LocalizerCache) { return LocalizerCache.GetOrAdd( - resourceType, + resourceName, _ => CreateStringLocalizerCacheItem(resource) ).Localizer; } @@ -69,9 +76,13 @@ public class AbpStringLocalizerFactory : IStringLocalizerFactory, IAbpStringLoca private StringLocalizerCacheItem CreateStringLocalizerCacheItem(LocalizationResource 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); @@ -84,7 +95,11 @@ public class AbpStringLocalizerFactory : IStringLocalizerFactory, IAbpStringLoca return new StringLocalizerCacheItem( new AbpDictionaryBasedStringLocalizer( resource, - resource.BaseResourceTypes.Select(Create).ToList(), + resource + .BaseResourceNames + .Select(CreateByResourceNameOrNull) + .Where(x => x != null) + .ToList(), AbpLocalizationOptions ) ); @@ -122,4 +137,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/IExternalLocalizationStore.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/IExternalLocalizationStore.cs new file mode 100644 index 0000000000..420bab414f --- /dev/null +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/IExternalLocalizationStore.cs @@ -0,0 +1,9 @@ +using JetBrains.Annotations; + +namespace Volo.Abp.Localization; + +public interface IExternalLocalizationStore +{ + [CanBeNull] + LocalizationResource GetResourceOrNull([NotNull] string resourceName); +} \ No newline at end of file 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..655175f1ac 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizableStringSerializer.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizableStringSerializer.cs @@ -55,13 +55,13 @@ public class LocalizableStringSerializer : ILocalizableStringSerializer, ITransi { throw new AbpException("Invalid LocalizableString value: " + value); } + + if (!LocalizationOptions.Resources.ContainsResource(resourceName)) + { + resourceName = null; + } - 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..7996ab2dbf 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResource.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResource.cs @@ -5,40 +5,26 @@ 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; } + private Type ResourceType { get; } public LocalizationResource( [NotNull] Type resourceType, [CanBeNull] string defaultCultureName = null, [CanBeNull] ILocalizationResourceContributor initialContributor = null) + : base(LocalizationResourceNameAttribute.GetName(resourceType)) { ResourceType = Check.NotNull(resourceType, nameof(resourceType)); DefaultCultureName = defaultCultureName; - BaseResourceTypes = new List(); - Contributors = new LocalizationResourceContributorList(); - + AddBaseResourceTypes(); + if (initialContributor != null) { Contributors.Add(initialContributor); } - - AddBaseResourceTypes(); } protected virtual void AddBaseResourceTypes() @@ -51,8 +37,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..f523569ba3 --- /dev/null +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceBase.cs @@ -0,0 +1,25 @@ +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) + { + ResourceName = Check.NotNullOrWhiteSpace(resourceName, nameof(resourceName)); + Contributors = new LocalizationResourceContributorList(); + BaseResourceNames = new(); + } +} \ No newline at end of file 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..548e7ae37d 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceDictionary.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceDictionary.cs @@ -56,4 +56,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/NullExternalLocalizationStore.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/NullExternalLocalizationStore.cs new file mode 100644 index 0000000000..f44174d1d3 --- /dev/null +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/NullExternalLocalizationStore.cs @@ -0,0 +1,11 @@ +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Localization; + +public class NullExternalLocalizationStore : IExternalLocalizationStore, ISingletonDependency +{ + public LocalizationResource GetResourceOrNull(string resourceName) + { + return null; + } +} \ No newline at end of file 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.Localization.Tests/Volo/Abp/Localization/AbpStringLocalizerFactory_Tests.cs b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpStringLocalizerFactory_Tests.cs index d5e41a624a..352a7ea3a3 100644 --- a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpStringLocalizerFactory_Tests.cs +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpStringLocalizerFactory_Tests.cs @@ -32,7 +32,6 @@ public class AbpStringLocalizerFactory_Tests : AbpIntegratedTest(); - localizableString.ResourceType.ShouldBe(typeof(LocalizationTestResource)); + localizableString.ResourceName.ShouldBe("Test"); localizableString.Name.ShouldBe("Car"); Assert.Throws(() =>