Using translation API with Sitecore ASP .NET Core Rendering Host

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();
}
}
view raw Startup.cs hosted with ❤ by GitHub

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/&quot; xmlns:set="http://www.sitecore.net/xmlconfig/set/"&gt;
<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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s