From ba423a010831844a542169a53a8f7eef36b1c611 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Date: Tue, 18 Jul 2023 11:53:14 +0300 Subject: [PATCH] V12: Add ISO codes to make the migration from language IDs easier (#14567) * Change the obsoletion messages for language IDs to target V14 instead of V13. * Wrong Language file * Add ISO codes required to migrate custom code from language IDs * Population of the new language FallbackIsoCode prop * Changing obsoletion msgs from v13 to v14 * Fix breaking changes --- .../Models/ContentEditing/Language.cs | 2 +- src/Umbraco.Core/Models/DictionaryItem.cs | 2 +- .../Models/DictionaryItemExtensions.cs | 4 +-- .../Models/DictionaryTranslation.cs | 32 ++++++++++++++--- .../Models/IDictionaryTranslation.cs | 14 +++++--- src/Umbraco.Core/Models/ILanguage.cs | 19 +++++++++- src/Umbraco.Core/Models/Language.cs | 11 ++++-- .../Persistence/Factories/LanguageFactory.cs | 3 +- .../Implement/LanguageRepository.cs | 35 ++++++++++++++----- .../Builders/LanguageBuilder.cs | 2 +- 10 files changed, 98 insertions(+), 26 deletions(-) diff --git a/src/Umbraco.Core/Models/ContentEditing/Language.cs b/src/Umbraco.Core/Models/ContentEditing/Language.cs index 112aeb5aac..99c011d608 100644 --- a/src/Umbraco.Core/Models/ContentEditing/Language.cs +++ b/src/Umbraco.Core/Models/ContentEditing/Language.cs @@ -22,7 +22,7 @@ public class Language [DataMember(Name = "isMandatory")] public bool IsMandatory { get; set; } - [Obsolete("This will be replaced by fallback language ISO code in V13.")] + [Obsolete("This will be replaced by fallback language ISO code in V14.")] [DataMember(Name = "fallbackLanguageId")] public int? FallbackLanguageId { get; set; } } diff --git a/src/Umbraco.Core/Models/DictionaryItem.cs b/src/Umbraco.Core/Models/DictionaryItem.cs index 90576a85e3..b0e787de02 100644 --- a/src/Umbraco.Core/Models/DictionaryItem.cs +++ b/src/Umbraco.Core/Models/DictionaryItem.cs @@ -34,7 +34,7 @@ public class DictionaryItem : EntityBase, IDictionaryItem _translations = new List(); } - [Obsolete("This will be removed in V13.")] + [Obsolete("This will be removed in V14.")] public Func? GetLanguage { get; set; } /// diff --git a/src/Umbraco.Core/Models/DictionaryItemExtensions.cs b/src/Umbraco.Core/Models/DictionaryItemExtensions.cs index 09654d5137..341f185ff9 100644 --- a/src/Umbraco.Core/Models/DictionaryItemExtensions.cs +++ b/src/Umbraco.Core/Models/DictionaryItemExtensions.cs @@ -10,7 +10,7 @@ public static class DictionaryItemExtensions /// /// /// - [Obsolete("This will be replaced in V13 by a corresponding method accepting language ISO code instead of language ID.")] + [Obsolete("This will be replaced in V14 by a corresponding method accepting language ISO code instead of language ID.")] public static string? GetTranslatedValue(this IDictionaryItem d, int languageId) { IDictionaryTranslation? trans = d.Translations.FirstOrDefault(x => x.LanguageId == languageId); @@ -22,7 +22,7 @@ public static class DictionaryItemExtensions /// /// /// - [Obsolete("Warning: This method ONLY works in very specific scenarios. It will be removed in V13.")] + [Obsolete("Warning: This method ONLY works in very specific scenarios. It will be removed in V14.")] public static string? GetDefaultValue(this IDictionaryItem d) { IDictionaryTranslation? defaultTranslation = d.Translations.FirstOrDefault(x => x.Language?.Id == 1); diff --git a/src/Umbraco.Core/Models/DictionaryTranslation.cs b/src/Umbraco.Core/Models/DictionaryTranslation.cs index ab79b77e44..7f4471785c 100644 --- a/src/Umbraco.Core/Models/DictionaryTranslation.cs +++ b/src/Umbraco.Core/Models/DictionaryTranslation.cs @@ -1,5 +1,8 @@ using System.Runtime.Serialization; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Models; @@ -11,6 +14,7 @@ namespace Umbraco.Cms.Core.Models; public class DictionaryTranslation : EntityBase, IDictionaryTranslation { private ILanguage? _language; + private string? _languageIsoCode; // note: this will be memberwise cloned private string _value; @@ -20,6 +24,7 @@ public class DictionaryTranslation : EntityBase, IDictionaryTranslation _language = language ?? throw new ArgumentNullException("language"); LanguageId = _language.Id; _value = value; + LanguageIsoCode = language.IsoCode; } public DictionaryTranslation(ILanguage language, string value, Guid uniqueId) @@ -27,17 +32,18 @@ public class DictionaryTranslation : EntityBase, IDictionaryTranslation _language = language ?? throw new ArgumentNullException("language"); LanguageId = _language.Id; _value = value; + LanguageIsoCode = language.IsoCode; Key = uniqueId; } - [Obsolete("Please use constructor that accepts ILanguage. This will be removed in V13.")] + [Obsolete("Please use constructor that accepts ILanguage. This will be removed in V14.")] public DictionaryTranslation(int languageId, string value) { LanguageId = languageId; _value = value; } - [Obsolete("Please use constructor that accepts ILanguage. This will be removed in V13.")] + [Obsolete("Please use constructor that accepts ILanguage. This will be removed in V14.")] public DictionaryTranslation(int languageId, string value, Guid uniqueId) { LanguageId = languageId; @@ -58,7 +64,7 @@ public class DictionaryTranslation : EntityBase, IDictionaryTranslation /// returned /// on a callback. /// - [Obsolete("This will be removed in V13. From V13 onwards you should get languages by ISO code from ILanguageService.")] + [Obsolete("This will be removed in V14. From V14 onwards you should get languages by ISO code from ILanguageService.")] [DataMember] [DoNotClone] public ILanguage? Language @@ -86,7 +92,7 @@ public class DictionaryTranslation : EntityBase, IDictionaryTranslation } } - [Obsolete("This will be replaced by language ISO code in V13.")] + [Obsolete("This will be replaced by language ISO code in V14.")] public int LanguageId { get; private set; } /// @@ -99,6 +105,24 @@ public class DictionaryTranslation : EntityBase, IDictionaryTranslation set => SetPropertyValueAndDetectChanges(value, ref _value!, nameof(Value)); } + /// + public string LanguageIsoCode + { + get + { + // TODO: this won't be necessary after obsoleted ctors are removed in v14. + if (_languageIsoCode is null) + { + var _languageService = StaticServiceProvider.Instance.GetRequiredService(); + _languageIsoCode = _languageService.GetLanguageById(LanguageId)?.IsoCode ?? string.Empty; + } + + return _languageIsoCode; + } + + private set => SetPropertyValueAndDetectChanges(value, ref _languageIsoCode!, nameof(LanguageIsoCode)); + } + protected override void PerformDeepClone(object clone) { base.PerformDeepClone(clone); diff --git a/src/Umbraco.Core/Models/IDictionaryTranslation.cs b/src/Umbraco.Core/Models/IDictionaryTranslation.cs index 45d71e3f9b..8f8d9ffaa4 100644 --- a/src/Umbraco.Core/Models/IDictionaryTranslation.cs +++ b/src/Umbraco.Core/Models/IDictionaryTranslation.cs @@ -6,18 +6,24 @@ namespace Umbraco.Cms.Core.Models; public interface IDictionaryTranslation : IEntity, IRememberBeingDirty { /// - /// Gets or sets the for the translation + /// Gets or sets the for the translation. /// - [Obsolete("This will be removed in V13. From V13 onwards you should get languages by ISO code from ILanguageService.")] + [Obsolete("This will be removed in V14. From V14 onwards you should get languages by ISO code from ILanguageService.")] [DataMember] ILanguage? Language { get; set; } - [Obsolete("This will be replaced by language ISO code in V13.")] + [Obsolete("This will be replaced by language ISO code in V14.")] int LanguageId { get; } /// - /// Gets or sets the translated text + /// Gets or sets the translated text. /// [DataMember] string Value { get; set; } + + /// + /// Gets the ISO code of the language. + /// + [DataMember] + string LanguageIsoCode => Language?.IsoCode ?? string.Empty; } diff --git a/src/Umbraco.Core/Models/ILanguage.cs b/src/Umbraco.Core/Models/ILanguage.cs index 88c76ae7b0..885833cd5c 100644 --- a/src/Umbraco.Core/Models/ILanguage.cs +++ b/src/Umbraco.Core/Models/ILanguage.cs @@ -55,7 +55,24 @@ public interface ILanguage : IEntity, IRememberBeingDirty /// define fallback strategies when a value does not exist for a requested language. /// /// - [Obsolete("This will be replaced by fallback language ISO code in V13.")] + [Obsolete("This will be replaced by fallback language ISO code in V14.")] [DataMember] int? FallbackLanguageId { get; set; } + + + /// + /// Gets or sets the ISO code of a fallback language. + /// + /// + /// + /// The fallback language can be used in multi-lingual scenarios, to help + /// define fallback strategies when a value does not exist for a requested language. + /// + /// + [DataMember] + string? FallbackIsoCode + { + get => null; + set { } + } } diff --git a/src/Umbraco.Core/Models/Language.cs b/src/Umbraco.Core/Models/Language.cs index 9871cf3eed..62a65f086b 100644 --- a/src/Umbraco.Core/Models/Language.cs +++ b/src/Umbraco.Core/Models/Language.cs @@ -1,6 +1,5 @@ using System.Globalization; using System.Runtime.Serialization; -using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.Entities; namespace Umbraco.Cms.Core.Models; @@ -14,6 +13,7 @@ public class Language : EntityBase, ILanguage { private string _cultureName; private int? _fallbackLanguageId; + private string? _fallbackLanguageIsoCode; private bool _isDefaultVariantLanguage; private string _isoCode; private bool _mandatory; @@ -74,10 +74,17 @@ public class Language : EntityBase, ILanguage } /// - [Obsolete("This will be replaced by fallback language ISO code in V13.")] + [Obsolete("This will be replaced by fallback language ISO code in V14.")] public int? FallbackLanguageId { get => _fallbackLanguageId; set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguageId, nameof(FallbackLanguageId)); } + + /// + public string? FallbackIsoCode + { + get => _fallbackLanguageIsoCode; + set => SetPropertyValueAndDetectChanges(value, ref _fallbackLanguageIsoCode, nameof(FallbackIsoCode)); + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs index 9ab958c306..6ef14238af 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs @@ -6,7 +6,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories; internal static class LanguageFactory { - public static ILanguage BuildEntity(LanguageDto dto) + public static ILanguage BuildEntity(LanguageDto dto, string? fallbackIsoCode) { ArgumentNullException.ThrowIfNull(dto); if (dto.IsoCode is null) @@ -22,6 +22,7 @@ internal static class LanguageFactory IsDefault = dto.IsDefault, IsMandatory = dto.IsMandatory, FallbackLanguageId = dto.FallbackLanguageId, + FallbackIsoCode = fallbackIsoCode }; // Reset dirty initial properties diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs index 398a55ebaf..590fae26c0 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs @@ -120,7 +120,19 @@ internal class LanguageRepository : EntityRepositoryBase, ILangu new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); protected ILanguage ConvertFromDto(LanguageDto dto) - => LanguageFactory.BuildEntity(dto); + { + // yes, we want to lock _codeIdMap + lock (_codeIdMap) + { + string? fallbackIsoCode = null; + if (dto.FallbackLanguageId.HasValue && _idCodeMap.TryGetValue(dto.FallbackLanguageId.Value, out fallbackIsoCode) == false) + { + throw new ArgumentException($"The ISO code map did not contain ISO code for fallback language ID: {dto.FallbackLanguageId}. Please reload the caches."); + } + + return LanguageFactory.BuildEntity(dto, fallbackIsoCode); + } + } // do NOT leak that language, it's not deep-cloned! private ILanguage GetDefault() @@ -172,20 +184,25 @@ internal class LanguageRepository : EntityRepositoryBase, ILangu sql.OrderBy(x => x.Id); // get languages - var languages = Database.Fetch(sql).Select(ConvertFromDto).OrderBy(x => x.Id).ToList(); + List? languageDtos = Database.Fetch(sql) ?? new List(); - // initialize the code-id map - lock (_codeIdMap) + // initialize the code-id map if we've reloaded the entire set of languages + if (ids?.Any() == false) { - _codeIdMap.Clear(); - _idCodeMap.Clear(); - foreach (ILanguage language in languages) + lock (_codeIdMap) { - _codeIdMap[language.IsoCode] = language.Id; - _idCodeMap[language.Id] = language.IsoCode.ToLowerInvariant(); + _codeIdMap.Clear(); + _idCodeMap.Clear(); + foreach (LanguageDto languageDto in languageDtos) + { + ArgumentException.ThrowIfNullOrEmpty(languageDto.IsoCode, nameof(LanguageDto.IsoCode)); + _codeIdMap[languageDto.IsoCode] = languageDto.Id; + _idCodeMap[languageDto.Id] = languageDto.IsoCode; + } } } + var languages = languageDtos.Select(ConvertFromDto).OrderBy(x => x.Id).ToList(); return languages; } diff --git a/tests/Umbraco.Tests.Common/Builders/LanguageBuilder.cs b/tests/Umbraco.Tests.Common/Builders/LanguageBuilder.cs index 16283e0adf..0aa81a7381 100644 --- a/tests/Umbraco.Tests.Common/Builders/LanguageBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/LanguageBuilder.cs @@ -95,7 +95,7 @@ public class LanguageBuilder return this; } - [Obsolete("This will be replaced in V13 by a corresponding method accepting language ISO code instead of language ID.")] + [Obsolete("This will be replaced in V14 by a corresponding method accepting language ISO code instead of language ID.")] public LanguageBuilder WithFallbackLanguageId(int fallbackLanguageId) { _fallbackLanguageId = fallbackLanguageId;