Thursday, August 12, 2010

Tackling Fallback Fields and Values in Sitecore

I recently had the opportunity to fight with …. Err … learn about globalization within Sitecore. If you don’t know what Globalization is within Sitecore, then you are reading the right article, because your knowledge on the subject is the same as mine was when I first started working with it. I almost viewed it as the scary monster under my bed, about to tear my feet off the moment the light goes out. But, trudging forward and facing my fears, I pressed on and the outcome was quite surprising. I actually had a lot of fun once I got around some of the major obstacles in my way, the largest and most pronounced being my very own lack of knowledge on this specific subject. So, after beating my head against the wall, sweating profusely for days at a time, and finally consulting an oracle (he looked kind of like a homeless person to me, but I took his word for it) here is what I have found out about Globalization within Sitecore.

Before I begin, let me preface this with the following statement: this post is not an end-all be-all for globalization, just information I happen to remember and care to relate from my experiences. It also only deals with globalization within the context of Sitecore. Good, now that we got that out of the way, we can continue….

As you may or may not know, globalization is just a fancy pants way to say multi-lingual, which is an even more fancy way to say multiple languages. So, if you want multiple languages to display on your site, then you are going to have to use some form of globalization. For the purposes of this post, let us assume that we are dealing with English as your main language and Bangladeshi (just because I like the way it sounds….. Bangladeshi  ) as your secondary language.

In order to create a user-friendly website that caters to both of your demographic groups, we need some form of language switching on the site. Putting a dropdown at the top of your site is easy enough. In this example, we are setting our language embedding to “always”, so the URL of the current page also defines the language context being used. We do this with the following line in the web.config:

<linkManager defaultProvider="sitecore">
<providers>
<clear />
<add name="sitecore" type="Sitecore.Links.LinkProvider, Sitecore.Kernel" addAspxExtension="true" alwaysIncludeServerUrl="false" encodeNames="true" languageEmbedding="always" languageLocation="filePath" shortenUrls="true" useDisplayName="false" />
</providers>
</linkManager>

Now when you navigate to a page within Sitecore, you should see something like this: http://www.yoursite.com/en/ItemName.aspx for the English version or http://www.yoursite.com/bgd/ItemName.aspx for the Bangladeshi version.

Create a Bangladeshi versions of “ItemName”, fill it with content, publish, and BLAMO, Bangladeshi content for all to see. Easy enough, right? Yay for out of the box functionality! And for most websites this is all that will ever be required. But let’s get our knees dirty and dig in a bit deeper.

Say we want to fall back to the English version of the page if the Bangladeshi version doesn’t exist. All we do is write a custom pipeline process that will determine if the item exists in the current language, and if it doesn’t, send the user to the /en/ version of the page. This is pretty basic stuff. Here is a quick overview on how to do that if you are interested:

Update web.config with your custom pipeline process. Make sure it’s in the correct location…

<processor type="Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel" />
<processor type="YourAssemblyHere.Pipelines.HttpRequest.FallbackLanguageProcessor, YourAssemblyHere " />
<processor type="Sitecore.Pipelines.HttpRequest.LayoutResolver, Sitecore.Kernel" />

And add the corresponding class to your project…

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace YourAssemblyHere.Pipelines.HttpRequest
{
public class FallbackLanguageProcessor
{
public void Process(Sitecore.Pipelines.HttpRequest.HttpRequestArgs args)
{
Sitecore.Data.Items.Item contextItem = Sitecore.Context.Item;
if (contextItem == null || contextItem.Versions.Count > 0)
return;


Sitecore.Globalization.Language language = Sitecore.Context.Language;
if (Sitecore.Context.Language.Name != "en")
language = Sitecore.Globalization.Language.Parse("en");
Sitecore.Data.Database contextDatabase = Sitecore.Context.Database;
Sitecore.Context.Item = contextDatabase.GetItem(Sitecore.Context.Item.ID, language);
}
}
}

As you can see, all this does is determine if the current item does NOT have a version (so you are trying to view a Bangladeshi page but it doesn’t exist), get the English version of this page, reset the context, and you are on your merry way. Weeeeeeeeee, isn’t this fun!?!

By this time in the project, I was feeling pretty good about my accomplishments and up to this point wasn’t getting tripped up too much. But alas, a new requirement came down from above: let’s make each FIELD fall back to English if there is no value within the Bangladeshi version. This means that if I go to the About Us page, and I’m in the Bangladeshi context, and there is no About Us page within Sitecore for the Bangladeshi item then I need to display the English version of that page without changing the context of the site! I know, I get a migraine just thinking about it.

Before I continue, I must give you fellow Globalization newbie’s a bit of advice… If you can help it, NEVER display multiple languages to the user on the same page. Not only does this create confusion for the user, it is rather pointless. If the user doesn’t understand English, then why would they ever care to see it on your website? Once you start mixing and matching languages on the same page the context of the site gets confusing for the user. They see content that is in their native language, but the navigation links (some or all of them) are in some foreign language (English in this case). But what the heck right? Let’s see just how extensible Sitecore really is (I love trying to break stuff!).

The process is similar to what we did earlier. We need to add a process to the “renderField” pipeline section of the web.config like this…

<processor type="Sitecore.Pipelines.RenderField.GetDateFieldValue, Sitecore.Kernel" />
<processor type="YourAssemblyHere.Pipelines.FieldRender.FallbackLanguageProcessor, YourAssemblyHere" />

<processor type="Sitecore.Pipelines.RenderField.AddBeforeAndAfterValues, Sitecore.Kernel" />

And add another class that handles this process like so…

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore.Pipelines.RenderField;


namespace YourAssemblyHere.Pipelines.FieldRender
{
public class FallbackLanguageProcessor
{
public void Process(RenderFieldArgs args)
{
//only perform this logic if this is not the default language
if (Sitecore.Context.Language.Name != "en")
{
if (args.Item != null)
{
//Get the current Item passed in by the arguments
Sitecore.Data.Items.Item currentItem = args.Item;
if (currentItem != null)
{
if (currentItem.Fields[args.FieldName] != null)
{
//if the current item has no valid value
if (currentItem.Fields[args.FieldName].Value.Equals("$name") ||
(currentItem.Fields[args.FieldName].Value.Equals(string.Empty)))
{
//Determine the fallback language for this item. in this case, we are forcing english.
Sitecore.Globalization.Language language = Sitecore.Globalization.Language.Parse("en");
//get the item in the default language
Sitecore.Data.Items.Item newItem = Sitecore.Context.Database.GetItem(currentItem.ID, language);


if (newItem != null)
{
//only make the update if the english field has a value
if (newItem.Fields[args.FieldName] != null)
if (!string.IsNullOrEmpty(newItem.Fields[args.FieldName].Value))
args.Result.FirstPart = newItem.Fields[args.FieldName].Value;
//disable web edit for this field, since it’s a fallback field editors should not be able to edit
args.DisableWebEditContentEditing = true;
}
}
}
}
}
}
}
}
}

Seems easy enough! Now, like before, our process will tick off and check to see if the FIELD that is trying to be displayed exist in Bangladeshi, and if not, go get the English value for that field and display that instead. Quick note: if you are accustomed to using sc:fld(‘Title’,.) in your xslt renderings, then those values don’t go through the fieldRender pipeline. You must use sc:field(‘Title’,.) in order for our custom process to have any effect. This specific issue causes grey hair apparently, so take heed! Just make sure your outputting your xslt value-of’s with sc:field. If you are using <sc:text, etc, then it should work fine.

Now, we have our FIELDS displaying English if the Bangladeshi field doesn’t have a value (this is mostly due to lazy or “too-busy-to-get-to-it” content editors ). So, here is where we currently are at. We are on the Bangladeshi site, looking at the About Us page. The top navigation is displaying either English or Bangladeshi links, depending on if that value exists in Sitecore, and all is good in the world.

“What about non-“fieldRender” pipeline values that are grabbed programmatically?” you say. Alright, let’s explore that. One example of why it is important to be able to get a fallback for a specific field programmatically is when you are setting the text property of a label to a field of an item.

So, pretend that on the Page_Load method we set a <asp: Label’s “Text” property to the Title field of the current item. We have to check to see if the item exists in Bangladeshi first, and if it doesn’t, then get the English version of the item and then display the title. Here is how to do that.

public static bool HasContextLanguage(Item item)
{
Item latestVersion = item.Versions.GetLatestVersion();
return ((latestVersion != null) && (latestVersion.Versions.Count > 0));
}

As you can see, pass it the item you want to check and it will return a “true” if the item exist within Bangladeshi (assuming you are within the Bangladeshi context), and “false” if it does not. You will use this method to see if you need to perform some additional logic, like grabbing an English version of the item and displaying the Title. Your Page_Load method might look a little something like this….

protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
Item currentItem = Sitecore.Context.CurrentItem;
if (!HasContextLanguage(currentItem))
currentItem = currentItem.Versions.GetLatestVersion(Sitecore.Globalization.Language.Parse("en"));
lblTitle.Text = currentItem.Fields["Title"].Value;
}
}

The last thing I want to go over is quite a bit more complex that what has been covered so far, so I apologize in advance for the mass confusion you are about to read and my general lack of ability to convey something in a more precise manner. It has to do with a fallback field on a fallback item that is a referenced target item of the current item. All that alludes to is how we handle globalization on referenced items within Sitecore (lookup fields, reference fields, multi-select fields, etc).

For example, imagine you have a “Page” Template with a lookup field called “Contact”. This “Contact” field will reference an item based on a “Contact” template that has the following fields: Name, Title, Company, Phone Number, and Address. Assume that the page and the contact are already created within Sitecore, but only in English. Along comes the Bangladeshi content editor and they add a Bangladeshi version to the page. While entering in content for the page, they select the English-only contact (we will call him “Bob” for now) and publish the item. We have to take a few things into consideration when trying to output Bob’s information.

When someone goes to the page and is in the Bangladeshi context, how do we display Bob correctly? Normally you get the TargetItem of the Contact field and display the name. In this case, it’s not so simple. You have to remember that Bob doesn’t exist within the Bangladeshi context. So first we have to get the Target item, and then do our check to see if it (Bob) exists in the current context and if not, get the English version of Bob to display. Here is how we would go about that:

Sitecore.Data.Fields.LookupField lookupField = item.Fields["Contact"];
if (lookupField != null && lookupField.TargetItem != null)
{
Item targetItem = lookupField.TargetItem;
if (!HasContextLanguage(targetItem))
targetItem = targetItem.Versions.GetLatestVersion(Sitecore.Globalization.Language.Parse("en"));
//Do Stuff with Bob here....
}

This logic is getting the “Contact” lookup field, getting its “TargetItem”(Bob), checking to see if the TargetItem exist within the current context, and if not, re-getting the target item as the English version, and then displaying the information as necessary.

I hope this all makes sense, I know I babble on and on and on and on a ….err, heh. Oh, and even after typing it 18,976 times, I still like the word “Bangladeshi”. Congratulations, you are at the end of my ranting and can go on with your lives. I hope you have learned something of value. Happy globalization!

Thanks – Caleb Miller.

*EDIT October 28, 2010. We have moved our blog to http://blog.roundedcube.com and you can now comment on this specific post at http://www.roundedcube.com/WhatsNew/Blog/tackling-fallback-fields-and-values-in-sitecore