Creating a json resource reader for dotnet core
Tags: csharp
Published: 2021-07-15 20:38:35



In this post, we're going to create a custom Resource reader to use with dotnet core libraries. In the end, we can have a project dedicated to resources.

First, we need a class that represents our `json` file:

```csharp
using System.Collections.Generic;

namespace I18N
{
    internal class JsonLocalization
    {
        public string Key { get; set; }
        public Dictionary<string, string> LocalizedValues { get; set; }
    }
}
```

The `Key` is an unique identifier for the localization and `LocalizedValues` is a dictionary, which its key is the language and the value the text that must be displayed.

We also going to create a new type of `Exception`, to easily pinpoint what went wrong with the application.

```csharp
using System;

namespace I18N
{
    public class I18NException : Exception
    {
        public I18NException(string message) : base(message)
        {
        }

        public I18NException(string message, Exception innerException) : base(message, innerException)
        {
        }

        public I18NException()
        {
        }
    }
}
```

Here is where the magic happens, the `JsonLocalizer` class will read our `json resources files`, store them in memory and make them available to our application.

In our constructor we expect two parameters, `useBase` and `additionalPaths`.

If `useBase` is set to true, the localizer will load `*.json` files that are in the `Resources` folder.
`additionalPaths` uses a type as a key, the localizer will use this type to find the assembly path and read the `*.json` files in the `Resources` folder.

```csharp
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Newtonsoft.Json;

namespace I18N
{
    public class JsonLocalizer
    {
        private readonly Dictionary<string, JsonLocalization[]> _localization
            = new Dictionary<string, JsonLocalization[]>();

        public JsonLocalizer(bool useBase = true, Dictionary<Type, string> additionalPaths = null)
        {
            if (useBase)
                PopulateLocalization("Resources");

            if (additionalPaths == null) return;
            foreach (var additional in additionalPaths)
            {
                var codeBase = additional.Key.Assembly.CodeBase;
                var uri = new UriBuilder(codeBase);
                var data = Uri.UnescapeDataString(uri.Path);
                var path = Path.GetDirectoryName(data);
                var fullPath = Path.Combine(path, additional.Value);
                PopulateLocalization(fullPath);
            }
        }

        /// <summary>
        /// resource:key:culture
        /// resource is the resource name
        /// key is the key you're looking for
        /// culture is optional
        /// </summary>
        /// <param name="key"></param>
        public string this[string key] => GetString(key);


        private void PopulateLocalization(string path)
        {
            foreach (var resource in Directory.GetFiles(path, "*.json", SearchOption.AllDirectories))
            {
                try
                {
                    var fileInfo = new FileInfo(resource);
                    var fileName = fileInfo.Name.Substring(0, fileInfo.Name.IndexOf('.'));
                    var loc = JsonConvert.DeserializeObject<JsonLocalization[]>(File.ReadAllText(resource));
                    _localization.Add(fileName, loc);
                }
                catch (ArgumentException e)
                {
                    throw new I18NException($"Resource {resource} was already added, check your files.", e);
                }
                catch (Exception ex)
                {
                    throw new I18NException("Something wrong is not right, check inner exception", ex);
                }
            }
        }

        private string GetString(string query)
        {
            try
            {
                string culture = null;

                var split = query.Split(':');
                var resource = split[0];
                var key = split[1];
                if (split.Length > 2)
                    culture = split[2];

                culture = culture ?? CultureInfo.CurrentCulture.Name;

                return _localization
                    .Single(l => l.Key == resource)
                    .Value.Single(x => x.Key == key)
                    .LocalizedValues[culture];
            }
            catch (Exception ex)
            {
                throw new I18NException($"Couldn't find key: {query}", ex);
            }
        }
    }
}
```

In `dotnet core` applications, you can add the `JsonLocalizer` using the `IServiceCollection` in the `ConfigureServices` method.

```csharp
// use it in DI as a singleton
public void ConfigureServices(IServiceCollection services)
{
   // Other configurations ...
   services.AddSingleton<JsonLocalizer>();
}
```

To handle `additionalPaths`

```csharp
var additional = new Dictionary<Type, string>
	         {
	            { typeof(MyClass), "My Resource Folder" },
	            { typeof(MyAnotherClass), "My Resource Folder/Even Handles sub folders" }
	         };

var withExternalSources = new JsonLocalizer(additionalPaths: additional);
```

Now that we have everything set up, we can start using our localizer:

```csharp
private readonly JsonLocalizer _localizer;

public class MySampleClass(JsonLocalizer localizer)
{
   _localizer = localizer;
}

public string GetLocalizedMessage()
{
   return _localizer["MyAppResource:MyKey"];
}
```

The Localizer will find your text by: 

`FileName:Key:Language`

Here are some examples of how to write your resource files:

| File Name                  | Resource Name |
| -------------------------- | ------------- |
| MyResource.json            | MyResource    |
| MyApp.Resource.json        | MyApp         |
| MyApp-Errors.Resource.json | MyApp-Errors  |
| MyApp.Errors.Resource.json | MyApp         |

The `Key` is the key inside the resource file, and the `Language` is the culture, if not informed, will use the `CultureInfo.CurrentCulture` value.

The `json` resource file should follow this format:

```json
[
   {
      "Key":"Name",
      "LocalizedValues":{
         "en-US":"Name",
         "pt-BR":"Nome"
      }
   },
   {
      "Key":"Age",
      "LocalizedValues":{
         "en-US":"Age",
         "pt-BR":"Idade"
      }
   }
]
```