diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs index 7265b763b89a..a200972cf7dc 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs @@ -317,7 +317,7 @@ private bool TryGetValueWithLanguageFallback(TryGetValueForCultureAndSegment< } var culture2 = language2.IsoCode; - T? culture2Value = getValue(culture2, segment); + T? culture2Value = TryGetExplicitlyContextualizedValue(getValue, culture2, segment); if (culture2Value != null) { value = culture2Value; @@ -329,6 +329,26 @@ private bool TryGetValueWithLanguageFallback(TryGetValueForCultureAndSegment< } private bool TryGetValueWithDefaultLanguageFallback(IPublishedProperty property, string? culture, string? segment, out T? value) + => TryGetValueWithDefaultLanguageFallback( + (actualCulture, actualSegment) + => property.HasValue(actualCulture, actualSegment) + ? property.Value(this, actualCulture, actualSegment) + : default, + culture, + segment, + out value); + + private bool TryGetValueWithDefaultLanguageFallback(IPublishedElement element, string alias, string? culture, string? segment, out T? value) + => TryGetValueWithDefaultLanguageFallback( + (actualCulture, actualSegment) + => element.HasValue(alias, actualCulture, actualSegment) + ? element.Value(this, alias, actualCulture, actualSegment) + : default, + culture, + segment, + out value); + + private bool TryGetValueWithDefaultLanguageFallback(TryGetValueForCultureAndSegment getValue, string? culture, string? segment, out T? value) { value = default; @@ -337,33 +357,39 @@ private bool TryGetValueWithDefaultLanguageFallback(IPublishedProperty proper return false; } - string? defaultCulture = _localizationService?.GetDefaultLanguageIsoCode(); - if (culture.InvariantEquals(defaultCulture) == false && property.HasValue(defaultCulture, segment)) + var defaultCulture = _localizationService?.GetDefaultLanguageIsoCode(); + if (defaultCulture.IsNullOrWhiteSpace()) { - value = property.Value(this, defaultCulture, segment); - return true; + return false; } - return false; - } - - private bool TryGetValueWithDefaultLanguageFallback(IPublishedElement element, string alias, string? culture, string? segment, out T? value) - { - value = default; - - if (culture.IsNullOrWhiteSpace()) + if (culture.InvariantEquals(defaultCulture)) { return false; } - string? defaultCulture = _localizationService?.GetDefaultLanguageIsoCode(); - if (culture.InvariantEquals(defaultCulture) == false && element.HasValue(alias, defaultCulture, segment)) + T? fallbackValue = TryGetExplicitlyContextualizedValue(getValue, defaultCulture, segment); + if (fallbackValue == null) { - value = element.Value(this, alias, defaultCulture, segment); - return true; + return false; } - return false; + value = fallbackValue; + return true; + } + + private T? TryGetExplicitlyContextualizedValue(TryGetValueForCultureAndSegment getValue, string culture, string? segment) + { + VariationContext? current = _variationContextAccessor.VariationContext; + try + { + _variationContextAccessor.VariationContext = new VariationContext(culture, segment); + return getValue(culture, segment); + } + finally + { + _variationContextAccessor.VariationContext = current; + } } private delegate T? TryGetValueForCultureAndSegment(string actualCulture, string? actualSegment); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishedContentFallbackTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishedContentFallbackTests.cs index f051aa1557e0..6739487317f1 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishedContentFallbackTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishedContentFallbackTests.cs @@ -35,6 +35,8 @@ public class PublishedContentFallbackTests : UmbracoIntegrationTest private IApiContentBuilder ApiContentBuilder => GetRequiredService(); + private ILanguageService LanguageService => GetRequiredService(); + protected override void CustomTestSetup(IUmbracoBuilder builder) => builder .AddUmbracoHybridCache() @@ -98,6 +100,36 @@ public async Task Property_Value_Performs_Fallback_To_Default_Segment_For_Delive Assert.AreEqual(invariantTitle, invariantValue); } + [TestCase("Danish title", true)] + [TestCase("Danish title", false)] + [TestCase(null, true)] + [TestCase(null, false)] + public async Task Property_Value_Can_Perform_Explicit_Language_Fallback(string? danishTitle, bool performFallbackToDefaultLanguage) + { + var danishLanguage = new Language("da-DK", "Danish") + { + FallbackIsoCode = "en-US" + }; + await LanguageService.CreateAsync(danishLanguage, Constants.Security.SuperUserKey); + + UmbracoContextFactory.EnsureUmbracoContext(); + + const string englishTitle = "English title"; + var publishedContent = await SetupCultureVariantContentAsync(englishTitle, danishTitle); + + VariationContextAccessor.VariationContext = new VariationContext(culture: "da-DK", segment: null); + var danishValue = publishedContent.Value(PublishedValueFallback, "title"); + Assert.AreEqual(danishTitle ?? string.Empty, danishValue); + + var fallback = performFallbackToDefaultLanguage ? Fallback.ToDefaultLanguage : Fallback.ToLanguage; + var fallbackValue = publishedContent.Value(PublishedValueFallback, "title", fallback: fallback); + Assert.AreEqual(danishTitle ?? englishTitle, fallbackValue); + + VariationContextAccessor.VariationContext = new VariationContext(culture: "en-US", segment: null); + var englishValue = publishedContent.Value(PublishedValueFallback, "title"); + Assert.AreEqual(englishTitle, englishValue); + } + private async Task SetupSegmentedContentAsync(string? invariantTitle, string? segmentedTitle) { var contentType = new ContentTypeBuilder() @@ -124,11 +156,47 @@ private async Task SetupSegmentedContentAsync(string? invaria ContentService.Save(content); ContentService.Publish(content, ["*"]); + return GetPublishedContent(content.Key); + } + + private async Task SetupCultureVariantContentAsync(string englishTitle, string? danishTitle) + { + var contentType = new ContentTypeBuilder() + .WithAlias("theContentType") + .WithContentVariation(ContentVariation.Culture) + .AddPropertyType() + .WithAlias("title") + .WithName("Title") + .WithDataTypeId(Constants.DataTypes.Textbox) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithValueStorageType(ValueStorageType.Nvarchar) + .WithVariations(ContentVariation.Culture) + .Done() + .WithAllowAsRoot(true) + .Build(); + await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey); + + var content = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName("en-US", "EN") + .WithCultureName("da-DK", "DA") + .WithName("Content") + .Build(); + content.SetValue("title", englishTitle, culture: "en-US"); + content.SetValue("title", danishTitle, culture: "da-DK"); + ContentService.Save(content); + ContentService.Publish(content, ["en-US", "da-DK"]); + + return GetPublishedContent(content.Key); + } + + private IPublishedContent GetPublishedContent(Guid key) + { ContentCacheRefresher.Refresh([new ContentCacheRefresher.JsonPayload { ChangeTypes = TreeChangeTypes.RefreshAll }]); UmbracoContextAccessor.Clear(); var umbracoContext = UmbracoContextFactory.EnsureUmbracoContext().UmbracoContext; - var publishedContent = umbracoContext.Content.GetById(content.Key); + var publishedContent = umbracoContext.Content.GetById(key); Assert.IsNotNull(publishedContent); return publishedContent; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs index 48e225045d28..db57a313e86c 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs @@ -1932,4 +1932,95 @@ void AssertPropertyValues(string culture, IContent expectedPickedContent) Assert.AreEqual(expectedPickedContent.Key, actualPickedPublishedContent.Key); } } + + [TestCase(ContentVariation.Culture, false)] + [TestCase(ContentVariation.Culture, true)] + [TestCase(ContentVariation.Nothing, false)] + [TestCase(ContentVariation.Nothing, true)] + public async Task Can_Perform_Language_Fallback(ContentVariation elementTypeVariation, bool performFallbackToDefaultLanguage) + { + var daDkLanguage = await LanguageService.GetAsync("da-DK"); + Assert.IsNotNull(daDkLanguage); + daDkLanguage.FallbackIsoCode = "en-US"; + var saveLanguageResult = await LanguageService.UpdateAsync(daDkLanguage, Constants.Security.SuperUserKey); + Assert.IsTrue(saveLanguageResult.Success); + + daDkLanguage = await LanguageService.GetAsync("da-DK"); + Assert.AreEqual("en-US", daDkLanguage?.FallbackIsoCode); + + var elementType = CreateElementType(elementTypeVariation); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType, ContentVariation.Culture); + + var content = CreateContent( + contentType, + elementType, + new [] + { + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "English invariantText content value" }, + new() { Alias = "variantText", Value = "English variantText content value" } + }, + new List + { + new() { Alias = "invariantText", Value = "English invariantText settings value" }, + new() { Alias = "variantText", Value = "English variantText settings value" } + }, + "en-US", + null) + }, + true); + + AssertPropertyValuesWithFallback("en-US", + "English invariantText content value", "English variantText content value", + "English invariantText settings value", "English variantText settings value"); + + AssetEmptyPropertyValues("da-DK"); + + AssertPropertyValuesWithFallback("da-DK", + "English invariantText content value", "English variantText content value", + "English invariantText settings value", "English variantText settings value"); + + void AssertPropertyValuesWithFallback(string culture, + string expectedInvariantContentValue, string expectedVariantContentValue, + string expectedInvariantSettingsValue, string expectedVariantSettingsValue) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var fallback = performFallbackToDefaultLanguage ? Fallback.ToDefaultLanguage : Fallback.ToLanguage; + + var publishedValueFallback = GetRequiredService(); + var value = publishedContent.Value(publishedValueFallback, "blocks", fallback: fallback); + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + Assert.AreEqual(expectedInvariantContentValue, blockListItem.Content.Value("invariantText")); + Assert.AreEqual(expectedVariantContentValue, blockListItem.Content.Value("variantText")); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + Assert.AreEqual(expectedInvariantSettingsValue, blockListItem.Settings.Value("invariantText")); + Assert.AreEqual(expectedVariantSettingsValue, blockListItem.Settings.Value("variantText")); + }); + } + + void AssetEmptyPropertyValues(string culture) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.NotNull(value); + Assert.IsEmpty(value); + } + } }