It’s very easy to get translations in back-end code — either Razor views or .cs files — by simply calling to Sitecore.Globalization.Translate.Text(). But what about JavaScript widgets? There we have several options:
- Item Web API results in overhead and is hard to use without additional implementation.
- StringDictionary embeds values right in HTML and needs to be configured accordingly.
- Injecting translated text into .js components via HTML tags has the same drawbacks as StringDictionary.
All these options are cumbersome and inconvenient.
I will show you a convenient way to have Translate.Text() right in JavaScript! The idea is to use a JavaScript dictionary object in our .js widgets/components. To achieve the goal we need the following:
- Serialize dictionary values into .json files
- Implement Dictionary.js
Let’s start coding!
Serializing the dictionary into .json files
Serializing the whole dictionary creates one .json file per language in a temp folder. The file’s name looks like “dictionary.{language}.json”, e.g. “dictionary.fr-CA.json”. The dictionary is serialized each time we perform a site publish or when we publish a Dictionary root item or its descendants.
using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Sitecore; using Sitecore.Data; using Sitecore.Data.Events; using Sitecore.Data.Items; using Sitecore.Data.Managers; using Sitecore.Diagnostics; using Sitecore.Events; using Sitecore.Globalization; using Sitecore.IO; using Sitecore.Publishing; using System; using System.Collections.Generic; using System.Dynamic; using System.Linq; namespace Company.Project.EventHandlers { /// <summary> /// Creates dictionary .json files on publish events /// </summary> public class SerializeDictionaryToJson { private static ID DictionaryFolderTemplateID = new ID("{267D9AC7-5D85-4E9D-AF89-99AB296CC218}"); public void OnPublishEnd(object sender, EventArgs args) { var sitecoreArgs = args as SitecoreEventArgs; Assert.IsNotNull(sitecoreArgs, "The parameter 'sitecoreArgs' is null"); var publisher = sitecoreArgs.Parameters[0] as Publisher; Assert.IsNotNull(publisher, "The parameter 'publisher' is null"); var rootItem = publisher.Options.RootItem; if (ShouldSearializeDictionary(rootItem)) { SerializeDictionaryToJsonFiles(); } } public void OnPublishEndRemote(object sender, EventArgs args) { var remoteEventArgs = args as PublishEndRemoteEventArgs; Assert.IsNotNull(remoteEventArgs, "The parameter 'remoteEventArgs' is null"); Item rootItem = null; var rootItemID = remoteEventArgs.RootItemId; if (rootItemID != default(Guid)) { var db = Database.GetDatabase("web"); rootItem = db.GetItem(ID.Parse(rootItemID)); } if (ShouldSearializeDictionary(rootItem)) { SerializeDictionaryToJsonFiles(); } } private bool ShouldSearializeDictionary(Item rootItem) { return rootItem == null || rootItem.TemplateID == DictionaryFolderTemplateID || rootItem.TemplateID == TemplateIDs.DictionaryEntry || rootItem.ID == ItemIDs.Dictionary; } public void SerializeDictionaryToJsonFiles() { var db = Database.GetDatabase("web"); var languages = LanguageManager.GetLanguages(db); foreach (var language in languages) { var values = GetDictionaryValues(db, language); CreateDictionaryJsonFile(values, language); } Log.Info("Dictionary has been serialized to json files successfully.", this); } public void CreateDictionaryJsonFile(IDictionary<string, object> values, Language language) { var json = JsonConvert.SerializeObject(values, new KeyValuePairConverter()); var filePath = $"{TempFolder.Folder}/dictionary.{language.Name}.json"; FileUtil.WriteToFile(filePath, json); } public IDictionary<string, object> GetDictionaryValues(Database db, Language language) { IDictionary<string, object> dictionary = new ExpandoObject(); using (new LanguageSwitcher(language)) { var root = db.GetItem("/sitecore/system/Dictionary"); var items = root.Axes.GetDescendants() .Where(i => i.TemplateID == TemplateIDs.DictionaryEntry); foreach (var item in items) { var key = item[FieldIDs.DictionaryKey]; dictionary[key] = item[FieldIDs.DictionaryPhrase]; } } return dictionary; } } }
The last thing is to plug SerializeDictionaryToJson.cs to publish:end and publish:end:remote events by using patch config.
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <events> <event name="publish:end"> <handler type="Company.Project.EventHandlers.SerializeDictionaryToJson, Company.Project" method="OnPublishEnd"/> </event> <event name="publish:end:remote"> <handler type="Company.Project.EventHandlers.SerializeDictionaryToJson, Company.Project" method="OnPublishEndRemote"/> </event> </events> </sitecore> </configuration>
Implement Dictionary.js
I assume you are using a module loader; in my example it’s require.js. Dictionary.js loads the proper .json file only once per page based on the current context language and provides translation.
define(["jquery"], function ($) { Dictionary._instance = null; function Dictionary() { this.values = {}; } Dictionary.prototype.translate = function (key) { return this.values[key] || key; } Dictionary.prototype.getContextLanguage = function () { return $('meta[http-equiv="content-language"]').attr("content"); } Dictionary.prototype.loadValues = function () { var language = this.getContextLanguage(); var valuesUrl = "/temp/dictionary." + language + ".json"; // We disable browser's cache to ensure translations are up to date $.ajax({ cache: false, async: false, url: valuesUrl }) .done(function (data) { this.values = Object.freeze(data); }.bind(this)) .fail(function () { console.error("Couldn't load dictionary values"); }); } Dictionary.getInstance = function () { if (Dictionary._instance == null) { var dictionary = new Dictionary(); dictionary.loadValues(); Dictionary._instance = dictionary; } return Dictionary._instance; } return Dictionary.getInstance(); });
How to use Dictionary.js
In this sample, our SearchBox widget utilizes translations based on the current page language.
define(["Dictionary"], function (dictionary) { function SearchBox(options) { this.noResultsFoundText = dictionary.translate("NoResultsFound"); //... } //... return SearchBox; });
dictionary.en.json snippet
{ ... "NoResultsFound": "Sorry, we have no content matching your criteria." ... }
dictionary.fr-CA.json snippet
{ ... "NoResultsFound": "Désolé, nous avons aucun contenu ne correspond à vos critères." ... }
Dictionary.js features
- Doesn’t inject values into HTML.
- Doesn’t impact SEO.
- Lazy loading.
- No need to configure.
- Small overhead: serialized .json file is 13kB for 300 items in real-world application.
- Can be loaded from browser’s cache.
Might want to plug that into “publish:end:remote” as well otherwise the file will not be created on the CD servers.
> Thanks for pointing me out, you’re absolutely right. I will update the post in case of CD.
That’s very similar to a way how translation is done in Experience Editor(look at TranslationUtil.js).
define([“sitecore”], function (Sitecore) {
var translationUtils = {
keys: {
},
translateText: function (key) {
return Sitecore.Resources.Dictionary[key];
},
translateTextByServer: function (key) {
…
},
};
return translationUtils;
});
TranslationUtil.js looks ‘similar’ but:
TranslationUtil.translateText uses values injected in html and StringDictionary needs to be configured for each page.
TranslationUtil.translateTextByServer can be used in Experience Editor context only and sends one ajax per ‘key’ which results in request/response overhead.
The approach described in the post uses more flexible way without needs to configure, injections and so on.
Pingback: Sitecore Dictionaries Best Practices | Sitecore notes - Stelio Di Bello