Monday, October 24, 2011

Language Fallback and Sitecore Dictionary

Sitecore CMS Dictionary is the best place to keep all texts that can be used across the site: "read more", "click here", etc. . It's internal architecture is a little unusual - it stores key/value pairs in the temp file  /{yourSiteRoot}/temp/dictionary.dat, which is updated during item:saved event(via the handler in web.config).



This approach excludes possible delays caused by loading dictionary entries from the database upon website restart. However, it makes it impossible to implement any kind of fallback because dictionary.dat file is not populated if there are no versions in a given language - just take a look at the handler code:
internal void OnItemSaved(object sender, EventArgs args)
    {
        Assert.ArgumentNotNull(sender, "sender");
        Assert.ArgumentNotNull(args, "args");
        Item item = Event.ExtractParameter(args, 0) as Item;
        Assert.IsNotNull(item, "No item in parameters");
        if (item.TemplateID == TemplateIDs.DictionaryEntry)
        {
            Hashtable languages = Translate.Languages;
            string str = item["Key"];
            foreach (Language language in item.Languages)
            {
                Hashtable hashtable2 = languages[language.ToString()] as Hashtable;
                if (hashtable2 != null)
                {
                    Item item2 = item.Database.Items[item.ID, language];
                    if (item2 != null)
                    {
                        hashtable2[str] = item2["Phrase"];
                    }
                }
            }
            Translate.Save();
        }
    }

The following solutions seem to be simple and straightforward:

1) Always create dictionary items in all languages / copy fallback values.
2) Use custom Translate.Text implementation.

but both of them have a lot of disadvantages - like storing garbage in a database, etc.

Another approach is more complicated, but only at the implementation phase - override event handler to write fallback value to the dictionary.dat if there is no version in a current language.

The problems are following - both Translate.Languages property and Translate.Save() method are marked as internal, that means they cannot be called outside of Sitecore.Kernel. While it's generally not recommended to use reflection, it seems that there are no more workarounds in this particular case.

Translate.Languages can be replaced with
(Hashtable)typeof(Translate).GetProperty("Languages", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null, null);
and Translate.Save() with
typeof (Translate).GetMethod("Save", BindingFlags.NonPublic | BindingFlags.Static);

In this example I'll get all language, and use the text from DefaultLanguage version as a fallback value. It can be also customized to use different fallback values per language.

var allLanguages = LanguageManager.GetLanguages(Sitecore.Data.Database.GetDatabase("master"));
...
var fallbackPhrase = item.Database.GetItem(item.ID, LanguageManager.DefaultLanguage)["Phrase"];
...
hashtable2[str] = string.IsNullOrEmpty(item2["Phrase"]) ? fallbackPhrase : item2["Phrase"];

Here is the complete source code:

internal void OnItemSaved(object sender, EventArgs args)
        {
            Assert.ArgumentNotNull(sender, "sender");
            Assert.ArgumentNotNull(args, "args");
            Item item = Event.ExtractParameter(args, 0) as Item;
            Assert.IsNotNull(item, "No item in parameters");

            var allLanguages = LanguageManager.GetLanguages(Sitecore.Data.Database.GetDatabase("master"));

            if (item.TemplateID == TemplateIDs.DictionaryEntry)
            {
                Hashtable languages = (Hashtable)typeof(Translate).GetProperty("Languages", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null, null);
                var fallbackPhrase = item.Database.GetItem(item.ID, LanguageManager.DefaultLanguage)["Phrase"];

                string str = item["Key"];
                foreach (Language language in allLanguages)
                {
                    Hashtable hashtable2 = languages[language.ToString()] as Hashtable;
                    if (hashtable2 != null)
                    {
                        Item item2 = item.Database.Items[item.ID, language];
                        if (item2 != null)
                        {
                            hashtable2[str] = string.IsNullOrEmpty(item2["Phrase"]) ? fallbackPhrase : item2["Phrase"];
                        }
                    }
                }

                var saveMethod = typeof (Translate).GetMethod("Save", BindingFlags.NonPublic | BindingFlags.Static);
                saveMethod.Invoke(null, new object[0]);
            }
        }

All you need to do to make it work - compile the code and replace item:saved enjoy handler. Enjoy!