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.