From f00bfc408e903ca1a55d9444b339af16558a2c51 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 2 May 2022 15:42:19 +0200 Subject: [PATCH] v10: Make language name editable (#12243) * Update language models to get and set manual name * Save custom language name in controller * Rewrite AngularJS language edit view and controller * Cleanup language overview * Remove icon from language overview * Make styling of control group the same as properties * Ensure both ISO code and culture name are set in language model * Use new language model constructor * Update tests to use new language constructor * Update culture name in dictionary package export * Use language name in dictionary * Fix language nullability issues * Cleanup GetAllCultures and added null checks * Re-add obsolete constructors * Make language name required and update Cypress test * Fix routing/saveNewLanguages Cypress test * Make language name optional (improved backwards compatibility) Co-authored-by: Ronald Barendse --- .../Models/ContentEditing/Language.cs | 4 +- src/Umbraco.Core/Models/ILanguage.cs | 4 +- src/Umbraco.Core/Models/Language.cs | 85 +++------- .../Models/Mapping/DictionaryMapDefinition.cs | 4 +- .../Models/Mapping/LanguageMapDefinition.cs | 4 +- .../UmbracoBuilder.CoreServices.cs | 6 - .../UmbracoBuilder.Services.cs | 57 +++++-- .../Packaging/PackageDataInstallation.cs | 81 ++++++--- .../Persistence/Factories/LanguageFactory.cs | 22 ++- .../Implement/LanguageRepository.cs | 15 +- .../Controllers/LanguageController.cs | 83 ++++----- .../Templates/TemplateRenderer.cs | 2 +- .../components/html/umb-control-group.html | 2 +- .../src/views/languages/edit.controller.js | 159 +++++++----------- .../src/views/languages/edit.html | 102 ++++++----- .../views/languages/overview.controller.js | 52 +++--- .../src/views/languages/overview.html | 28 ++- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 1 + .../umbraco/config/lang/en_us.xml | 1 + .../cypress/integration/Content/routing.ts | 1 + .../Builders/LanguageBuilder.cs | 5 +- .../Testing/SqliteTestDatabase.cs | 3 +- .../Packaging/PackageDataInstallationTests.cs | 7 +- .../Repositories/DictionaryRepositoryTest.cs | 25 ++- .../Repositories/DocumentRepositoryTest.cs | 4 +- .../Repositories/DomainRepositoryTest.cs | 5 +- .../Repositories/LanguageRepositoryTest.cs | 64 ++++--- .../Repositories/MediaRepositoryTest.cs | 2 +- .../Repositories/TemplateRepositoryTest.cs | 2 +- .../Scoping/ScopedRepositoryTests.cs | 4 +- .../ContentServiceNotificationTests.cs | 16 +- .../ContentServicePublishBranchTests.cs | 11 +- .../ContentTypeServiceVariantsTests.cs | 19 ++- .../Services/EntityServiceTests.cs | 5 +- .../Services/EntityXmlSerializerTests.cs | 2 +- .../Services/Importing/Dictionary-Package.xml | 2 +- .../Routing/GetContentUrlsTests.cs | 3 +- .../PublishedContentLanguageVariantTests.cs | 52 +++++- 38 files changed, 471 insertions(+), 473 deletions(-) diff --git a/src/Umbraco.Core/Models/ContentEditing/Language.cs b/src/Umbraco.Core/Models/ContentEditing/Language.cs index bc9aa6f881..0a0ed03a2a 100644 --- a/src/Umbraco.Core/Models/ContentEditing/Language.cs +++ b/src/Umbraco.Core/Models/ContentEditing/Language.cs @@ -1,4 +1,3 @@ -using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; @@ -12,10 +11,9 @@ namespace Umbraco.Cms.Core.Models.ContentEditing [DataMember(Name = "culture", IsRequired = true)] [Required(AllowEmptyStrings = false)] - public string? IsoCode { get; set; } + public string IsoCode { get; set; } = null!; [DataMember(Name = "name")] - [ReadOnly(true)] public string? Name { get; set; } [DataMember(Name = "isDefault")] diff --git a/src/Umbraco.Core/Models/ILanguage.cs b/src/Umbraco.Core/Models/ILanguage.cs index fc20642464..de5170cff6 100644 --- a/src/Umbraco.Core/Models/ILanguage.cs +++ b/src/Umbraco.Core/Models/ILanguage.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; @@ -19,7 +19,7 @@ namespace Umbraco.Cms.Core.Models /// Gets or sets the culture name of the language. /// [DataMember] - string? CultureName { get; set; } + string CultureName { get; set; } /// /// Gets the object for the language. diff --git a/src/Umbraco.Core/Models/Language.cs b/src/Umbraco.Core/Models/Language.cs index 4d976e7f26..20d936af61 100644 --- a/src/Umbraco.Core/Models/Language.cs +++ b/src/Umbraco.Core/Models/Language.cs @@ -1,7 +1,5 @@ -using System; using System.Globalization; using System.Runtime.Serialization; -using System.Threading; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.Entities; @@ -14,18 +12,28 @@ namespace Umbraco.Cms.Core.Models [DataContract(IsReference = true)] public class Language : EntityBase, ILanguage { - private readonly GlobalSettings _globalSettings; - - private string _isoCode = null!; - private string? _cultureName; + private string _isoCode; + private string _cultureName; private bool _isDefaultVariantLanguage; private bool _mandatory; private int? _fallbackLanguageId; + /// + /// Initializes a new instance of the class. + /// + /// The ISO code of the language. + /// The name of the language. + public Language(string isoCode, string cultureName) + { + _isoCode = isoCode ?? throw new ArgumentNullException(nameof(isoCode)); + _cultureName = cultureName ?? throw new ArgumentNullException(nameof(cultureName)); + } + + [Obsolete("Use the constructor not requiring global settings and accepting an explicit name instead, scheduled for removal in V11.")] public Language(GlobalSettings globalSettings, string isoCode) { - IsoCode = isoCode; - _globalSettings = globalSettings; + _isoCode = isoCode ?? throw new ArgumentNullException(nameof(isoCode)); + _cultureName = CultureInfo.GetCultureInfo(isoCode).EnglishName; } /// @@ -33,64 +41,25 @@ namespace Umbraco.Cms.Core.Models public string IsoCode { get => _isoCode; - set => SetPropertyValueAndDetectChanges(value, ref _isoCode!, nameof(IsoCode)); + set + { + ArgumentNullException.ThrowIfNull(value); + + SetPropertyValueAndDetectChanges(value, ref _isoCode!, nameof(IsoCode)); + } } /// [DataMember] - public string? CultureName + public string CultureName { - // CultureInfo.DisplayName is the name in the installed .NET language - // .NativeName is the name in culture info's language - // .EnglishName is the name in English - // - // there is no easy way to get the name in a specified culture (which would need to be installed on the server) - // this works: - // var rm = new ResourceManager("mscorlib", typeof(int).Assembly); - // var name = rm.GetString("Globalization.ci_" + culture.Name, displayCulture); - // but can we rely on it? - // - // and... DisplayName is captured and cached in culture infos returned by GetCultureInfo(), using - // the value for the current thread culture at the moment it is first retrieved - whereas creating - // a new CultureInfo() creates a new instance, which _then_ can get DisplayName again in a different - // culture - // - // I assume that, on a site, all language names should be in the SAME language, in DB, - // and that would be the Umbraco.Core.DefaultUILanguage (app setting) - BUT if by accident - // ANY culture has been retrieved with another current thread culture - it's now corrupt - // - // so, the logic below ensures that the name always end up being the correct name - // see also LanguageController.GetAllCultures which is doing the same - // - // all this, including the ugly settings injection, because se store language names in db, - // otherwise it would be ok to simply return new CultureInfo(IsoCode).DisplayName to get the name - // in whatever culture is current - we should not do it, see task #3623 - // - // but then, some tests that compare audit strings (for culture names) would need to be fixed - - get + get => _cultureName; + set { - if (_cultureName != null) return _cultureName; + ArgumentNullException.ThrowIfNull(value); - // capture - var threadUiCulture = Thread.CurrentThread.CurrentUICulture; - - try - { - var defaultUiCulture = CultureInfo.GetCultureInfo(_globalSettings.DefaultUILanguage); - Thread.CurrentThread.CurrentUICulture = defaultUiCulture; - - // get name - new-ing an instance to get proper display name - return new CultureInfo(IsoCode).DisplayName; - } - finally - { - // restore - Thread.CurrentThread.CurrentUICulture = threadUiCulture; - } + SetPropertyValueAndDetectChanges(value, ref _cultureName!, nameof(CultureName)); } - - set => SetPropertyValueAndDetectChanges(value, ref _cultureName, nameof(CultureName)); } /// diff --git a/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs index 6d46ffc673..a5db1d4b96 100644 --- a/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs @@ -84,7 +84,7 @@ namespace Umbraco.Cms.Core.Models.Mapping target.Translations.Add(new DictionaryTranslationDisplay { IsoCode = lang.IsoCode, - DisplayName = lang.CultureInfo?.DisplayName, + DisplayName = lang.CultureName, Translation = translation?.Value ?? string.Empty, LanguageId = lang.Id }); @@ -106,7 +106,7 @@ namespace Umbraco.Cms.Core.Models.Mapping target.Translations.Add( new DictionaryOverviewTranslationDisplay { - DisplayName = lang.CultureInfo?.DisplayName, + DisplayName = lang.CultureName, HasTranslation = translation != null && string.IsNullOrEmpty(translation.Value) == false }); } diff --git a/src/Umbraco.Core/Models/Mapping/LanguageMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/LanguageMapDefinition.cs index 8234244e38..7450ec62b4 100644 --- a/src/Umbraco.Core/Models/Mapping/LanguageMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/LanguageMapDefinition.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Core.Mapping; @@ -31,7 +31,7 @@ namespace Umbraco.Cms.Core.Models.Mapping { target.Id = source.Id; target.IsoCode = source.IsoCode; - target.Name = source.CultureInfo?.DisplayName; + target.Name = source.CultureName; target.IsDefault = source.IsDefault; target.IsMandatory = source.IsMandatory; target.FallbackLanguageId = source.FallbackLanguageId; diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 546b216aab..05eda0e94d 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -1,4 +1,3 @@ -using System.Runtime.InteropServices; using Examine; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -47,7 +46,6 @@ using Umbraco.Cms.Infrastructure.Media; using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Migrations.PostMigrations; -using Umbraco.Cms.Infrastructure.Packaging; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; @@ -55,7 +53,6 @@ using Umbraco.Cms.Infrastructure.Runtime; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Infrastructure.Search; using Umbraco.Cms.Infrastructure.Serialization; -using Umbraco.Cms.Infrastructure.Services; using Umbraco.Cms.Infrastructure.Services.Implement; using Umbraco.Extensions; @@ -104,7 +101,6 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddSingleton(f => f.GetRequiredService()); builder.Services.AddSingleton(f => f.GetRequiredService()); - builder.Services.AddScoped(); builder.Services.AddSingleton(); @@ -202,8 +198,6 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddSingleton(SixLabors.ImageSharp.Configuration.Default); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddTransient(); builder.AddInstaller(); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index 7fa36bc910..eff921077c 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -1,21 +1,23 @@ -using System; -using System.IO; -using System.Linq; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.Extensions; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Implement; +using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Packaging; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Infrastructure.Services; using Umbraco.Cms.Infrastructure.Services.Implement; using Umbraco.Cms.Infrastructure.Telemetry.Providers; @@ -45,10 +47,10 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); - builder.Services.AddTransient(LocalizedTextServiceFileSourcesFactory); + builder.Services.AddTransient(CreateLocalizedTextServiceFileSourcesFactory); builder.Services.AddUnique(factory => CreatePackageRepository(factory, "createdPackages.config")); builder.Services.AddUnique(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(CreatePackageDataInstallation); builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddTransient(); @@ -56,10 +58,21 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddTransient(); builder.Services.AddTransient(); - return builder; } + private static LocalizedTextServiceFileSources CreateLocalizedTextServiceFileSourcesFactory(IServiceProvider container) + { + var hostEnvironment = container.GetRequiredService(); + var mainLangFolder = new DirectoryInfo(hostEnvironment.MapPathContentRoot(WebPath.Combine(Constants.SystemDirectories.Umbraco, "config", "lang"))); + + return new LocalizedTextServiceFileSources( + container.GetRequiredService>(), + container.GetRequiredService(), + mainLangFolder, + container.GetServices()); + } + private static PackagesRepository CreatePackageRepository(IServiceProvider factory, string packageRepoFileName) => new PackagesRepository( factory.GetRequiredService(), @@ -68,7 +81,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection factory.GetRequiredService(), factory.GetRequiredService(), factory.GetRequiredService(), - factory.GetRequiredService(), + factory.GetRequiredService(), factory.GetRequiredService(), factory.GetRequiredService>(), factory.GetRequiredService(), @@ -77,16 +90,24 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection factory.GetRequiredService(), packageRepoFileName); - private static LocalizedTextServiceFileSources LocalizedTextServiceFileSourcesFactory(IServiceProvider container) - { - var hostingEnvironment = container.GetRequiredService(); - var mainLangFolder = new DirectoryInfo(hostingEnvironment.MapPathContentRoot(WebPath.Combine(Constants.SystemDirectories.Umbraco, "config", "lang"))); + // Factory registration is only required because of ambiguous constructor + private static PackageDataInstallation CreatePackageDataInstallation(IServiceProvider factory) + => new PackageDataInstallation( + factory.GetRequiredService(), + factory.GetRequiredService>(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService()); - return new LocalizedTextServiceFileSources( - container.GetRequiredService>(), - container.GetRequiredService(), - mainLangFolder, - container.GetServices()); - } } } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs index 1a72fa19b5..69271baf2c 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Net; using System.Xml.Linq; using System.Xml.XPath; @@ -16,10 +13,10 @@ using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Packaging; using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Packaging @@ -35,11 +32,9 @@ namespace Umbraco.Cms.Infrastructure.Packaging private readonly PropertyEditorCollection _propertyEditors; private readonly IScopeProvider _scopeProvider; private readonly IShortStringHelper _shortStringHelper; - private readonly GlobalSettings _globalSettings; private readonly IConfigurationEditorJsonSerializer _serializer; private readonly IMediaService _mediaService; private readonly IMediaTypeService _mediaTypeService; - private readonly IHostingEnvironment _hostingEnvironment; private readonly IEntityService _entityService; private readonly IContentTypeService _contentTypeService; private readonly IContentService _contentService; @@ -57,11 +52,9 @@ namespace Umbraco.Cms.Infrastructure.Packaging PropertyEditorCollection propertyEditors, IScopeProvider scopeProvider, IShortStringHelper shortStringHelper, - IOptions globalSettings, IConfigurationEditorJsonSerializer serializer, IMediaService mediaService, - IMediaTypeService mediaTypeService, - IHostingEnvironment hostingEnvironment) + IMediaTypeService mediaTypeService) { _dataValueEditorFactory = dataValueEditorFactory; _logger = logger; @@ -69,22 +62,58 @@ namespace Umbraco.Cms.Infrastructure.Packaging _macroService = macroService; _localizationService = localizationService; _dataTypeService = dataTypeService; - _propertyEditors = propertyEditors; - _scopeProvider = scopeProvider; - _shortStringHelper = shortStringHelper; - _globalSettings = globalSettings.Value; - _serializer = serializer; - _mediaService = mediaService; - _mediaTypeService = mediaTypeService; - _hostingEnvironment = hostingEnvironment; _entityService = entityService; _contentTypeService = contentTypeService; _contentService = contentService; + _propertyEditors = propertyEditors; + _scopeProvider = scopeProvider; + _shortStringHelper = shortStringHelper; + _serializer = serializer; + _mediaService = mediaService; + _mediaTypeService = mediaTypeService; } + // Also remove factory service registration when this constructor is removed + [Obsolete("Use the constructor with Infrastructure.IScopeProvider and without global settings and hosting environment instead.")] + public PackageDataInstallation( + IDataValueEditorFactory dataValueEditorFactory, + ILogger logger, + IFileService fileService, + IMacroService macroService, + ILocalizationService localizationService, + IDataTypeService dataTypeService, + IEntityService entityService, + IContentTypeService contentTypeService, + IContentService contentService, + PropertyEditorCollection propertyEditors, + Core.Scoping.IScopeProvider scopeProvider, + IShortStringHelper shortStringHelper, + IOptions globalSettings, + IConfigurationEditorJsonSerializer serializer, + IMediaService mediaService, + IMediaTypeService mediaTypeService, + IHostingEnvironment hostingEnvironment) + : this( + dataValueEditorFactory, + logger, + fileService, + macroService, + localizationService, + dataTypeService, + entityService, + contentTypeService, + contentService, + propertyEditors, + scopeProvider, + shortStringHelper, + serializer, + mediaService, + mediaTypeService) + { } + #region Install/Uninstall - public InstallationSummary InstallPackageData(CompiledPackage compiledPackage, int userId) + public InstallationSummary InstallPackageData(CompiledPackage compiledPackage, int userId) { using (var scope = _scopeProvider.CreateScope()) { @@ -1435,14 +1464,22 @@ namespace Umbraco.Cms.Infrastructure.Packaging foreach (var languageElement in languageElements) { var isoCode = languageElement.AttributeValue("CultureAlias"); + if (string.IsNullOrEmpty(isoCode)) + { + continue; + } + var existingLanguage = _localizationService.GetLanguageByIsoCode(isoCode); if (existingLanguage != null) - continue; - var langauge = new Language(_globalSettings, isoCode!) { - CultureName = languageElement.AttributeValue("FriendlyName")! - }; + continue; + } + + var cultureName = languageElement.AttributeValue("FriendlyName") ?? isoCode; + + var langauge = new Language(isoCode, cultureName); _localizationService.Save(langauge, userId); + list.Add(langauge); } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs index 2de56aa44d..2c7c6c081e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs @@ -1,5 +1,3 @@ -using System.Globalization; -using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; @@ -7,28 +5,36 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories { internal static class LanguageFactory { - public static ILanguage BuildEntity(GlobalSettings globalSettings, LanguageDto dto) + public static ILanguage BuildEntity(LanguageDto dto) { - var lang = new Language(globalSettings, dto.IsoCode!) + ArgumentNullException.ThrowIfNull(dto); + if (dto.IsoCode == null || dto.CultureName == null) + { + throw new InvalidOperationException("Language ISO code and/or culture name can't be null."); + } + + var lang = new Language(dto.IsoCode, dto.CultureName) { - CultureName = dto.CultureName, Id = dto.Id, IsDefault = dto.IsDefault, IsMandatory = dto.IsMandatory, FallbackLanguageId = dto.FallbackLanguageId }; - // reset dirty initial properties (U4-1946) + // Reset dirty initial properties lang.ResetDirtyProperties(false); + return lang; } public static LanguageDto BuildDto(ILanguage entity) { + ArgumentNullException.ThrowIfNull(entity); + var dto = new LanguageDto { - CultureName = entity.CultureName, IsoCode = entity.IsoCode, + CultureName = entity.CultureName, IsDefault = entity.IsDefault, IsMandatory = entity.IsMandatory, FallbackLanguageId = entity.FallbackLanguageId @@ -36,7 +42,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories if (entity.HasIdentity) { - dto.Id = short.Parse(entity.Id.ToString(CultureInfo.InvariantCulture)); + dto.Id = (short)entity.Id; } return dto; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs index 92806b0043..bf1bc4f4b4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs @@ -23,15 +23,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement /// internal class LanguageRepository : EntityRepositoryBase, ILanguageRepository { - private readonly GlobalSettings _globalSettings; private readonly Dictionary _codeIdMap = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _idCodeMap = new Dictionary(); - public LanguageRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IOptions globalSettings) + public LanguageRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) : base(scopeAccessor, cache, logger) - { - _globalSettings = globalSettings.Value; - } + { } protected override IRepositoryCachePolicy CreateCachePolicy() { @@ -238,17 +235,15 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement #endregion protected ILanguage ConvertFromDto(LanguageDto dto) - { - var entity = LanguageFactory.BuildEntity(_globalSettings, dto); - return entity; - } + => LanguageFactory.BuildEntity(dto); public ILanguage? GetByIsoCode(string isoCode) { // ensure cache is populated, in a non-expensive way if (TypedCachePolicy != null) + { TypedCachePolicy.GetAllCached(PerformGetAll); - + } var id = GetIdByIsoCode(isoCode, throwOnNotFound: false); return id.HasValue ? Get(id.Value) : null; diff --git a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs index cef800fcb6..1ed6efe9c6 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Mapping; @@ -25,36 +23,26 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { private readonly ILocalizationService _localizationService; private readonly IUmbracoMapper _umbracoMapper; - private readonly GlobalSettings _globalSettings; - public LanguageController(ILocalizationService localizationService, - IUmbracoMapper umbracoMapper, - IOptionsSnapshot globalSettings) + [ActivatorUtilitiesConstructor] + public LanguageController(ILocalizationService localizationService, IUmbracoMapper umbracoMapper) { _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); - _globalSettings = globalSettings.Value ?? throw new ArgumentNullException(nameof(globalSettings)); } + [Obsolete("Use the constructor without global settings instead, scheduled for removal in V11.")] + public LanguageController(ILocalizationService localizationService, IUmbracoMapper umbracoMapper, IOptionsSnapshot globalSettings) + : this(localizationService, umbracoMapper) + { } + /// /// Returns all cultures available for creating languages. /// /// [HttpGet] public IDictionary GetAllCultures() - { - // get cultures - new-ing instances to get proper display name, - // in the current culture, and not the cached one - // (see notes in Language class about culture info names) - // TODO: Fix this requirement, see https://github.com/umbraco/Umbraco-CMS/issues/3623 - return CultureInfo.GetCultures(CultureTypes.AllCultures) - .Select(x=>x.Name) - .Distinct() - .Where(x => !x.IsNullOrWhiteSpace()) - .Select(x => new CultureInfo(x)) // important! - .OrderBy(x => x.EnglishName) - .ToDictionary(x => x.Name, x => x.EnglishName); - } + => CultureInfo.GetCultures(CultureTypes.AllCultures).DistinctBy(x => x.Name).OrderBy(x => x.EnglishName).ToDictionary(x => x.Name, x => x.EnglishName); /// /// Returns all currently configured languages. @@ -73,7 +61,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { var lang = _localizationService.GetLanguageById(id); if (lang == null) + { return NotFound(); + } return _umbracoMapper.Map(lang); } @@ -101,7 +91,6 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // service is happy deleting a language that's fallback for another language, // will just remove it - so no need to check here - _localizationService.Delete(language); return Ok(); @@ -115,7 +104,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public ActionResult SaveLanguage(Language language) { if (!ModelState.IsValid) + { return ValidationProblem(ModelState); + } // this is prone to race conditions but the service will not let us proceed anyways var existingByCulture = _localizationService.GetLanguageByIsoCode(language.IsoCode); @@ -129,17 +120,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (existingByCulture != null && language.Id != existingByCulture.Id) { - //someone is trying to create a language that already exist + // Someone is trying to create a language that already exist ModelState.AddModelError("IsoCode", "The language " + language.IsoCode + " already exists"); return ValidationProblem(ModelState); } var existingById = language.Id != default ? _localizationService.GetLanguageById(language.Id) : null; - if (existingById == null) { - //Creating a new lang... - + // Creating a new lang... CultureInfo culture; try { @@ -152,9 +141,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } // create it (creating a new language cannot create a fallback cycle) - var newLang = new Cms.Core.Models.Language(_globalSettings, culture.Name) + var newLang = new Core.Models.Language(culture.Name, language.Name ?? culture.EnglishName) { - CultureName = culture.DisplayName, IsDefault = language.IsDefault, IsMandatory = language.IsMandatory, FallbackLanguageId = language.FallbackLanguageId @@ -164,7 +152,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return _umbracoMapper.Map(newLang); } - existingById.IsMandatory = language.IsMandatory; + existingById.IsoCode = language.IsoCode; + if (!string.IsNullOrEmpty(language.Name)) + { + existingById.CultureName = language.Name; + } // note that the service will prevent the default language from being "un-defaulted" // but does not hurt to test here - though the UI should prevent it too @@ -174,22 +166,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return ValidationProblem(ModelState); } - // Update language - CultureInfo cultureAfterChange; - try - { - // language has the CultureName of the previous lang so we get information about new culture. - cultureAfterChange = CultureInfo.GetCultureInfo(language.IsoCode!); - } - catch (CultureNotFoundException) - { - ModelState.AddModelError("IsoCode", "No Culture found with name " + language.IsoCode); - return ValidationProblem(ModelState); - } - existingById.CultureName = cultureAfterChange.DisplayName; existingById.IsDefault = language.IsDefault; + existingById.IsMandatory = language.IsMandatory; existingById.FallbackLanguageId = language.FallbackLanguageId; - existingById.IsoCode = language.IsoCode!; // modifying an existing language can create a fallback, verify // note that the service will check again, dealing with race conditions @@ -201,6 +180,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers ModelState.AddModelError("FallbackLanguage", "The selected fall back language does not exist."); return ValidationProblem(ModelState); } + if (CreatesCycle(existingById, languages)) { ModelState.AddModelError("FallbackLanguage", $"The selected fall back language {languages[existingById.FallbackLanguageId.Value].IsoCode} would create a circular path."); @@ -216,13 +196,24 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private bool CreatesCycle(ILanguage language, IDictionary languages) { // a new language is not referenced yet, so cannot be part of a cycle - if (!language.HasIdentity) return false; + if (!language.HasIdentity) + { + return false; + } var id = language.FallbackLanguageId; while (true) // assuming languages does not already contains a cycle, this must end { - if (!id.HasValue) return false; // no fallback means no cycle - if (id.Value == language.Id) return true; // back to language = cycle! + if (!id.HasValue) + { + return false; // no fallback means no cycle + } + + if (id.Value == language.Id) + { + return true; // back to language = cycle! + } + id = languages[id.Value].FallbackLanguageId; // else keep chaining } } diff --git a/src/Umbraco.Web.Common/Templates/TemplateRenderer.cs b/src/Umbraco.Web.Common/Templates/TemplateRenderer.cs index 982ec53ed9..55a70e4f81 100644 --- a/src/Umbraco.Web.Common/Templates/TemplateRenderer.cs +++ b/src/Umbraco.Web.Common/Templates/TemplateRenderer.cs @@ -93,7 +93,7 @@ namespace Umbraco.Cms.Web.Common.Templates requestBuilder.SetCulture(defaultLanguage == null ? CultureInfo.CurrentUICulture.Name - : defaultLanguage.CultureInfo?.Name); + : defaultLanguage.IsoCode); } else { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/html/umb-control-group.html b/src/Umbraco.Web.UI.Client/src/views/components/html/umb-control-group.html index ae9edd4143..1d635d2bc5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/html/umb-control-group.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/html/umb-control-group.html @@ -1,5 +1,5 @@
-
+