diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js index a11aa8bff8..5f1c46de4c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function LanguagesEditController($scope, $timeout, $location, $routeParams, navigationService, notificationsService, localizationService, languageResource, contentEditingHelper, formHelper, eventsService) { + function LanguagesEditController($scope, $q, $timeout, $location, $routeParams, overlayService, navigationService, notificationsService, localizationService, languageResource, contentEditingHelper, formHelper, eventsService) { var vm = this; @@ -20,6 +20,8 @@ vm.toggleMandatory = toggleMandatory; vm.toggleDefault = toggleDefault; + var currCulture = null; + function init() { // localize labels @@ -32,7 +34,8 @@ "languages_addLanguage", "languages_noFallbackLanguageOption", "languages_fallbackLanguageDescription", - "languages_fallbackLanguage" + "languages_fallbackLanguage", + "defaultdialogs_confirmSure" ]; localizationService.localizeMany(labelKeys).then(function (values) { @@ -43,6 +46,7 @@ vm.labels.defaultLanguageHelp = values[4]; vm.labels.addLanguage = values[5]; vm.labels.noFallbackLanguageOption = values[6]; + vm.labels.areYouSure = values[9]; $scope.properties = { fallbackLanguage: { @@ -53,46 +57,56 @@ }; if ($routeParams.create) { - vm.page.name = vm.labels.addLanguage; - languageResource.getCultures().then(function (culturesDictionary) { - var cultures = []; - angular.forEach(culturesDictionary, function (value, key) { - cultures.push({ - name: key, - displayName: value - }); - }); - vm.availableCultures = cultures; - }); + vm.page.name = vm.labels.addLanguage; } }); vm.loading = true; - languageResource.getAll().then(function (languages) { + + var promises = []; + + //load all culture/languages + promises.push(languageResource.getCultures().then(function (culturesDictionary) { + var cultures = []; + angular.forEach(culturesDictionary, function (value, key) { + cultures.push({ + name: key, + displayName: value + }); + }); + vm.availableCultures = cultures; + })); + + //load all possible fallback languages + promises.push(languageResource.getAll().then(function (languages) { vm.availableLanguages = languages.filter(function (l) { return $routeParams.id != l.id; }); vm.loading = false; - }); + })); if (!$routeParams.create) { - vm.loading = true; - - languageResource.getById($routeParams.id).then(function(lang) { + promises.push(languageResource.getById($routeParams.id).then(function(lang) { vm.language = lang; vm.page.name = vm.language.name; - /* we need to store the initial default state so we can disabel the toggle if it is the default. + /* we need to store the initial default state so we can disable the toggle if it is the default. we need to prevent from not having a default language. */ vm.initIsDefault = angular.copy(vm.language.isDefault); - vm.loading = false; makeBreadcrumbs(); - }); + + //store to check if we are changing the lang culture + currCulture = vm.language.culture; + })); } + $q.all(promises, function () { + vm.loading = false; + }); + $timeout(function () { navigationService.syncTree({ tree: "languages", path: "-1" }); }); @@ -103,31 +117,54 @@ if (formHelper.submitForm({ scope: $scope })) { vm.page.saveButtonState = "busy"; - languageResource.save(vm.language).then(function (lang) { + //check if the culture is being changed + if (currCulture && vm.language.culture !== currCulture) { - formHelper.resetForm({ scope: $scope }); + const changeCultureAlert = { + title: vm.labels.areYouSure, + view: "views/languages/overlays/change.html", + submitButtonLabelKey: "general_continue", + submit: function (model) { + saveLanguage(); + overlayService.close(); + }, + close: function () { + overlayService.close(); + vm.page.saveButtonState = "init"; + } + }; - vm.language = lang; - vm.page.saveButtonState = "success"; - localizationService.localize("speechBubbles_languageSaved").then(function(value){ - notificationsService.success(value); - }); - - // emit event when language is created or updated/saved - var args = { language: lang, isNew: $routeParams.create ? true : false }; - eventsService.emit("editors.languages.languageSaved", args); - - back(); - - }, function (err) { - vm.page.saveButtonState = "error"; - - formHelper.handleError(err); - - }); + overlayService.open(changeCultureAlert); + } + else { + saveLanguage(); + } } + } + function saveLanguage() { + languageResource.save(vm.language).then(function (lang) { + formHelper.resetForm({ scope: $scope }); + + vm.language = lang; + vm.page.saveButtonState = "success"; + localizationService.localize("speechBubbles_languageSaved").then(function (value) { + notificationsService.success(value); + }); + + // emit event when language is created or updated/saved + var args = { language: lang, isNew: $routeParams.create ? true : false }; + eventsService.emit("editors.languages.languageSaved", args); + + back(); + + }, function (err) { + vm.page.saveButtonState = "error"; + + formHelper.handleError(err); + + }); } function back() { diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overlays/change.html b/src/Umbraco.Web.UI.Client/src/views/languages/overlays/change.html new file mode 100644 index 0000000000..c5e813fa51 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overlays/change.html @@ -0,0 +1,7 @@ +
+ +
+ Changing the culture for a language may be an expensive operation and will result in the content cache and indexes being rebuilt. +
+ +
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index d7d743e7e9..86283310fd 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -414,6 +414,7 @@ Click to add a Macro Insert table This will delete the language + Changing the culture for a language may be an expensive operation and will result in the content cache and indexes being rebuilt Last Edited Link Internal link: diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index eff344d4f3..5fa5ad6ea3 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -417,6 +417,7 @@ Click to add a Macro Insert table This will delete the language + Changing the culture for a language may be an expensive operation and will result in the content cache and indexes being rebuilt Last Edited Link Internal link: diff --git a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs index 65b29aabc6..b00a4818f6 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs @@ -266,13 +266,21 @@ namespace Umbraco.Web.Cache public static void RefreshLanguageCache(this DistributedCache dc, ILanguage language) { if (language == null) return; - dc.Refresh(LanguageCacheRefresher.UniqueId, language.Id); + + var payload = new LanguageCacheRefresher.JsonPayload(language.Id, language.IsoCode, + language.WasPropertyDirty(nameof(ILanguage.IsoCode)) + ? LanguageCacheRefresher.JsonPayload.LanguageChangeType.ChangeCulture + : LanguageCacheRefresher.JsonPayload.LanguageChangeType.Update); + + dc.RefreshByPayload(LanguageCacheRefresher.UniqueId, new[] { payload }); } public static void RemoveLanguageCache(this DistributedCache dc, ILanguage language) { if (language == null) return; - dc.Remove(LanguageCacheRefresher.UniqueId, language.Id); + + var payload = new LanguageCacheRefresher.JsonPayload(language.Id, language.IsoCode, LanguageCacheRefresher.JsonPayload.LanguageChangeType.Remove); + dc.RefreshByPayload(LanguageCacheRefresher.UniqueId, new[] { payload }); } #endregion diff --git a/src/Umbraco.Web/Editors/LanguageController.cs b/src/Umbraco.Web/Editors/LanguageController.cs index 650dcea6e9..cb7fde23db 100644 --- a/src/Umbraco.Web/Editors/LanguageController.cs +++ b/src/Umbraco.Web/Editors/LanguageController.cs @@ -99,24 +99,28 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); // this is prone to race conditions but the service will not let us proceed anyways - var existing = Services.LocalizationService.GetLanguageByIsoCode(language.IsoCode); + var existingByCulture = Services.LocalizationService.GetLanguageByIsoCode(language.IsoCode); // the localization service might return the generic language even when queried for specific ones (e.g. "da" when queried for "da-DK") // - we need to handle that explicitly - if (existing?.IsoCode != language.IsoCode) + if (existingByCulture?.IsoCode != language.IsoCode) { - existing = null; + existingByCulture = null; } - if (existing != null && language.Id != existing.Id) + if (existingByCulture != null && language.Id != existingByCulture.Id) { //someone is trying to create a language that already exist ModelState.AddModelError("IsoCode", "The language " + language.IsoCode + " already exists"); throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); } - if (existing == null) + var existingById = language.Id != default ? Services.LocalizationService.GetLanguageById(language.Id) : null; + + if (existingById == null) { + //Creating a new lang... + CultureInfo culture; try { @@ -141,38 +145,39 @@ namespace Umbraco.Web.Editors return Mapper.Map(newLang); } - existing.IsMandatory = language.IsMandatory; + existingById.IsMandatory = language.IsMandatory; // 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 - if (existing.IsDefault && !language.IsDefault) + if (existingById.IsDefault && !language.IsDefault) { ModelState.AddModelError("IsDefault", "Cannot un-default the default language."); throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); } - existing.IsDefault = language.IsDefault; - existing.FallbackLanguageId = language.FallbackLanguageId; + existingById.IsDefault = language.IsDefault; + 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 - if (existing.FallbackLanguageId.HasValue) + if (existingById.FallbackLanguageId.HasValue) { var languages = Services.LocalizationService.GetAllLanguages().ToDictionary(x => x.Id, x => x); - if (!languages.ContainsKey(existing.FallbackLanguageId.Value)) + if (!languages.ContainsKey(existingById.FallbackLanguageId.Value)) { ModelState.AddModelError("FallbackLanguage", "The selected fall back language does not exist."); throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); } - if (CreatesCycle(existing, languages)) + if (CreatesCycle(existingById, languages)) { - ModelState.AddModelError("FallbackLanguage", $"The selected fall back language {languages[existing.FallbackLanguageId.Value].IsoCode} would create a circular path."); + ModelState.AddModelError("FallbackLanguage", $"The selected fall back language {languages[existingById.FallbackLanguageId.Value].IsoCode} would create a circular path."); throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); } } - Services.LocalizationService.Save(existing); - return Mapper.Map(existing); + Services.LocalizationService.Save(existingById); + return Mapper.Map(existingById); } // see LocalizationService