[su_note note_color=”#fafafa”]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[/su_note]
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.
[su_note note_color=”#fafafa”]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.[/su_note]
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 useCustomData
as a container - Will use a no-no-no
GetDescendants()
. Use ContentSearch API instead. TheGetDescendants()
will recursively retrieve all the items. It’s not a lazy LINQ-friendlyIEnumerable
.
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>[su_note note_color=”#fafafa”]Note the use of
@Html.Raw()
. The editable field comes loaded with markup.Translate.Text()
works with strings and we need it to beHtmlString
for the Razor view to render it as markup.[/su_note]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 whenTranslate.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 thegetTranslation
hook either.Hurdle Three
The field type for
Phrase
is memo. It’s a legacy type later replaced withMulti-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!
Great stuff! I will definitely use it.
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?
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
Pingback: Dictionaries, fields and translations | Sitecore and such
This is really cool. I have been meaning to do something similar, looking for patterns and whatnot, but in R.