diff --git a/framework/src/Volo.Abp.MultiLingualObjects/Volo.Abp.MultiLingualObjects.csproj b/framework/src/Volo.Abp.MultiLingualObjects/Volo.Abp.MultiLingualObjects.csproj index da83bc80f4..77da44eb47 100644 --- a/framework/src/Volo.Abp.MultiLingualObjects/Volo.Abp.MultiLingualObjects.csproj +++ b/framework/src/Volo.Abp.MultiLingualObjects/Volo.Abp.MultiLingualObjects.csproj @@ -1,20 +1,21 @@ - - - - - - - netstandard2.0 - Volo.Abp.MultiLingualObject - $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; - false - false - false - - - - - - - - + + + + + + + netstandard2.0 + Volo.Abp.MultiLingualObject + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + enable + + + + + + + diff --git a/framework/src/Volo.Abp.MultiLingualObjects/Volo/Abp/MultiLingualObjects/IMultiLingualObjectManager.cs b/framework/src/Volo.Abp.MultiLingualObjects/Volo/Abp/MultiLingualObjects/IMultiLingualObjectManager.cs index 904f3abd32..4edbd984bd 100644 --- a/framework/src/Volo.Abp.MultiLingualObjects/Volo/Abp/MultiLingualObjects/IMultiLingualObjectManager.cs +++ b/framework/src/Volo.Abp.MultiLingualObjects/Volo/Abp/MultiLingualObjects/IMultiLingualObjectManager.cs @@ -5,16 +5,30 @@ namespace Volo.Abp.MultiLingualObjects; public interface IMultiLingualObjectManager { - Task GetTranslationAsync( + Task GetTranslationAsync( TMultiLingual multiLingual, - string culture = null, + string? culture = null, bool fallbackToParentCultures = true) where TMultiLingual : IMultiLingualObject where TTranslation : class, IObjectTranslation; - Task GetTranslationAsync( - ICollection translations, - string culture = null, + Task GetTranslationAsync( + IEnumerable translations, + string? culture = null, bool fallbackToParentCultures = true) + where TTranslation : class, IObjectTranslation; + + + Task> GetBulkTranslationsAsync( + IEnumerable> translationsCombined, + string? culture = null, + bool fallbackToParentCultures = true) + where TTranslation : class, IObjectTranslation; + + Task> GetBulkTranslationsAsync( + IEnumerable multiLinguals, + string? culture = null, + bool fallbackToParentCultures = true) + where TMultiLingual : IMultiLingualObject where TTranslation : class, IObjectTranslation; } diff --git a/framework/src/Volo.Abp.MultiLingualObjects/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager.cs b/framework/src/Volo.Abp.MultiLingualObjects/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager.cs index afb4509917..908431e997 100644 --- a/framework/src/Volo.Abp.MultiLingualObjects/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager.cs +++ b/framework/src/Volo.Abp.MultiLingualObjects/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager.cs @@ -19,16 +19,16 @@ public class MultiLingualObjectManager : IMultiLingualObjectManager, ITransientD { SettingProvider = settingProvider; } - public virtual async Task GetTranslationAsync( - ICollection translations, - string culture, + public virtual async Task GetTranslationAsync( + IEnumerable translations, + string? culture, bool fallbackToParentCultures) where TTranslation : class, IObjectTranslation { culture ??= CultureInfo.CurrentUICulture.Name; - if (translations.IsNullOrEmpty()) + if (translations == null || !translations.Any()) { return null; } @@ -65,9 +65,9 @@ public class MultiLingualObjectManager : IMultiLingualObjectManager, ITransientD return translation; } - public virtual Task GetTranslationAsync( + public virtual Task GetTranslationAsync( TMultiLingual multiLingual, - string culture = null, + string? culture = null, bool fallbackToParentCultures = true) where TMultiLingual : IMultiLingualObject where TTranslation : class, IObjectTranslation @@ -75,13 +75,13 @@ public class MultiLingualObjectManager : IMultiLingualObjectManager, ITransientD return GetTranslationAsync(multiLingual.Translations, culture: culture, fallbackToParentCultures: fallbackToParentCultures); } - protected virtual TTranslation GetTranslationBasedOnCulturalRecursive( - CultureInfo culture, ICollection translations, int currentDepth) + protected virtual TTranslation? GetTranslationBasedOnCulturalRecursive( + CultureInfo culture, IEnumerable translations, int currentDepth) where TTranslation : class, IObjectTranslation { if (culture == null || culture.Name.IsNullOrWhiteSpace() || - translations.IsNullOrEmpty() || + translations == null || !translations.Any() || currentDepth > MaxCultureFallbackDepth) { return null; @@ -89,5 +89,108 @@ public class MultiLingualObjectManager : IMultiLingualObjectManager, ITransientD var translation = translations.FirstOrDefault(pt => pt.Language.Equals(culture.Name, StringComparison.OrdinalIgnoreCase)); return translation ?? GetTranslationBasedOnCulturalRecursive(culture.Parent, translations, currentDepth + 1); - } + } + + public virtual async Task> GetBulkTranslationsAsync(IEnumerable> translationsCombined, string? culture, bool fallbackToParentCultures) + where TTranslation : class, IObjectTranslation + { + culture ??= CultureInfo.CurrentUICulture.Name; + + if (translationsCombined == null || !translationsCombined.Any()) + { + return new(); + } + + var someHaveNoTranslations = false; + var res = new List(); + foreach (var translations in translationsCombined) + { + if (!translations.Any()) + { + //if the src has no translations, don't try to find a translation + res.Add(null); + continue; + } + var translation = translations.FirstOrDefault(pt => pt.Language == culture); + if (translation != null) + { + res.Add(translation); + } + else + { + if (fallbackToParentCultures) + { + translation = GetTranslationBasedOnCulturalRecursive( + CultureInfo.CurrentUICulture.Parent, + translations, + 0 + ); + + if (translation != null) + { + res.Add(translation); + } + else + { + res.Add(null); + someHaveNoTranslations = true; + } + } + else + { + res.Add(null); + someHaveNoTranslations = true; + } + } + } + + + if (someHaveNoTranslations) + { + var defaultLanguage = await SettingProvider.GetOrNullAsync(LocalizationSettingNames.DefaultLanguage); + + var index = 0; + foreach (var translations in translationsCombined) + { + if (!translations.Any()) + { + //don't try to find a translation + } + else + { + var translation = res[index]; + if (translation != null) + { + continue; + } + translation = translations.FirstOrDefault(pt => pt.Language == defaultLanguage); + if (translation != null) + { + res[index] = translation; + } + else + { + res[index] = translations.FirstOrDefault(); + } + } + index++; + } + } + return res; + } + + public virtual async Task> GetBulkTranslationsAsync(IEnumerable multiLinguals, string? culture, bool fallbackToParentCultures) + where TMultiLingual : IMultiLingualObject + where TTranslation : class, IObjectTranslation + { + var resInitial = await GetBulkTranslationsAsync(multiLinguals.Select(x => x.Translations), culture, fallbackToParentCultures); + var index = 0; + var res = new List<(TMultiLingual entity, TTranslation? translation)>(); + foreach (var item in multiLinguals) + { + var t = resInitial[index++]; + res.Add((item, t)); + } + return res; + } } diff --git a/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo.Abp.MultiLingualObjects.Tests.csproj b/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo.Abp.MultiLingualObjects.Tests.csproj index a037cfbcf3..b26ea500bf 100644 --- a/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo.Abp.MultiLingualObjects.Tests.csproj +++ b/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo.Abp.MultiLingualObjects.Tests.csproj @@ -1,18 +1,21 @@ - + - - net7.0 - - + + net7.0 + + enable + - - - - - - - + + + + + + + + + diff --git a/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/AbpMultiLingualObjectsTestModule.cs b/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/AbpMultiLingualObjectsTestModule.cs index f099b4fa6c..6fc3a45132 100644 --- a/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/AbpMultiLingualObjectsTestModule.cs +++ b/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/AbpMultiLingualObjectsTestModule.cs @@ -1,19 +1,34 @@ -using Volo.Abp.Autofac; -using Volo.Abp.Localization; -using Volo.Abp.Modularity; -using Volo.Abp.ObjectMapping; -using Volo.Abp.Settings; - -namespace Volo.Abp.MultiLingualObjects; - -[DependsOn( - typeof(AbpAutofacModule), - typeof(AbpLocalizationModule), - typeof(AbpSettingsModule), - typeof(AbpObjectMappingModule), - typeof(AbpMultiLingualObjectsModule), - typeof(AbpTestBaseModule) -)] -public class AbpMultiLingualObjectsTestModule : AbpModule -{ -} +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Autofac; +using Volo.Abp.AutoMapper; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Settings; + +namespace Volo.Abp.MultiLingualObjects; + +[DependsOn( + typeof(AbpAutofacModule), + typeof(AbpLocalizationModule), + typeof(AbpSettingsModule), + typeof(AbpObjectMappingModule), + typeof(AbpMultiLingualObjectsModule), + typeof(AbpTestBaseModule), + typeof(AbpAutoMapperModule) +)] +public class AbpMultiLingualObjectsTestModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.DefinitionProviders.Add(); + }); + context.Services.AddAutoMapperObjectMapper(); + Configure(options => + { + options.AddMaps(validate: true); + }); + } +} diff --git a/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs b/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs index afe15bff99..e442d80953 100644 --- a/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs +++ b/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Shouldly; +using Volo.Abp.AutoMapper; using Volo.Abp.Localization; using Volo.Abp.MultiLingualObjects.TestObjects; using Volo.Abp.Testing; @@ -14,40 +16,63 @@ public class MultiLingualObjectManager_Tests : AbpIntegratedTest _books; + private readonly IMapperAccessor _mapperAccessor; + private readonly Dictionary _testTranslations = new() + { + ["ar"] = "C# التعمق في", + ["zh-Hans"] = "深入理解C#", + ["en"] = "C# in Depth" + }; + public MultiLingualObjectManager_Tests() { - _multiLingualObjectManager = ServiceProvider.GetRequiredService(); - + _multiLingualObjectManager = ServiceProvider.GetRequiredService(); + + //Single Lookup + _book = GetTestBook("en", "zh-Hans"); + //Bulk lookup + _books = new List + { + //has no translations + GetTestBook(), + //english only + GetTestBook("en"), + //arabic only + GetTestBook("ar"), + //arabic + english + GetTestBook("en","ar"), + //arabic + english + chineese + GetTestBook("en", "ar", "zh-Hans") + }; + _mapperAccessor = ServiceProvider.GetRequiredService(); + } + MultiLingualBook GetTestBook(params string[] included) + { var id = Guid.NewGuid(); - _book = new MultiLingualBook(id, 100) - { - Translations = new List() - }; - - var en = new MultiLingualBookTranslation - { - Language = "en", - Name = "C# in Depth", - }; - var zh = new MultiLingualBookTranslation - { - Language = "zh-Hans", - Name = "深入理解C#", - }; - - _book.Translations.Add(en); - _book.Translations.Add(zh); - } + //Single book + var res = new MultiLingualBook(id, 100); + foreach (var language in included) + { + res.Translations.Add(new MultiLingualBookTranslation + { + Language = language, + Name = _testTranslations[language], + }); + } + + return res; + } + [Fact] public async Task GetTranslationAsync() { using (CultureHelper.Use("en-us")) { var translation = await _multiLingualObjectManager.GetTranslationAsync(_book); - - translation.Name.ShouldBe("C# in Depth"); + translation.ShouldNotBeNull(); + translation.Name.ShouldBe(_testTranslations["en"]); } } @@ -57,8 +82,8 @@ public class MultiLingualObjectManager_Tests : AbpIntegratedTest(_book, culture: "en"); + translation.ShouldNotBeNull(); + translation.Name.ShouldBe(_testTranslations["en"]); + } + } + + + [Fact] + public async Task GetBulkTranslationsAsync() + { + using (CultureHelper.Use("en-us")) + { + var translations = await _multiLingualObjectManager.GetBulkTranslationsAsync(_books); + foreach (var (entity, translation) in translations) + { + if (entity.Translations.Any(x => x.Language == "en")) + { + translation.ShouldNotBeNull(); + translation.Name.ShouldBe(_testTranslations["en"]); + } + else + { + translation.ShouldBeNull(); + } + } + } + } - translation.Name.ShouldBe("C# in Depth"); + [Fact] + public async Task GetBulkTranslationsFromListAsync() + { + using (CultureHelper.Use("en-us")) + { + var translations = await _multiLingualObjectManager.GetBulkTranslationsAsync(_books.Select(x => x.Translations)); + foreach (var translation in translations) + { + translation?.Name.ShouldBe(_testTranslations["en"]); + } } } + + [Fact] + public async Task TestBulkMapping() + { + using (CultureHelper.Use("en-us")) + { + var translations = await _multiLingualObjectManager.GetBulkTranslationsAsync(_books); + var translationsDict = translations.ToDictionary(x => x.entity.Id, x => x.translation); + var mapped = _mapperAccessor.Mapper.Map, List>(_books, options => + { + options.Items.Add(nameof(MultiLingualBookTranslation), translationsDict); + }); + Assert.Equal(mapped.Count, _books.Count); + for (int i = 0; i < mapped.Count; i++) + { + var og = _books[i]; + var m = mapped[i]; + Assert.Equal(og.Translations.FirstOrDefault(x => x.Language == "en")?.Name, m.Name); + } + } + } } diff --git a/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectTestProfile.cs b/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectTestProfile.cs new file mode 100644 index 0000000000..89cc9a51b7 --- /dev/null +++ b/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectTestProfile.cs @@ -0,0 +1,23 @@ +namespace Volo.Abp.MultiLingualObjects; + +using System; +using System.Collections.Generic; +using global::AutoMapper; +using Volo.Abp.MultiLingualObjects.TestObjects; + +public class MultiLingualObjectTestProfile : Profile +{ + public MultiLingualObjectTestProfile() + { + CreateMap() + .ForMember(x => x.Name, + x => x.MapFrom((src, target, member, context) => + { + if (context.Items.TryGetValue(nameof(MultiLingualBookTranslation), out var translationsRaw) && translationsRaw is IReadOnlyDictionary translations) + { + return translations.GetValueOrDefault(src.Id)?.Name; + } + return null; + })); + } +} diff --git a/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBook.cs b/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBook.cs index 92a295e0c5..3b880f48f2 100644 --- a/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBook.cs +++ b/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBook.cs @@ -1,19 +1,19 @@ -using System; -using System.Collections.Generic; - -namespace Volo.Abp.MultiLingualObjects.TestObjects; - -public class MultiLingualBook : IMultiLingualObject -{ - public MultiLingualBook(Guid id, decimal price) - { - Id = id; - Price = price; - } - - public Guid Id { get; } - - public decimal Price { get; set; } - - public ICollection Translations { get; set; } -} +using System; +using System.Collections.Generic; + +namespace Volo.Abp.MultiLingualObjects.TestObjects; + +public class MultiLingualBook : IMultiLingualObject +{ + public MultiLingualBook(Guid id, decimal price) + { + Id = id; + Price = price; + } + + public Guid Id { get; } + + public decimal Price { get; set; } + + public ICollection Translations { get; set; } = new List(); +} diff --git a/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBookDto.cs b/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBookDto.cs index ea7e7e9afd..8d61449961 100644 --- a/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBookDto.cs +++ b/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBookDto.cs @@ -1,12 +1,12 @@ -using System; - -namespace Volo.Abp.MultiLingualObjects.TestObjects; - -public class MultiLingualBookDto -{ - public Guid Id { get; set; } - - public string Name { get; set; } - - public decimal Price { get; set; } -} +using System; + +namespace Volo.Abp.MultiLingualObjects.TestObjects; + +public class MultiLingualBookDto +{ + public Guid Id { get; set; } + + public string? Name { get; set; } + + public decimal Price { get; set; } +} diff --git a/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBookTranslation.cs b/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBookTranslation.cs index 06cfc33b55..adf482fa17 100644 --- a/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBookTranslation.cs +++ b/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBookTranslation.cs @@ -1,8 +1,8 @@ -namespace Volo.Abp.MultiLingualObjects.TestObjects; - -public class MultiLingualBookTranslation : IObjectTranslation -{ - public string Name { get; set; } - - public string Language { get; set; } -} +namespace Volo.Abp.MultiLingualObjects.TestObjects; + +public class MultiLingualBookTranslation : IObjectTranslation +{ + public string? Name { get; set; } + + public required string Language { get; set; } +}