From a7f9628b48011bfe38696ec5802b9afa68e0a2d5 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 30 Apr 2018 16:06:53 +0200 Subject: [PATCH 01/15] Updated ContentItemDisplay model to contain IsEdited property --- src/Umbraco.Web/Models/ContentEditing/ContentVariation.cs | 5 ++++- .../Models/Mapping/ContentItemDisplayVariationResolver.cs | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentVariation.cs b/src/Umbraco.Web/Models/ContentEditing/ContentVariation.cs index 83fb3b03f3..3546b95e34 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentVariation.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentVariation.cs @@ -23,7 +23,7 @@ namespace Umbraco.Web.Models.ContentEditing /// [DataMember(Name = "name")] public string Name { get; set; } - + [DataMember(Name = "state")] public string PublishedState { get; set; } @@ -33,6 +33,9 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "exists")] public bool Exists { get; set; } + [DataMember(Name = "isEdited")] + public bool IsEdited { get; set; } + /// /// Determines if this is the variant currently being edited /// diff --git a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs index 07b64ac309..26b4332bca 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs @@ -36,6 +36,7 @@ namespace Umbraco.Web.Models.Mapping Name = source.GetName(x.IsoCode), Exists = source.IsCultureAvailable(x.IsoCode), // segments ?? PublishedState = source.PublishedState.ToString(), + IsEdited = source.IsCultureEdited(x.IsoCode) //Segment = ?? We'll need to populate this one day when we support segments }).ToList(); From 92d88d5a4c4fe29a9db9275b54fe84922e46efc9 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 30 Apr 2018 16:18:35 +0200 Subject: [PATCH 02/15] Publish dialog should show only variants in Draft state --- .../overlays/publish/publish.controller.js | 49 +++++++++++++------ .../common/overlays/publish/publish.html | 2 +- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.controller.js index a489e9927d..c0e457e0b9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.controller.js @@ -1,23 +1,27 @@ (function () { "use strict"; - function PublishController($scope, $timeout) { + function PublishController($scope, eventsService) { var vm = this; - vm.variants = $scope.model.variants; + var variants = $scope.model.variants; vm.changeSelection = changeSelection; + vm.loading = true; + + vm.dirtyVariants = []; + vm.pristineVariants = []; //watch this model, if it's reset, then re init - $scope.$watch(function() { - return $scope.model.variants; - }, - function(newVal, oldVal) { + $scope.$watch(function () { + return $scope.model.variants; + }, + function (newVal, oldVal) { vm.variants = newVal; if (oldVal && oldVal.length) { //re-bind the selections for (var i = 0; i < oldVal.length; i++) { - var found = _.find(vm.variants, function(v) { - return v.language.id == oldVal[i].language.id; + var found = _.find(variants, function (v) { + return v.language.id === oldVal[i].language.id; }); if (found) { found.publish = oldVal[i].publish; @@ -28,24 +32,39 @@ }); function changeSelection(variant) { - var firstSelected = _.find(vm.variants, function(v) { + var firstSelected = _.find(variants, function (v) { return v.publish; }); $scope.model.disableSubmitButton = !firstSelected; //disable submit button if there is none selected } function onInit() { - _.each(vm.variants, - function (v) { - v.compositeId = v.language.id + "_" + (v.segment ? v.segment : ""); - v.htmlId = "publish_variant_" + v.compositeId; + _.each(variants, + function (variant) { + variant.compositeId = variant.language.id + "_" + (variant.segment ? variant.segment : ""); + variant.htmlId = "publish_variant_" + variant.compositeId; + + //append Draft state to variant + if (variant.isEdited === true && !variant.state.includes("Draft")) { + variant.state += ", Draft"; + vm.dirtyVariants.push(variant); + } else if (variant.isEdited === true) { + vm.dirtyVariants.push(variant); + } else { + vm.pristineVariants.push(variant); + } }); + + vm.loading = false; + + console.log("Dirty Variants", vm.dirtyVariants); + //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function(v) { + vm.dirtyVariants = _.sortBy(vm.dirtyVariants, function (v) { return v.current ? 0 : 1; }); //ensure that the current one is selected - vm.variants[0].publish = true; + vm.dirtyVariants[0].publish = true; } } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.html index ed7f32fc25..f0d721ec04 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.html @@ -9,7 +9,7 @@
-
+
diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index b4483fc09a..efdc6c23ab 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -253,11 +253,11 @@ namespace Umbraco.Web.Editors /// Gets the content json for the content id /// /// - /// + /// /// [OutgoingEditorModelEvent] [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetById(int id, int? languageId = null) + public ContentItemDisplay GetById(int id, string culture = null) { var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); if (foundContent == null) @@ -266,7 +266,7 @@ namespace Umbraco.Web.Editors return null;//irrelevant since the above throws } - var content = MapToDisplay(foundContent, GetLanguageCulture(languageId)); + var content = MapToDisplay(foundContent, culture); return content; } @@ -573,12 +573,12 @@ namespace Umbraco.Web.Editors public ContentItemDisplay PostSave([ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) { var contentItemDisplay = PostSaveInternal(contentItem, content => Services.ContentService.Save(contentItem.PersistedContent, Security.CurrentUser.Id)); - //ensure the active language is still selected - if (contentItem.LanguageId.HasValue) + //ensure the active culture is still selected + if (!contentItem.Culture.IsNullOrWhiteSpace()) { foreach (var contentVariation in contentItemDisplay.Variants) { - contentVariation.IsCurrent = contentVariation.Language.Id == contentItem.LanguageId; + contentVariation.IsCurrent = contentVariation.Language.IsoCode.InvariantEquals(contentItem.Culture); } } return contentItemDisplay; @@ -606,7 +606,7 @@ namespace Umbraco.Web.Editors { //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! // add the modelstate to the outgoing object and throw a validation message - var forDisplay = MapToDisplay(contentItem.PersistedContent, GetLanguageCulture(contentItem.LanguageId)); + var forDisplay = MapToDisplay(contentItem.PersistedContent, contentItem.Culture); forDisplay.Errors = ModelState.ToErrorDictionary(); throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); @@ -643,30 +643,30 @@ namespace Umbraco.Web.Editors else { //publish the item and check if it worked, if not we will show a diff msg below - contentItem.PersistedContent.TryPublishValues(GetLanguageCulture(contentItem.LanguageId)); //we are not checking for a return value here because we've already pre-validated the property values + contentItem.PersistedContent.TryPublishValues(contentItem.Culture); //we are not checking for a return value here because we've already pre-validated the property values //check if we are publishing other variants and validate them - var allLangs = Services.LocalizationService.GetAllLanguages().ToDictionary(x => x.Id, x => x); - var variantsToValidate = contentItem.PublishVariations.Where(x => x.LanguageId != contentItem.LanguageId).ToList(); + var allLangs = Services.LocalizationService.GetAllLanguages().ToDictionary(x => x.IsoCode, x => x, StringComparer.InvariantCultureIgnoreCase); + var variantsToValidate = contentItem.PublishVariations.Where(x => !x.Culture.InvariantEquals(contentItem.Culture)).ToList(); foreach (var publishVariation in variantsToValidate) { - if (!contentItem.PersistedContent.TryPublishValues(GetLanguageCulture(publishVariation.LanguageId))) + if (!contentItem.PersistedContent.TryPublishValues(publishVariation.Culture)) { - var errMsg = Services.TextService.Localize("speechBubbles/contentLangValidationError", new[] {allLangs[publishVariation.LanguageId].CultureName}); - ModelState.AddModelError("publish_variant_" + publishVariation.LanguageId + "_", errMsg); + var errMsg = Services.TextService.Localize("speechBubbles/contentLangValidationError", new[] {allLangs[publishVariation.Culture].CultureName}); + ModelState.AddModelError("publish_variant_" + publishVariation.Culture + "_", errMsg); } } //validate any mandatory variants that are not in the list var mandatoryLangs = Mapper.Map, IEnumerable>(allLangs.Values) - .Where(x => variantsToValidate.All(v => v.LanguageId != x.Id)) //don't include variants above - .Where(x => x.Id != contentItem.LanguageId) //don't include the current variant + .Where(x => variantsToValidate.All(v => !v.Culture.InvariantEquals(x.IsoCode))) //don't include variants above + .Where(x => !x.IsoCode.InvariantEquals(contentItem.Culture)) //don't include the current variant .Where(x => x.Mandatory); foreach (var lang in mandatoryLangs) { - if (contentItem.PersistedContent.Validate(GetLanguageCulture(lang.Id)).Length > 0) + if (contentItem.PersistedContent.Validate(lang.IsoCode).Length > 0) { - var errMsg = Services.TextService.Localize("speechBubbles/contentReqLangValidationError", new[]{allLangs[lang.Id].CultureName}); + var errMsg = Services.TextService.Localize("speechBubbles/contentReqLangValidationError", new[]{allLangs[lang.IsoCode].CultureName}); ModelState.AddModelError("publish_variant_" + lang.Id + "_", errMsg); } } @@ -676,7 +676,7 @@ namespace Umbraco.Web.Editors } //get the updated model - var display = MapToDisplay(contentItem.PersistedContent, GetLanguageCulture(contentItem.LanguageId)); + var display = MapToDisplay(contentItem.PersistedContent, contentItem.Culture); //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 HandleInvalidModelState(display); @@ -954,10 +954,9 @@ namespace Umbraco.Web.Editors if (contentItem.Name.IsNullOrWhiteSpace() == false) { //set the name according to the culture settings - if (contentItem.LanguageId.HasValue && contentItem.PersistedContent.ContentType.Variations.HasFlag(ContentVariation.CultureNeutral)) + if (!contentItem.Culture.IsNullOrWhiteSpace() && contentItem.PersistedContent.ContentType.Variations.HasFlag(ContentVariation.CultureNeutral)) { - var culture = Services.LocalizationService.GetLanguageById(contentItem.LanguageId.Value).IsoCode; - contentItem.PersistedContent.SetName(culture, contentItem.Name); + contentItem.PersistedContent.SetName(contentItem.Culture, contentItem.Name); } else { @@ -990,8 +989,8 @@ namespace Umbraco.Web.Editors base.MapPropertyValues( contentItem, - (save, property) => property.GetValue(GetLanguageCulture(save.LanguageId)), //get prop val - (save, property, v) => property.SetValue(v, GetLanguageCulture(save.LanguageId))); //set prop val + (save, property) => property.GetValue(save.Culture), //get prop val + (save, property, v) => property.SetValue(v, save.Culture)); //set prop val } /// @@ -1199,11 +1198,6 @@ namespace Umbraco.Web.Editors return display; } - - private string GetLanguageCulture(int? languageId) - { - if (languageId == null) return null; - return Core.Composing.Current.Services.LocalizationService.GetLanguageById(languageId.Value).IsoCode; // fixme optimize! - } + } } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs index ca410100ec..01d1a50460 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs @@ -15,8 +15,8 @@ namespace Umbraco.Web.Models.ContentEditing /// /// The language Id for the content variation being saved /// - [DataMember(Name = "languageId")] - public int? LanguageId { get; set; } //TODO: Change this to ContentVariationPublish, but this will all change anyways when we can edit all variants at once + [DataMember(Name = "culture")] + public string Culture { get; set; } //TODO: Change this to ContentVariationPublish, but this will all change anyways when we can edit all variants at once /// /// The template alias to save diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentVariationPublish.cs b/src/Umbraco.Web/Models/ContentEditing/ContentVariationPublish.cs index aefc487e7c..71c4672ccb 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentVariationPublish.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentVariationPublish.cs @@ -8,9 +8,9 @@ namespace Umbraco.Web.Models.ContentEditing /// public class ContentVariationPublish { - [DataMember(Name = "languageId", IsRequired = true)] + [DataMember(Name = "culture", IsRequired = true)] [Required] - public int LanguageId { get; set; } + public string Culture { get; set; } [DataMember(Name = "segment")] public string Segment { get; set; } diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index 53528484d1..ad190fe8ed 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -47,7 +47,7 @@ namespace Umbraco.Web.Trees /// protected override TreeNode GetSingleTreeNode(IEntitySlim entity, string parentId, FormDataCollection queryStrings) { - var langId = queryStrings["languageId"].TryConvertTo(); + var langId = queryStrings["culture"].TryConvertTo(); var allowedUserOptions = GetAllowedUserMenuItemsForNode(entity); if (CanUserAccessNode(entity, allowedUserOptions, langId.Success ? langId.Result : null)) diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index 4e7b78efe9..0c0308a471 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -149,8 +149,8 @@ namespace Umbraco.Web.Trees // get child entities - if id is root, but user's start nodes do not contain the // root node, this returns the start nodes instead of root's children - var langId = queryStrings["languageId"].TryConvertTo(); - var entities = GetChildEntities(id, langId.Success ? langId.Result : null).ToList(); + var culture = queryStrings["culture"].TryConvertTo(); + var entities = GetChildEntities(id, culture.Success ? culture.Result : null).ToList(); nodes.AddRange(entities.Select(x => GetSingleTreeNodeWithAccessCheck(x, id, queryStrings)).Where(x => x != null)); // if the user does not have access to the root node, what we have is the start nodes, @@ -182,7 +182,7 @@ namespace Umbraco.Web.Trees protected abstract UmbracoObjectTypes UmbracoObjectType { get; } - protected IEnumerable GetChildEntities(string id, int? langId) + protected IEnumerable GetChildEntities(string id, string culture) { // try to parse id as an integer else use GetEntityFromId // which will grok Guids, Udis, etc and let use obtain the id @@ -211,7 +211,7 @@ namespace Umbraco.Web.Trees } //This should really never be null, but we'll error check anyways - var currLangId = langId ?? Services.LocalizationService.GetDefaultLanguageId(); + culture = culture ?? Services.LocalizationService.GetDefaultLanguageIsoCode(); //Try to see if there is a variant name for the current language for the item and set the name accordingly. //If any of this fails, the tree node name will remain the default invariant culture name. @@ -219,14 +219,14 @@ namespace Umbraco.Web.Trees //fixme - what if there is no name found at all ? This could occur if the doc type is variant and the user fills in all language values, then creates a new lang and sets it as the default //fixme - what if the user changes this document type to not allow culture variants after it's already been created with culture variants, this means we'll be displaying the culture variant name when in fact we should be displaying the invariant name... but that would be null - if (currLangId.HasValue) + if (!culture.IsNullOrWhiteSpace()) { foreach (var e in result) { if (e.AdditionalData.TryGetValue("CultureNames", out var cultureNames) - && cultureNames is IDictionary cnd) + && cultureNames is IDictionary cnd) { - if (cnd.TryGetValue(currLangId.Value, out var name)) + if (cnd.TryGetValue(culture, out var name)) { e.Name = name; } @@ -393,9 +393,9 @@ namespace Umbraco.Web.Trees /// A list of MenuItems that the user has permissions to execute on the current document /// By default the user must have Browse permissions to see the node in the Content tree /// - internal bool CanUserAccessNode(IUmbracoEntity doc, IEnumerable allowedUserOptions, int? langId) + internal bool CanUserAccessNode(IUmbracoEntity doc, IEnumerable allowedUserOptions, string culture) { - //TODO: At some stage when we implement permissions on languages we'll need to take care of langId + //TODO: At some stage when we implement permissions on languages we'll need to take care of culture return allowedUserOptions.Select(x => x.Action).OfType().Any(); } diff --git a/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs b/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs index 525b1fcf2c..8c3b705bb6 100644 --- a/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs +++ b/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs @@ -27,14 +27,14 @@ namespace Umbraco.Web.WebApi.Binders protected override ContentItemDto MapFromPersisted(ContentItemSave model) { - return MapFromPersisted(model.PersistedContent, model.LanguageId); + return MapFromPersisted(model.PersistedContent, model.Culture); } - internal static ContentItemDto MapFromPersisted(IContent content, int? languageId) + internal static ContentItemDto MapFromPersisted(IContent content, string culture) { return ContextMapper.Map>(content, new Dictionary { - [ContextMapper.CultureKey] = languageId + [ContextMapper.CultureKey] = culture }); } } From a4b5e08a73ea4d980b0c725e9670e62badc22456 Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 1 May 2018 12:03:34 +0200 Subject: [PATCH 07/15] Simplified the way we mark variants as drafts trough localization keys, show published languages in the overlay instead of hiding them --- .../editor/umbeditorheader.directive.js | 11 ---- .../overlays/publish/publish.controller.js | 30 ++++++----- .../common/overlays/publish/publish.html | 51 ++++++++++++++----- .../components/editor/umb-editor-header.html | 4 +- .../umbraco/config/lang/en_us.xml | 1 + 5 files changed, 58 insertions(+), 39 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js index 1e9c1376a6..5abdd78c9d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js @@ -213,14 +213,11 @@ Use this directive to construct a header inside the main editor window. scope.vm.currentVariant = ""; function onInit() { - setVariantDraftState(scope.variants); - setCurrentVariant(scope.variants); setVariantStatusColor(scope.variants); } function setCurrentVariant(variants) { - setVariantDraftState(variants); angular.forEach(variants, function (variant) { if(variant.current) { @@ -249,14 +246,6 @@ Use this directive to construct a header inside the main editor window. }); } - function setVariantDraftState(variants) { - _.each(variants, function (variant) { - if (variant.isEdited === true && !variant.state.includes("Draft")) { - variant.state += ", Draft"; - } - }); - } - scope.goBack = function () { if (scope.onBack) { scope.onBack(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.controller.js index 081a923d4e..4a490ee735 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.controller.js @@ -39,32 +39,36 @@ } function onInit() { + console.log(variants); _.each(variants, function (variant) { variant.compositeId = variant.language.id + "_" + (variant.segment ? variant.segment : ""); variant.htmlId = "publish_variant_" + variant.compositeId; - //append Draft state to variant - if (variant.isEdited === true && !variant.state.includes("Draft")) { - variant.state += ", Draft"; + //separate "pristine" and "dirty" variants + if (variant.isEdited === true) { vm.dirtyVariants.push(variant); - } else if (variant.isEdited === true) { + } else if (variant.isEdited === true || + variant.isEdited === false && variant.state === "Unpublished") { vm.dirtyVariants.push(variant); } else { vm.pristineVariants.push(variant); } }); + if (vm.dirtyVariants.length !== 0) { + //now sort it so that the current one is at the top + vm.dirtyVariants = _.sortBy(vm.dirtyVariants, function (v) { + return v.current ? 0 : 1; + }); + //ensure that the current one is selected + vm.dirtyVariants[0].publish = true; + } else { + //disable Publish button if we have nothing to publish + $scope.model.disableSubmitButton = true; + } + vm.loading = false; - - console.log("Dirty Variants", vm.dirtyVariants); - - //now sort it so that the current one is at the top - vm.dirtyVariants = _.sortBy(vm.dirtyVariants, function (v) { - return v.current ? 0 : 1; - }); - //ensure that the current one is selected - vm.dirtyVariants[0].publish = true; } } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.html index f0d721ec04..aa97bf09d2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/publish/publish.html @@ -1,34 +1,57 @@ -
- +
+
-

What languages would you like to publish?

+

{{vm.dirtyVariants.length > 0 ? 'What languages would you like to publish?' : 'Nothing here to publish!'}}

-
+
+
+
+
+ + + +
+ +
+ +
+
+

Published languages.

+
+ +
+
+
{{ variant.language.name }}
+
{{ variant.state }}
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html index 41d44114a0..1b1eadab14 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html @@ -59,7 +59,9 @@ {{variant.language.name}} - {{variant.state}} + + + {{variant.state}}