.NET Zero to Hero Series is now LIVE! JOIN 🚀

8 min read

JSON Based Localization in ASP.NET Core With Caching - Super Easy Guide

#dotnet

In this article, we are going to learn how to implement JSON Based Localization in ASP.NET Core and club it with Caching to make it even more efficient. In a previous article (Globalization and Localization in ASP.NET Core), we learned the basics of localization where we developed an MVC solution that gives us the option to switch between various languages /cultures making use of RESX files, where we stored the corresponding language strings. But now, let’s use JSON files to store the localized strings and implement middleware to switch languages via language keys in the request header.

You can find the entire source code of this implementation here.

What we’ll build?

We will be building a simple .NET 5.0 WebAPI which returns messages based on the request header’s Accepted Language. Behind the scenes, we will also cache the string via IDistributedCache. The main objective of this implementation is to read the language strings from a JSON file rather than a RESX file. For this, we will be adding in a new implementation for the IStringLocalizer. This will be a pretty straightforward and simple article yet helps a lot in production-ready applications. Who doesn’t want JSON files? :D

It will be as simple as adding 3 new classes and a couple of service registrations. Let’s get started!

Getting started with JSON Based Localization in ASP.NET Core

Open up your favorite IDE (I use Visual Studio 2019 Community), and create a new ASP.NET Core Web API Project. Make sure to select .NET 5.0 Framework (or the latest one at the time of reading this article. Note that .NET 6 LTS is just around the corner!).

To keep this implementation simple, I will not be adding in any additional class libraries. I removed the weather controllers and related files from the WebAPI solution to tidy up the project.

As mentioned earlier, we have two parts to this implementation:

  • A Middleware that can determine the language code passed in at the request header by the client (which will be postman in our case).
  • An implementation of the IStringLocalizer to support JSON files. I intend to store the JSON file by the locale name (en-US.json) under a Resources folder. Note that we will also use IDistributedCache to make our system more effecient.

Let’s create a new class and name it JsonStringLocalizer.cs

public class JsonStringLocalizer : IStringLocalizer
{
private readonly IDistributedCache _cache;
private readonly JsonSerializer _serializer = new JsonSerializer();
public JsonStringLocalizer(IDistributedCache cache)
{
_cache = cache;
}
public LocalizedString this[string name]
{
get
{
string value = GetString(name);
return new LocalizedString(name, value ?? name, value == null);
}
}
public LocalizedString this[string name, params object[] arguments]
{
get
{
var actualValue = this[name];
return !actualValue.ResourceNotFound
? new LocalizedString(name, string.Format(actualValue.Value, arguments), false)
: actualValue;
}
}
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
{
string filePath = $"Resources/{Thread.CurrentThread.CurrentCulture.Name}.json";
using (var str = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
using (var sReader = new StreamReader(str))
using (var reader = new JsonTextReader(sReader))
{
while (reader.Read())
{
if (reader.TokenType != JsonToken.PropertyName)
continue;
string key = (string)reader.Value;
reader.Read();
string value = _serializer.Deserialize<string>(reader);
yield return new LocalizedString(key, value, false);
}
}
}
private string GetString(string key)
{
string relativeFilePath = $"Resources/{Thread.CurrentThread.CurrentCulture.Name}.json";
string fullFilePath = Path.GetFullPath(relativeFilePath);
if (File.Exists(fullFilePath))
{
string cacheKey = $"locale_{Thread.CurrentThread.CurrentCulture.Name}_{key}";
string cacheValue = _cache.GetString(cacheKey);
if (!string.IsNullOrEmpty(cacheValue)) return cacheValue;
string result = GetValueFromJSON(key, Path.GetFullPath(relativeFilePath));
if (!string.IsNullOrEmpty(result)) _cache.SetString(cacheKey, result);
return result;
}
return default(string);
}
private string GetValueFromJSON(string propertyName, string filePath)
{
if (propertyName == null) return default;
if (filePath == null) return default;
using (var str = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
using (var sReader = new StreamReader(str))
using (var reader = new JsonTextReader(sReader))
{
while (reader.Read())
{
if (reader.TokenType == JsonToken.PropertyName && (string)reader.Value == propertyName)
{
reader.Read();
return _serializer.Deserialize<string>(reader);
}
}
return default;
}
}
}

Note that we are implementing the IStringLocalizer interface.

Line #3: we use IDistributedCache here.

Line #27 - 44: GetAllStrings() here we try to read the JSON file name according to the CurrentCulture, and return a list of LocalizedString objects. Note that this list would contain both the key and values of all the entries in the found JSON file. Each of the read JSON values is deserialized.

Line #45 - 59: GetString() this is the function that is responsible for localizing strings. Here too, the file path is determined in accordance with the current culture of the request. If the file exists, a cache key is created with a pretty unique name. Ideally, the system tries to check if any value exists in the cache memory for the corresponding key. If a value is found in the cache, it is returned. Else, the application accesses the JSON file and tries to get and return the found string.

Line #60 - 78: GetValueFromJSON() as the name suggests, this method accepts the property name and file path of the JSON file, which is then opened in Read Mode. If the corresponding property is found within the JSON file, it is returned. Else a null value would be returned.

Line #9 - 16: this[string name] This is the entry method that we would be using in our controller. It accepts a key and tried to find the corresponding values from the JSON file using the previously explained method. It’s important to note that the method would return the same key if there is no value found in the JSON file.

Next, let’s add a Factory class that would be responsible for internally generating the JsonStringLocalizer instance. Name the new class as JsonStringLocalizerFactory.

public class JsonStringLocalizerFactory : IStringLocalizerFactory
{
private readonly IDistributedCache _cache;
public JsonStringLocalizerFactory(IDistributedCache cache)
{
_cache = cache;
}
public IStringLocalizer Create(Type resourceSource) =>
new JsonStringLocalizer(_cache);
public IStringLocalizer Create(string baseName, string location) =>
new JsonStringLocalizer(_cache);
}

Next comes the interesting part where we create a Middleware that can read the Accept-Language key from the request header and set the language of the current thread if the culture is valid.

Create a new class and name it LocalizationMiddleware.

public class LocalizationMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var cultureKey = context.Request.Headers["Accept-Language"];
if (!string.IsNullOrEmpty(cultureKey))
{
if (DoesCultureExist(cultureKey))
{
var culture = new System.Globalization.CultureInfo(cultureKey);
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
}
}
await next(context);
}
private static bool DoesCultureExist(string cultureName)
{
return CultureInfo.GetCultures(CultureTypes.AllCultures).Any(culture => string.Equals(culture.Name, cultureName,
StringComparison.CurrentCultureIgnoreCase));
}
}

Line #5: Here we read the Accept-Language from the request header of the current HTTP context.

Line #8-13: If a valid culture is found, we set the current thread culture.

With that done, let’s add some language files. Create a new folder named Resources and add in 2 new JSON files. We will be naming the JSON files as en-US.json and de-DE.json.

Here is a sample en-US.json

{
"hi": "Hello",
"welcome": "Welcome {0}, How are you?"
}

Next,de-DE.json. PS, the following was translated via Google Translate.

{
"hi": "Hallo",
"welcome": "Willkommen {0}, wie geht es dir?"
}

As you can see, we have 2 keys, hi and welcome which are meant to be translated by the application depending on the request header.

Now comes the important part where we do the service registrations of our middleware and the JSONLocalizer. Open up Startup.cs and add the following under ConfigureServices method.

services.AddLocalization();
services.AddSingleton<LocalizationMiddleware>();
services.AddDistributedMemoryCache();
services.AddSingleton<IStringLocalizerFactory, JsonStringLocalizerFactory>();

Under the Configure method, add in the following. Note that we are defining en-US as the default culture of our application. You can easily make this configurable by moving it to the appsettings as well.

var options = new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture(new CultureInfo("en-US"))
};
app.UseRequestLocalization(options);
app.UseStaticFiles();
app.UseMiddleware<LocalizationMiddleware>();

Finally, let’s create a new API Controller to demonstrate the usage of our JSON localizer. Add in a new Controller and name it DemoController.

public class DemoController : ControllerBase
{
private readonly ILogger<DemoController> _logger;
private readonly IStringLocalizer<DemoController> _loc;
public DemoController(ILogger<DemoController> logger, IStringLocalizer<DemoController> loc)
{
_logger = logger;
_loc = loc;
}
[HttpGet]
public IActionResult Get()
{
_logger.LogInformation(_loc["hi"]);
var message = _loc["hi"].ToString();
return Ok(message);
}
[HttpGet("{name}")]
public IActionResult Get(string name)
{
var message = string.Format(_loc["welcome"],name);
return Ok(message);
}
[HttpGet("all")]
public IActionResult GetAll()
{
var message = _loc.GetAllStrings();
return Ok(message);
}
}

Line #3-9: Constructor Injection of ILogger and IStringLocalizer instances.

Line #11-16: Here is a simple test where we try to print the localized version of the key ‘hi’ onto the console as well as return it as a response.

Line #17-22: Here, we pass in a random name to this endpoint, and the application is expected to return a localized version of “Welcome xxxx, how are you?“. As simple as that.

Line #23-28: This method would ideally return all the keys and values found in the corresponding JSON file.

Testing with Postman

With that done, let’s fire up Postman and run some basic tests.

Here is what you get as a response when you send a GET request to the /demo endpoint with the Accept-Language as de-DE

json-based-localization-in-aspnet-core

Accept-Language is set to en-US. Simple, yeah?

json-based-localization-in-aspnet-core

Now, I try to send a GET request to the /demo endpoint along with my name. Accept-Language set to en-US

json-based-localization-in-aspnet-core

Accept-Language set to de-DE.

json-based-localization-in-aspnet-core

Finally, when you send a GET request to the /demo/all endpoint, you get to see all the key/value pairs of our strings in the JSON file related to the relevant accept language.

json-based-localization-in-aspnet-core

Pretty handy to have in your projects, right? That’s a wrap for the article.

Summary

In this article, we learnt a rather simple way to achieve JSON Based Localization in ASP.NET Core applications. Do share this article with your colleagues and dev circles if you found this interesting. You can find the source code of these mentioned implementations here. Thanks!

Source Code ✌️
Grab the source code of the entire implementation by clicking here. Do Follow me on GitHub .
Support ❤️
If you have enjoyed my content and code, do support me by buying a couple of coffees. This will enable me to dedicate more time to research and create new content. Cheers!
Share this Article
Share this article with your network to help others!
What's your Feedback?
Do let me know your thoughts around this article.

Mukesh's .NET Newsletter 🚀

Join 5,000+ Engineers to Boost your .NET Skills. I have started a .NET Zero to Hero Series that covers everything from the basics to advanced topics to help you with your .NET Journey! You will receive 1 Awesome Email every week.

Subscribe