Currently there is no client side API implementation done of the Sitecore dictionary translation API in the official Sitecore ASP .NET Core SDK, therefore I decided to implement a PoC. After googling a bit I found, Sitecore already provides an API endpoint to resolve dictionary items and it’s already implemented in the JSS clients. So based on this documentation I found the API endpoint on my local Sitecore CM instance, which is
/sitecore/api/jss/dictionary/<site>/<language>/?sc_apikey=<apikey>
The response of this endpoint is the following:
| { | |
| "lang": "en", | |
| "app": "Discovering.Sitecore101", | |
| "phrases": { | |
| "label1": "label 1", | |
| "label2": "label 2" | |
| } | |
| } |
1. Rendering Host implementation
Alright, then the task is to implement the client side in the Rendering Host. I got a recommendation from Nick Wesselman, that it would be cool to use something, that ASP .NET Core provides by default for localization. After a bit of research I ended up using the IStringLocalizer.
| public interface ISitecoreLocalizer : IStringLocalizer | |
| { | |
| Task Reload(); | |
| } | |
| public class SitecoreLocalizer : ISitecoreLocalizer | |
| { | |
| private static readonly Dictionary<string, Dictionary<string, string>> _sitecoreDictionary = new Dictionary<string, Dictionary<string, string>>(); | |
| private readonly HttpClient _httpClient; | |
| private readonly SitecoreLocalizerOptions _sitecoreLocalizerOptions; | |
| private readonly IHttpContextAccessor _httpContextAccessor; | |
| public SitecoreLocalizer( | |
| IHttpClientFactory httpClientFactory, | |
| IOptions<SitecoreLocalizerOptions> sitecoreLocalizerOptions, | |
| IHttpContextAccessor httpContextAccessor) | |
| { | |
| _httpClient = httpClientFactory.CreateClient("sitecoreLocalizer"); | |
| _sitecoreLocalizerOptions = sitecoreLocalizerOptions?.Value; | |
| _httpContextAccessor = httpContextAccessor; | |
| } | |
| public LocalizedString this[string name] | |
| => new LocalizedString(name, _sitecoreDictionary[_httpContextAccessor.HttpContext.Features.Get<IRequestCultureFeature>().RequestCulture.Culture.ToString()][name]); | |
| public LocalizedString this[string name, params object[] arguments] | |
| => new LocalizedString(name, string.Format(_sitecoreDictionary[_httpContextAccessor.HttpContext.Features.Get<IRequestCultureFeature>().RequestCulture.Culture.ToString()][name], arguments)); | |
| public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) | |
| { | |
| foreach (var dictionaryItem in _sitecoreDictionary[_httpContextAccessor.HttpContext.Features.Get<IRequestCultureFeature>().RequestCulture.Culture.ToString()]) | |
| { | |
| yield return new LocalizedString(dictionaryItem.Key, dictionaryItem.Value); | |
| } | |
| } | |
| public async Task Reload() | |
| { | |
| foreach (var culture in _sitecoreLocalizerOptions.Cultures) | |
| { | |
| var response = await _httpClient.GetAsync(_httpClient.BaseAddress.AbsoluteUri.Replace("[language]", culture.ToString())); | |
| if (response.IsSuccessStatusCode) | |
| { | |
| var content = await response.Content.ReadAsStringAsync(); | |
| _sitecoreDictionary[culture.ToString()] = JsonConvert.DeserializeObject<DictionaryModel>(content).Phrases; | |
| } | |
| } | |
| } | |
| public IStringLocalizer WithCulture(CultureInfo culture) | |
| { | |
| throw new System.NotImplementedException(); | |
| } | |
| } | |
| public class DictionaryModel | |
| { | |
| public Dictionary<string, string> Phrases { get; set; } | |
| } | |
| public class SitecoreLocalizerOptions | |
| { | |
| public IEnumerable<CultureInfo> Cultures { get; set; } | |
| } |
What this class does?
The Reload method is requesting the JSON result from the endpoint I mentioned above and then it saves it in a local cache, with this approach we are avoiding possible flooding of the Sitecore CM and CD server. This method is called by two sources:
- on startup of the Rendering Host to initialize this cache
- by the Sitecore CM server on publish via a Publishing Web Hook
The other methods are for to get the translated values from this “local cache”. To use this class, a few other adaptions are needed, let’s go through one-by-one.
Let’s start with the Startup, which consists of service registrations, dictionary cache initialization and opening the Reload endpoint to the Sitecore CM server. At this point I need to mention that I used the starter template which is provided by Sitecore, so that parts of the code are not included in the snippet below.
| public class Startup | |
| { | |
| private static readonly string _defaultLanguage = "en"; | |
| private static readonly List<CultureInfo> _supportedCultures = new List<CultureInfo> { new CultureInfo(_defaultLanguage) }; | |
| public void ConfigureServices(IServiceCollection services) | |
| { | |
| ... | |
| services.AddControllers(); | |
| // Dictionary registrations | |
| services.Configure<SitecoreLocalizerOptions>(options => options.Cultures = _supportedCultures); | |
| services.AddHttpClient<ISitecoreLocalizer, SitecoreLocalizer>("sitecoreLocalizer", client => | |
| { | |
| client.BaseAddress = Configuration.DictionaryServiceUri; | |
| }); | |
| services.AddTransient<ISitecoreLocalizer, SitecoreLocalizer>(); | |
| services.AddTransient<IStringLocalizer, SitecoreLocalizer>(); | |
| } | |
| public void Configure(IApplicationBuilder app, IWebHostEnvironment env) | |
| { | |
| app.UseEndpoints(endpoints => | |
| { | |
| ... | |
| // Open the endpoint to Sitecore CM publishing web hook | |
| endpoints.MapControllerRoute( | |
| "localization", | |
| "api/localization/reload", | |
| new { controller = "Localization", action = "Reload" } | |
| ); | |
| ... | |
| }); | |
| // Initialize the dictionary cache | |
| Task.Run(async () => await app.ApplicationServices.GetRequiredService<ISitecoreLocalizer>().Reload()).Wait(); | |
| } | |
| } |
Next one is the LocalizationController, which is really simple, it’s just exposing the Reload method to an endpoint.
| public class LocalizationController : Controller | |
| { | |
| private readonly ISitecoreLocalizer _sitecoreLocalizer; | |
| public LocalizationController(ISitecoreLocalizer sitecoreLocalizer) | |
| { | |
| _sitecoreLocalizer = sitecoreLocalizer; | |
| } | |
| [HttpPost] | |
| public IActionResult Reload() | |
| { | |
| _sitecoreLocalizer.Reload(); | |
| return Ok(); | |
| } | |
| } |
Finally, the usage of the ISitecoreLocalizer.
| @using Microsoft.Extensions.Localization | |
| @inject IStringLocalizer Localizer | |
| <div>@Localizer["label1"]</div> |
The ISitecoreLocalizer can be injected to any class or razor view where the IRequestCultureFeature is available.
2. Sitecore CM instance implementation
As I mentioned earlier, in Sitecore CM instance we would just need to configure a Publishing Web Hook entry to call the /api/localization/reload endpoint on publish to reload the local cache for the localization. This config looks like the following:
| <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/" xmlns:search="http://www.sitecore.net/xmlconfig/search/"> | |
| <sitecore> | |
| <layoutService> | |
| <publishWebHooks type="Sitecore.JavaScriptServices.AppServices.WebHooks.WebHooks, Sitecore.JavaScriptServices.AppServices"> | |
| <hooks hint="list:AddWebHook"> | |
| <hook type="Sitecore.JavaScriptServices.AppServices.WebHooks.WebHookDefinition, Sitecore.JavaScriptServices.AppServices"> | |
| <name>ReloadDictionary</name> | |
| <url>http://rendering/api/localization/reload</url> | |
| <method>POST</method> | |
| </hook> | |
| </hooks> | |
| </publishWebHooks> | |
| </layoutService> | |
| </sitecore> | |
| </configuration> |
Based on the logs, this should work BUT I encountered a delay in the update of the /sitecore/api/jss/dictionary/<site>/<language>/?sc_apikey=<apikey> endpoint result after publish. After digging into this implementation, I found there is a hidden caching mechanism which is caching the result of the phrases to 2 minutes. This unfortunately makes impossible to use the Publishing Web Hook for the cache reload in the Rendering Host, therefore I decided the overwrite the default caching mechanism with no caching at all. For that I had to overwrite the ApplicationDictionaryReader with my own:
| public class NoCacheApplicationDictionaryReader : ApplicationDictionaryReader | |
| { | |
| public NoCacheApplicationDictionaryReader( | |
| IDictionaryDomainResolver dictionaryDomainResolver, | |
| ITranslationDictionaryReader translationDictionaryReader, | |
| HttpContextBase httpContext) | |
| : base(dictionaryDomainResolver, translationDictionaryReader, httpContext) | |
| { | |
| } | |
| public override IDictionary<string, string> GetPhrases(AppConfiguration application, Language language) | |
| { | |
| var dictionaryDomain = this.DictionaryDomainResolver.GetDictionaryDomain(application); | |
| if (dictionaryDomain == null) | |
| { | |
| return null; | |
| } | |
| return this.TranslationDictionaryReader.GetPhrases(dictionaryDomain, language); | |
| } | |
| } |
| public class RegisterDependencies : IServicesConfigurator | |
| { | |
| public void Configure(IServiceCollection serviceCollection) | |
| { | |
| serviceCollection.AddScoped<IApplicationDictionaryReader, NoCacheApplicationDictionaryReader>(); | |
| } | |
| } |
| <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/"> | |
| <sitecore> | |
| <services> | |
| <configurator type="Discovering.Sitecore101.RegisterDependencies, Discovering.Sitecore101" /> | |
| </services> | |
| </sitecore> | |
| </configuration> |
I created a feature request – 483716 – and asked Sitecore Support to able switch off the caching mechanism. I got a nice answer:
After reviewing details at our end, it looks useful to be able to turn off the mentioned caching in certain usage scenarios, so we have registered a related feature request for the product so that it can be considered for future implementation.
Sitecore Support
Please keep in mind this is a PoC, not ready for production! It does not contain any error and exception handling. The goal of this post, to show the essentials of how you can implement a dictionary implementation in your solution, using the existing endpoint.
![Tamás Tárnok = [ C#, Sitecore, … ]](https://trnktms.com/wp-content/uploads/2021/11/pxart_white-small.png)