Editable Dictionary Items

This blog post is a response to a question over at SDN (here). It’s a prototype of a solution, please use at your own risk and make sure you harden the code properly. I intentionally simplified it for the blog post. If I spend a little more time on it I will publish it as a module to the Marketplace

Context

The gist of the questions: We are heavily dependent on Translate.Text(). We like Page Editor. Can’t migrate to datasource items. Would very much like our dictionary translations editable in Page Editor. Can it be done?

The gist of the answer: Yes

Approach

Translation.Text() runs through a getTranslation pipeline (surprise!). We could create a pipeline processor that would look up the dictionary item and send the phrase field via renderField pipeline when executing in PageMode.IsPageEditorEditing mode. Sounds plausible. Let’s find out if it’s feasible.

It will be an iterative process and the approach will change slightly as we go through the implementation and testing. I will move away from the hook in getTranslation to a nicer @Html.Sitecore().Translation(). Bear with me.

Translation Hook

First, let’s hook into the getTranslation pipeline:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getTranslation>
        <processor patch:after="processor[@type='Sitecore.Pipelines.GetTranslation.ResolveContentDatabase, Sitecore.Kernel']" 
                   type="Score.Custom.Pipelines.Translation.TryRenderEditable, Score.Custom"/>
      </getTranslation>
    </pipelines>
  </sitecore>
</configuration>

Here’s how we are going to do it:

using Sitecore;
using Sitecore.Mvc.Presentation;
using Sitecore.Pipelines;
using Sitecore.Pipelines.GetTranslation;

namespace Score.Custom.Pipelines.Translation
{
    public class TryRenderEditable
    {
        public void Process(GetTranslationArgs args)
        {
            if (!Context.PageMode.IsPageEditorEditing)
            {
                return;
            }

            RenderEditablePhrase(args);
        }

        public virtual void RenderEditablePhrase(GetTranslationArgs args)
        {
            CorePipeline.Run("score.translation.editable", args);
        }
    }
}

As you can tell we will follow Sitecore best practices and implement everything in a pipeline as well. You will see why it’s important.

Editable Translations Pipeline

<score.translation.editable>
  <processor type="Score.Custom.Pipelines.Translation.LookupDictionatyItem, Score.Custom"/>
  <processor type=Score.Custom.Pipelines.Translation.RenderEditable, Score.Custom"/>
</score.translation.editable>

Step 1: look up the dictionary item. I simplified it a lot for the proof of concept:

  • Will reuse GetTralsnationArgs and use CustomData as a container
  • Will use a no-no-no GetDescendants(). Use ContentSearch API instead. The GetDescendants() will recursively retrieve all the items. It’s not a lazy LINQ-friendly IEnumerable.
using System.Linq;
using Sitecore.Pipelines.GetTranslation;

namespace Score.Custom.Pipelines.Translation
{
    public class LookupDictionatyItem
    {
        public virtual void Process(GetTranslationArgs args)
        {
            var root = args.ContentDatabase.GetItem("/sitecore/system/Dictionary");

            if (root != null)
            {
                args.CustomData["item"] = root.Axes
                    .GetDescendants()
                    .FirstOrDefault(x => x["Key"] == args.Key);
            }
        }
    }
}

Step 2: make the Phrase field editable

using Sitecore.Data.Items;
using Sitecore.Pipelines;
using Sitecore.Pipelines.GetTranslation;
using Sitecore.Pipelines.RenderField;

namespace Score.Custom.Pipelines.Translation
{
    public class RenderEditable
    {
        public void Process(GetTranslationArgs args)
        {
            var item = args.CustomData["item"] as Item;
            if (item == null)
            {
                return;
            }

            args.Result = MakePhraseEditable(item);
        }

        private string MakePhraseEditable(Item item)
        {
            var args = new RenderFieldArgs()
            {
                FieldName = "Phrase",
                Item = item
            };

            CorePipeline.Run("renderField", args);

            return args.ToString();
        }
    }
}

That’s almost everything. Three hurdles to go through but first we need a guinea pig view to test it:

@model RenderingModel

<div>    
    @Html.Raw(Sitecore.Globalization.Translate.Text("Editable Translation"))
</div>
Note the use of @Html.Raw(). The editable field comes loaded with markup. Translate.Text() works with strings and we need it to be HtmlString for the Razor view to render it as markup.

Hurdle One

If you put it all together and run, your Page Editor will be half-broken due to a JavaScript error. It turns out the page extenders (the ribbon and other helpers that Sitecore needs to make Page Editor work) also use Translate.Text() to render some translations for JavaScript interactions. Receiving markup instead of a string breaks the script. Luckily, extenders run outside of rendering context so we can make sure we only do our editable magic when Translate.Text() is called from within a rendering:

    public class TryRenderEditable
    {
        public void Process(GetTranslationArgs args)
        {
            if (!Context.PageMode.IsPageEditorEditing || 
                RenderingContext.CurrentOrNull == null)
            {
                return;
            }

            RenderEditablePhrase(args);
        }
        // ...
    }

At this point it seems to work just fine but there are two more hurdles left.

Hurdle Two (and a New Design)

You may be using Translate.Text() somewhere else in your code that runs in a rendering context where you don’t need (or can’t handle) the extra editing markup. Or maybe you want to have a special page that would open up all kinds of translation for editing and keep the site like it is today with no modifications? It would be nice to be able to say:

@using Score.Custom.Pipelines.Translation
@model RenderingModel

<div>    
    @Html.Sitecore().Translation("Editable Translation")
</div>

Feels just like @Html.Sitecore().Field(), doesn’t it?

Here’s how:

using System.Web;
using Sitecore;
using Sitecore.Data;
using Sitecore.Globalization;
using Sitecore.Mvc.Helpers;
using Sitecore.Pipelines;
using Sitecore.Pipelines.GetTranslation;

namespace Score.Custom.Pipelines.Translation
{
    public static class HelperExtensions
    {
        public static IHtmlString Translation(this SitecoreHelper helper, string key)
        {
            if (Context.PageMode.IsPageEditorEditing)
            {
                return RenderEditablePhrase(key);
            }

            // There's a potential danger in allowing the Razor engine treat your message as markup 
            // but it's ok for the prorotype
            return new HtmlString(Translate.Text(key));
        }

        private static IHtmlString RenderEditablePhrase(string key)
        {
            var args = new GetTranslationArgs()
            {
                ContentDatabase = Context.ContentDatabase ?? Context.Database ?? Database.GetDatabase("core"),
                Key = key
            };

            CorePipeline.Run("score.translation.editable", args);

            return new HtmlString(args.Result);
        }
    }
}

Easy! Now we can chose to use Translation.Text() or @Html.Sitecore().Translation() based on what we need. No unexpected interference from the editable markup. No need for the getTranslation hook either.

Hurdle Three

The field type for Phrase is memo. It’s a legacy type later replaced with Multi-Line Text. Not a hurdle really but keep in mind that Page Editor does a little extra for these fields on save. You shouldn’t see any side effects if your phrases are simple sentences. I did see a nbsp in the field value during my testing once but couldn’t reproduce it.

Why Pipeline

I mentioned that I will explain why I built the logic as a pipeline. How about we add a processor that would create a dictionary item if one doesn’t exist? How often do you (or your team members) do Translate.Text() without creating a dictionary item? Let’s create /sitecore/system/Dictionary/Unprocessed folder and use it as a container for the auto-created dictionary items. Someone will later come and put them where they need to be.

First, add a processor:

<score.translation.editable>
  <processor type="Score.Custom.Pipelines.Translation.LookupDictionatyItem, Score.Custom"/>
  <processor type="Score.Custom.Pipelines.Translation.CreateIfMissing, Score.Custom">
    <Folder>Unprocessed</Folder>
  </processor>
  <processor type="Score.Custom.Pipelines.Translation.RenderEditable, Score.Custom"/>
</score.translation.editable>

Then code it:

using Sitecore;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.GetTranslation;

namespace Score.Custom.Pipelines.Translation
{
    public class CreateIfMissing
    {
        public string Folder { get; set; }

        public virtual void Process(GetTranslationArgs args)
        {
            if (args.CustomData["item"] != null)
            {
                return;
            }

            var folder = args.ContentDatabase.GetItem("/sitecore/system/Dictionary/" + Folder);
            if (folder == null)
            {
                return;
            }

            args.CustomData["item"] = CreateDictionaryItem(folder, args.Key);
        }

        public virtual Item CreateDictionaryItem(Item folder, string key)
        {
            Assert.IsNotNull(folder, "folder");
            Assert.IsNotNullOrEmpty(key, "key");

            var item = folder.Add(key, new TemplateID(TemplateIDs.DictionaryEntry));

            using (new EditContext(item))
            {
                item["Key"] = key;
                item["Phrase"] = key;
            }

            return item;
        }
    }
}

That’s it. Enjoy!

Pavel Veller

3 comments on Editable Dictionary Items

GöranMay 15, 2015 - Reply

Great stuff! I will definitely use it.

YuqingJanuary 14, 2016 - Reply

Great post! Just one question, how can we make sure that the dictionary items are published if they are changed in the Page Editor? Publish related items seems not working. Any idea?

Pham Thanh TienJuly 11, 2016 - Reply

Hi Pavel Veller,

I’m learning Sitecore and I’m using Sitecore 8.1
i don’t know how to config …
i throught an error ” ‘patch’ is an undeclared prefix.”
can you provider me an advice ?

Thanks
Tien Pham

Add a Comment

Your email address will not be published. Required fields are marked *

Or request call back