Editor friendly language fallback

Language fallback is really nice when working multi lingual websites. It save a lot of time and makes the solution very flexible. However, among content editors it can cause some headaches since the content shown to editors may not be the actual content being published.

I touched on this in my previous post about flatten the Sitecore web database, where this becomes a real problem unless we solve it. If we just flatten the web db as I described in that post, unapproved content can be published.

language fallback modelThe problem can be visualized with this diagram, representing one Sitecore item. Let’s say the Swedish (sv) language is configured to fallback to the English (en) language. When there is no content (null) in a fallback enabled field in the Swedish version, it will by default fallback to the latest English version. That content will be displayed to content editors while working in the master database, both in Content Editor and Experience Editor. This is confusing to editors, since that’s not the content being published.

The fallback operation is performed at runtime, meaning that in this example, only the first version will be published and therefore the fallback will be only to approved English content (correct) but not to the content shown to editors when editing and approving the Swedish language version. So let’s fix this.

The code example applies to Sitecore 8.1 with built in language fallback. The concept can applied to any version.

When Sitecore loads a field value for an item, the language fallback module executes the configured languageFallbackFieldValues provider. We can simply extend the default one with something like the one below. Essentially, I’ve kept the class as the default one, but extended it with some logic that will prevent it from performing a fallback to a unapproved version.

public class CustomLanguageFallbackFieldValuesProvider : LanguageFallbackFieldValuesProvider
{
  public override LanguageFallbackFieldValue GetLanguageFallbackValue(Field field, bool allowStandardValue)
  {
    // This is Sitecore default code
    var fallbackValuesFromCache = GetFallbackValuesFromCache(field, allowStandardValue);
    if (fallbackValuesFromCache != null)
      return fallbackValuesFromCache;

    var item = field.Item;
    Field fallbackField = null;
    string fieldValue = null;
    var list = new List<Language>(4);
    using (new EnforceVersionPresenceDisabler())
    {
      using (new LanguageFallbackItemSwitcher(false))
      {
        do
        {
          list.Add(item.Language);
          var fallbackLanguage = LanguageFallbackManager.GetFallbackLanguage(allowStandardValue ? 
            item.Language : item.OriginalLanguage, item.Database, item.ID);
          
          // Replaced code from here
          if (fallbackLanguage == null ||
            string.IsNullOrEmpty(fallbackLanguage.Name) ||
            list.Contains(fallbackLanguage))
          {
            break;
          }

          item = item.Database.GetItem(item.ID, fallbackLanguage, Sitecore.Data.Version.Latest);
          if (item == null)
            break;

          // Test if this version is valid. If not, load older versions
          if (!IsSharedValid(item))
            break;

          if (!IsItemVersionValid(item, field.ID))
          {
            item = item.Versions
              .GetOlderVersions()
              .Reverse()
              .FirstOrDefault(i => IsItemVersionValid(i, field.ID));
            if (item == null)
              break;
          }
          // End of replaced code

          fallbackField = item.Fields[field.ID];
          fieldValue = fallbackField.GetValue(allowStandardValue, false, false);
        }
        while (fieldValue == null);
      }
    }

    LanguageFallbackFieldValue languageFallbackFieldValue;
    if (fieldValue != null)
    {
      ItemUri uri = item.Uri;
      bool containsStandardValue = fallbackField.ContainsStandardValue;
      languageFallbackFieldValue = new LanguageFallbackFieldValue(fieldValue, containsStandardValue, uri.Language.Name);
    }
    else
      languageFallbackFieldValue = new LanguageFallbackFieldValue(null, false, null);

    AddLanguageFallbackValueToCache(field, allowStandardValue, languageFallbackFieldValue);
    return languageFallbackFieldValue;
  }

  // Extend this as needed
  public virtual bool IsSharedValid(Item item)
  {
    return item.Publishing.IsPublishable(DateTime.UtcNow, false);
  }

  // Extend this as needed
  public virtual bool IsItemVersionValid(Item item, ID fieldID)
  {
    bool hasPublishingErrors = PublishingInformationBuilder
      .GetPublishingInformation(item, PublishingInformationLevel.Item | PublishingInformationLevel.Version)
      .OfType<Sitecore.Publishing.Explanations.Error>()
      .Any();
    return !hasPublishingErrors;
  }
}

Then patch this into the config like this:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <languageFallbackFieldValues>
      <patch:attribute name="defaultProvider">custom</patch:attribute>
      <providers>
        <add name="custom" type="YourNamespace.CustomLanguageFallbackFieldValuesProvider, YourAssembly" />
      </providers>
    </languageFallbackFieldValues>
  </sitecore>
</configuration>

Now only approved and publishable content will be displayed to authors in the Sitecore editors. From web db perspective, this won’t make much difference, since only the latest publishable version will be published.

If we apply the flatten web db concept mentioned earlier, we get an additional advantage of this. The web database will be filled with approved content for each language. As the fallback content is approved and published (English version 2 in this example), other contents (Swedish versino 1 in this example) will stay as-is until it’s published.