diff --git a/src/Umbraco.Core/Collections/CompositeIntStringKey.cs b/src/Umbraco.Core/Collections/CompositeIntStringKey.cs index eb9db80990..74ef4e45e1 100644 --- a/src/Umbraco.Core/Collections/CompositeIntStringKey.cs +++ b/src/Umbraco.Core/Collections/CompositeIntStringKey.cs @@ -2,6 +2,7 @@ namespace Umbraco.Core.Collections { + /// /// Represents a composite key of (int, string) for fast dictionaries. /// @@ -40,4 +41,4 @@ namespace Umbraco.Core.Collections public static bool operator !=(CompositeIntStringKey key1, CompositeIntStringKey key2) => key1._key2 != key2._key2 || key1._key1 != key2._key1; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Collections/IReadOnlyKeyedCollection.cs b/src/Umbraco.Core/Collections/IReadOnlyKeyedCollection.cs new file mode 100644 index 0000000000..8d78241275 --- /dev/null +++ b/src/Umbraco.Core/Collections/IReadOnlyKeyedCollection.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Collections +{ + /// + /// A readonly keyed collection + /// + /// + public interface IReadOnlyKeyedCollection : IReadOnlyList + { + IEnumerable Keys { get; } + bool TryGetValue(TKey key, out TVal val); + TVal this[string key] { get; } + bool Contains(TKey key); + } +} diff --git a/src/Umbraco.Core/ContentExtensions.cs b/src/Umbraco.Core/ContentExtensions.cs index e36731a8cb..4f88c2b803 100644 --- a/src/Umbraco.Core/ContentExtensions.cs +++ b/src/Umbraco.Core/ContentExtensions.cs @@ -133,7 +133,7 @@ namespace Umbraco.Core } #endregion - + /// /// Removes characters that are not valide XML characters from all entity properties /// of type string. See: http://stackoverflow.com/a/961504/5018 diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index 238d87b186..2ee6762e61 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.Serialization; +using Umbraco.Core.Collections; using Umbraco.Core.Exceptions; namespace Umbraco.Core.Models @@ -20,8 +21,8 @@ namespace Umbraco.Core.Models private PublishedState _publishedState; private DateTime? _releaseDate; private DateTime? _expireDate; - private Dictionary _publishInfos; - private Dictionary _publishInfosOrig; + private CultureNameCollection _publishInfos; + private CultureNameCollection _publishInfosOrig; private HashSet _editedCultures; private static readonly Lazy Ps = new Lazy(); @@ -221,13 +222,13 @@ namespace Umbraco.Core.Models public bool IsCulturePublished(string culture) // just check _publishInfos // a non-available culture could not become published anyways - => _publishInfos != null && _publishInfos.ContainsKey(culture); + => _publishInfos != null && _publishInfos.Contains(culture); /// public bool WasCulturePublished(string culture) // just check _publishInfosOrig - a copy of _publishInfos // a non-available culture could not become published anyways - => _publishInfosOrig != null && _publishInfosOrig.ContainsKey(culture); + => _publishInfosOrig != null && _publishInfosOrig.Contains(culture); /// public bool IsCultureEdited(string culture) @@ -237,7 +238,7 @@ namespace Umbraco.Core.Models /// [IgnoreDataMember] - public IReadOnlyDictionary PublishNames => _publishInfos?.ToDictionary(x => x.Key, x => x.Value.Name, StringComparer.OrdinalIgnoreCase) ?? NoNames; + public IReadOnlyKeyedCollection PublishNames => _publishInfos ?? NoNames; /// public string GetPublishName(string culture) @@ -267,9 +268,11 @@ namespace Umbraco.Core.Models throw new ArgumentNullOrEmptyException(nameof(culture)); if (_publishInfos == null) - _publishInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); + _publishInfos = new CultureNameCollection(); - _publishInfos[culture.ToLowerInvariant()] = (name, date); + //TODO: Track changes? + + _publishInfos.AddOrUpdate(culture, name, date); } private void ClearPublishInfos() @@ -430,7 +433,11 @@ namespace Umbraco.Core.Models // take care of publish infos _publishInfosOrig = _publishInfos == null ? null - : new Dictionary(_publishInfos, StringComparer.OrdinalIgnoreCase); + : new CultureNameCollection(_publishInfos); + + if (_publishInfos != null) + foreach (var cultureName in _publishInfos) + cultureName.ResetDirtyProperties(rememberDirty); } /// diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index bf2fd580d9..2336188c50 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using System.Runtime.Serialization; using System.Web; +using Umbraco.Core.Collections; using Umbraco.Core.Exceptions; using Umbraco.Core.Models.Entities; @@ -19,14 +20,14 @@ namespace Umbraco.Core.Models [DebuggerDisplay("Id: {Id}, Name: {Name}, ContentType: {ContentTypeBase.Alias}")] public abstract class ContentBase : TreeEntityBase, IContentBase { - protected static readonly Dictionary NoNames = new Dictionary(); + protected static readonly CultureNameCollection NoNames = new CultureNameCollection(); private static readonly Lazy Ps = new Lazy(); private int _contentTypeId; protected IContentTypeComposition ContentTypeBase; private int _writerId; private PropertyCollection _properties; - private Dictionary _cultureInfos; + private CultureNameCollection _cultureInfos; /// /// Initializes a new instance of the class. @@ -69,7 +70,7 @@ namespace Umbraco.Core.Models public readonly PropertyInfo DefaultContentTypeIdSelector = ExpressionHelper.GetPropertyInfo(x => x.ContentTypeId); public readonly PropertyInfo PropertyCollectionSelector = ExpressionHelper.GetPropertyInfo(x => x.Properties); public readonly PropertyInfo WriterSelector = ExpressionHelper.GetPropertyInfo(x => x.WriterId); - public readonly PropertyInfo NamesSelector = ExpressionHelper.GetPropertyInfo>(x => x.CultureNames); + public readonly PropertyInfo CultureNamesSelector = ExpressionHelper.GetPropertyInfo>(x => x.CultureNames); } protected void PropertiesChanged(object sender, NotifyCollectionChangedEventArgs e) @@ -147,18 +148,18 @@ namespace Umbraco.Core.Models /// public IEnumerable AvailableCultures - => _cultureInfos?.Select(x => x.Key) ?? Enumerable.Empty(); + => _cultureInfos?.Keys ?? Enumerable.Empty(); /// public bool IsCultureAvailable(string culture) - => _cultureInfos != null && _cultureInfos.ContainsKey(culture); + => _cultureInfos != null && _cultureInfos.Contains(culture); /// [DataMember] - public virtual IReadOnlyDictionary CultureNames => _cultureInfos?.ToDictionary(x => x.Key, x => x.Value.Name, StringComparer.OrdinalIgnoreCase) ?? NoNames; - + public virtual IReadOnlyKeyedCollection CultureNames => _cultureInfos ?? NoNames; + /// - public virtual string GetCultureName(string culture) + public string GetCultureName(string culture) { if (culture.IsNullOrWhiteSpace()) return Name; if (!ContentTypeBase.VariesByCulture()) return null; @@ -176,7 +177,7 @@ namespace Umbraco.Core.Models } /// - public virtual void SetCultureName(string name, string culture) + public void SetCultureName(string name, string culture) { if (ContentTypeBase.VariesByCulture()) // set on variant content type { @@ -202,16 +203,18 @@ namespace Umbraco.Core.Models } } + //fixme: this isn't used anywhere internal void TouchCulture(string culture) { if (ContentTypeBase.VariesByCulture() && _cultureInfos != null && _cultureInfos.TryGetValue(culture, out var infos)) - _cultureInfos[culture] = (infos.Name, DateTime.Now); + _cultureInfos.AddOrUpdate(culture, infos.Name, DateTime.Now); } protected void ClearCultureInfos() { + if (_cultureInfos != null) + _cultureInfos.Clear(); _cultureInfos = null; - OnPropertyChanged(Ps.Value.NamesSelector); } protected void ClearCultureInfo(string culture) @@ -223,7 +226,6 @@ namespace Umbraco.Core.Models _cultureInfos.Remove(culture); if (_cultureInfos.Count == 0) _cultureInfos = null; - OnPropertyChanged(Ps.Value.NamesSelector); } // internal for repository @@ -236,10 +238,22 @@ namespace Umbraco.Core.Models throw new ArgumentNullOrEmptyException(nameof(culture)); if (_cultureInfos == null) - _cultureInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); + { + _cultureInfos = new CultureNameCollection(); + _cultureInfos.CollectionChanged += CultureNamesCollectionChanged; + } - _cultureInfos[culture.ToLowerInvariant()] = (name, date); - OnPropertyChanged(Ps.Value.NamesSelector); + _cultureInfos.AddOrUpdate(culture, name, date); + } + + /// + /// Event handler for when the culture names collection is modified + /// + /// + /// + private void CultureNamesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + OnPropertyChanged(Ps.Value.CultureNamesSelector); } #endregion @@ -387,6 +401,11 @@ namespace Umbraco.Core.Models // also reset dirty changes made to user's properties foreach (var prop in Properties) prop.ResetDirtyProperties(rememberDirty); + + // take care of culture names + if (_cultureInfos != null) + foreach (var cultureName in _cultureInfos) + cultureName.ResetDirtyProperties(rememberDirty); } /// diff --git a/src/Umbraco.Core/Models/CultureName.cs b/src/Umbraco.Core/Models/CultureName.cs new file mode 100644 index 0000000000..64db50c36d --- /dev/null +++ b/src/Umbraco.Core/Models/CultureName.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Umbraco.Core.Models.Entities; + +namespace Umbraco.Core.Models +{ + /// + /// The name of a content variant for a given culture + /// + public class CultureName : BeingDirtyBase, IDeepCloneable, IEquatable + { + private DateTime _date; + private string _name; + private static readonly Lazy Ps = new Lazy(); + + public CultureName(string culture, string name, DateTime date) + { + if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("message", nameof(culture)); + Culture = culture; + _name = name; + _date = date; + } + + public string Culture { get; private set; } + + public string Name + { + get => _name; + set => SetPropertyValueAndDetectChanges(value, ref _name, Ps.Value.NameSelector); + } + + public DateTime Date + { + get => _date; + set => SetPropertyValueAndDetectChanges(value, ref _date, Ps.Value.DateSelector); + } + + public object DeepClone() + { + return new CultureName(Culture, Name, Date); + } + + public override bool Equals(object obj) + { + return obj is CultureName && Equals((CultureName)obj); + } + + public bool Equals(CultureName other) + { + return Culture == other.Culture && + Name == other.Name; + } + + public override int GetHashCode() + { + var hashCode = 479558943; + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Culture); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Name); + return hashCode; + } + + /// + /// Allows deconstructing into culture and name + /// + /// + /// + public void Deconstruct(out string culture, out string name) + { + culture = Culture; + name = Name; + } + + // ReSharper disable once ClassNeverInstantiated.Local + private class PropertySelectors + { + public readonly PropertyInfo CultureSelector = ExpressionHelper.GetPropertyInfo(x => x.Culture); + public readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name); + public readonly PropertyInfo DateSelector = ExpressionHelper.GetPropertyInfo(x => x.Date); + } + } +} diff --git a/src/Umbraco.Core/Models/CultureNameCollection.cs b/src/Umbraco.Core/Models/CultureNameCollection.cs new file mode 100644 index 0000000000..3c00603c88 --- /dev/null +++ b/src/Umbraco.Core/Models/CultureNameCollection.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using Umbraco.Core.Collections; + +namespace Umbraco.Core.Models +{ + + + /// + /// The culture names of a content's variants + /// + public class CultureNameCollection : KeyedCollection, INotifyCollectionChanged, IDeepCloneable, IReadOnlyKeyedCollection + { + /// + /// Creates a new collection from another collection + /// + /// + public CultureNameCollection(IEnumerable names) : base(StringComparer.InvariantCultureIgnoreCase) + { + foreach (var n in names) + Add(n); + } + + /// + /// Creates a new collection + /// + public CultureNameCollection() : base(StringComparer.InvariantCultureIgnoreCase) + { + } + + /// + /// Returns all keys in the collection + /// + public IEnumerable Keys => Dictionary != null ? Dictionary.Keys : this.Select(x => x.Culture); + + public bool TryGetValue(string culture, out CultureName name) + { + name = this.FirstOrDefault(x => x.Culture.InvariantEquals(culture)); + return name != null; + } + + /// + /// Add or update the + /// + /// + public void AddOrUpdate(string culture, string name, DateTime date) + { + culture = culture.ToLowerInvariant(); + if (TryGetValue(culture, out var found)) + { + found.Name = name; + found.Date = date; + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, found, found)); + } + else + Add(new CultureName(culture, name, date)); + } + + /// + /// Gets the index for a specified culture + /// + public int IndexOfKey(string key) + { + for (var i = 0; i < Count; i++) + { + if (this[i].Culture.InvariantEquals(key)) + return i; + } + return -1; + } + + public event NotifyCollectionChangedEventHandler CollectionChanged; + + protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args) + { + CollectionChanged?.Invoke(this, args); + } + + public object DeepClone() + { + var clone = new CultureNameCollection(); + foreach (var name in this) + { + clone.Add((CultureName)name.DeepClone()); + } + return clone; + } + + protected override string GetKeyForItem(CultureName item) + { + return item.Culture; + } + + /// + /// Resets the collection to only contain the instances referenced in the parameter. + /// + /// The property groups. + /// + internal void Reset(IEnumerable names) + { + Clear(); + foreach (var name in names) + Add(name); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + protected override void SetItem(int index, CultureName item) + { + base.SetItem(index, item); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)); + } + + protected override void RemoveItem(int index) + { + var removed = this[index]; + base.RemoveItem(index); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed)); + } + + protected override void InsertItem(int index, CultureName item) + { + base.InsertItem(index, item); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); + } + + protected override void ClearItems() + { + base.ClearItems(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + } +} diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index d9bc32aaf0..3ddffe8f75 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Umbraco.Core.Collections; namespace Umbraco.Core.Models { @@ -132,7 +133,7 @@ namespace Umbraco.Core.Models /// Because a dictionary key cannot be null this cannot get the invariant /// name, which must be get via the property. /// - IReadOnlyDictionary PublishNames { get; } + IReadOnlyKeyedCollection PublishNames { get; } /// /// Gets the published cultures. diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs index 460bd521d4..811cbf74f3 100644 --- a/src/Umbraco.Core/Models/IContentBase.cs +++ b/src/Umbraco.Core/Models/IContentBase.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Umbraco.Core.Collections; using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Models @@ -57,7 +58,7 @@ namespace Umbraco.Core.Models /// Because a dictionary key cannot be null this cannot contain the invariant /// culture name, which must be get or set via the property. /// - IReadOnlyDictionary CultureNames { get; } + IReadOnlyKeyedCollection CultureNames { get; } /// /// Gets the available cultures. diff --git a/src/Umbraco.Core/Models/PropertyGroupCollection.cs b/src/Umbraco.Core/Models/PropertyGroupCollection.cs index d10b375285..80b663fa05 100644 --- a/src/Umbraco.Core/Models/PropertyGroupCollection.cs +++ b/src/Umbraco.Core/Models/PropertyGroupCollection.cs @@ -8,6 +8,7 @@ using System.Threading; namespace Umbraco.Core.Models { + /// /// Represents a collection of objects /// @@ -168,7 +169,7 @@ namespace Umbraco.Core.Models var clone = new PropertyGroupCollection(); foreach (var group in this) { - clone.Add((PropertyGroup) group.DeepClone()); + clone.Add((PropertyGroup)group.DeepClone()); } return clone; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index bf41cd1ad1..83d9e80f5f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -1172,8 +1172,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // using the default culture if it has a name, otherwise anything we can var defaultCulture = LanguageRepository.GetDefaultIsoCode(); content.Name = defaultCulture != null && content.CultureNames.TryGetValue(defaultCulture, out var cultureName) - ? cultureName - : content.CultureNames.First().Value; + ? cultureName.Name + : content.CultureNames[0].Name; } else { @@ -1234,7 +1234,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // update the name, and the publish name if published content.SetCultureName(uniqueName, culture); - if (publishing && content.PublishNames.ContainsKey(culture)) + if (publishing && content.PublishNames.Contains(culture)) content.SetPublishInfo(culture, uniqueName, DateTime.Now); } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index aef18e59db..0eba543e0d 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -109,6 +109,7 @@ + @@ -374,6 +375,8 @@ + + diff --git a/src/Umbraco.Tests/Models/ContentTests.cs b/src/Umbraco.Tests/Models/ContentTests.cs index 32fbd37d0e..a7d0f2f050 100644 --- a/src/Umbraco.Tests/Models/ContentTests.cs +++ b/src/Umbraco.Tests/Models/ContentTests.cs @@ -42,6 +42,33 @@ namespace Umbraco.Tests.Models Container.Register(_ => Mock.Of()); } + [Test] + public void Variant_Names_Track_Dirty_Changes() + { + var contentType = new ContentType(-1) { Alias = "contentType" }; + var content = new Content("content", -1, contentType) { Id = 1, VersionId = 1 }; + + const string langFr = "fr-FR"; + + contentType.Variations = ContentVariation.Culture; + + Assert.IsFalse(content.IsPropertyDirty("CultureNames")); //hasn't been changed + + content.SetCultureName("name-fr", langFr); + Assert.IsTrue(content.IsPropertyDirty("CultureNames")); //now it will be changed since the collection has changed + var frCultureName = content.CultureNames[langFr]; + Assert.IsFalse(frCultureName.IsPropertyDirty("Date")); //this won't be dirty because it wasn't actually updated, just created + + content.ResetDirtyProperties(); + + Assert.IsFalse(content.IsPropertyDirty("CultureNames")); //it's been reset + Assert.IsTrue(content.WasPropertyDirty("CultureNames")); + + content.SetCultureName("name-fr", langFr); + Assert.IsTrue(frCultureName.IsPropertyDirty("Date")); //this will be dirty because it was already created and now has been updated + Assert.IsTrue(content.IsPropertyDirty("CultureNames")); //it's true now since we've updated a name + } + [Test] public void Get_Non_Grouped_Properties() { diff --git a/src/Umbraco.Tests/Models/VariationTests.cs b/src/Umbraco.Tests/Models/VariationTests.cs index 8d566e81f2..0e62c41f46 100644 --- a/src/Umbraco.Tests/Models/VariationTests.cs +++ b/src/Umbraco.Tests/Models/VariationTests.cs @@ -237,10 +237,10 @@ namespace Umbraco.Tests.Models // variant dictionary of names work Assert.AreEqual(2, content.CultureNames.Count); - Assert.IsTrue(content.CultureNames.ContainsKey(langFr)); - Assert.AreEqual("name-fr", content.CultureNames[langFr]); - Assert.IsTrue(content.CultureNames.ContainsKey(langUk)); - Assert.AreEqual("name-uk", content.CultureNames[langUk]); + Assert.IsTrue(content.CultureNames.Contains(langFr)); + Assert.AreEqual("name-fr", content.CultureNames[langFr].Name); + Assert.IsTrue(content.CultureNames.Contains(langUk)); + Assert.AreEqual("name-uk", content.CultureNames[langUk].Name); } [Test] diff --git a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs index 68e29c4efe..62c3116ab1 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs @@ -767,7 +767,7 @@ namespace Umbraco.Tests.Persistence.Repositories foreach (var r in result) { var isInvariant = r.ContentType.Alias == "umbInvariantTextpage"; - var name = isInvariant ? r.Name : r.CultureNames["en-US"]; + var name = isInvariant ? r.Name : r.CultureNames["en-US"].Name; var namePrefix = isInvariant ? "INV" : "VAR"; //ensure the correct name (invariant vs variant) is in the result diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs index 8b23763763..4557dfb1bd 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs @@ -132,7 +132,7 @@ namespace Umbraco.Web.Models.Mapping // if we don't have a name for a culture, it means the culture is not available, and // hey we should probably not be mapping it, but it's too late, return a fallback name - return source.CultureNames.TryGetValue(culture, out var name) && !name.IsNullOrWhiteSpace() ? name : $"(({source.Name}))"; + return source.CultureNames.TryGetValue(culture, out var name) && !name.Name.IsNullOrWhiteSpace() ? name.Name : $"(({source.Name}))"; } } } diff --git a/src/Umbraco.Web/umbraco.presentation/page.cs b/src/Umbraco.Web/umbraco.presentation/page.cs index ac35c336b2..91cf690996 100644 --- a/src/Umbraco.Web/umbraco.presentation/page.cs +++ b/src/Umbraco.Web/umbraco.presentation/page.cs @@ -396,7 +396,7 @@ namespace umbraco return _cultureInfos; return _cultureInfos = _inner.PublishNames - .ToDictionary(x => x.Key, x => new PublishedCultureInfo(x.Key, x.Value, _inner.GetPublishDate(x.Key) ?? DateTime.MinValue)); + .ToDictionary(x => x.Culture, x => new PublishedCultureInfo(x.Culture, x.Name, x.Date)); } }