Localization Extensibility in ASP.NET Core 1.0

Feb 28, 2016     Viewed 6838 times    5 Comments
Posted in #Localization 

Localization Culture Providers

ASP.NET Core 1.0 came up with five providers that can determine the current culture of the web application:

  • AcceptLanguageHeaderRequestCultureProvider
  • CookieRequestCultureProvider
  • CustomRequestCultureProvider
  • QueryStringRequestCultureProvider
So, it can determine the culture either from cookies, http header or query string. And if we have a look to the source code in the localization repository, we will notice that all the providers inherit from RequestCultureProvider. The developer can easily use this extensible point to create a new culture provider that retrieves the culture from custom source by inheriting from the previous class.

In this article, I'm going to create a new culture provider called ConfigurationRequestCultureProvider which retrieves the culture from configuration file (JSON) in our case. As I mentioned before, we need to inherit from RequestCultureProvider and we will get the culture from the configuration file using the configuration APIs as the following:

public class ConfigurationRequestCultureProvider : RequestCultureProvider
{
    public override Task 
    DetermineProviderCultureResult(HttpContext httpContext)
    {
        if (httpContext == null)
        {
            throw new ArgumentNullException(nameof(httpContext));
        }

        var builder = new ConfigurationBuilder();
        builder.AddJsonFile("config.json");

        var config = builder.Build();
        string culture = config["culture"];
        string uiCulture = config["uiCulture"];

        culture = culture ?? "en-US";
        uiCulture = uiCulture ?? culture;

        return Task.FromResult(new ProviderCultureResult(culture, uiCulture));
    }
}

As we saw before, we get cuture information using culture & uiCulture keys which are defined in the json configuration file below:

    {
  "culture": "ar-SA"
  "uiCulture": "ar-SA"
}

After that, we need to add the new provider into RequestCultureProviders property which is available in RequestLocalizationOptions class.

public void Configure(IApplicationBuilder app, IStringLocalizer localizer)
{
    var supportedCultures = new List
    {
        new CultureInfo("en-US"),
        new CultureInfo("ar-SA")
    };

    var options = new RequestLocalizationOptions()
    {
        DefaultRequestCulture = new RequestCulture("en-US"),
        SupportedCultures = supportedCultures,
        SupportedUICultures = supportedCultures
    };

    options.RequestCultureProviders.Insert(0, new JsonRequestCultureProvider());
    app.UseRequestLocalization(options);
    ...
}   

You can find the source of the above sample on the ASP.NET Entropy repository.

Localization Resources

The second point that the ASP.NET developer may use to extend the localization is specifying the localization entries aka Resource, which a key/value pair, that contains all the required entries with their translation for a specific culture. ASP.NETCore 1.0 out-of-the-box uses the old source .resx files to store the culture specific entries, but give you the ability to switch into your custom storage such as XML, JSON, .. etc., to retrieve the localization entries.

In this article, we will use a memory storage using EntityFramework, to store the localization entries. I will not dive into much details about EF, so we will start building our needed models Culture and Resource.

public class Culture
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual List Resources { get; set; }
}
public class Resource
{
    public int Id { get; set; }
    public string Key { get; set; }
    public string Value { get; set; }
    public virtual Culture Culture { get; set; }
}

After that, we define the DataContext

public class LocalizationDbContext : DbContext
{
    public DbSet Cultures { get; set; }
    public DbSet Resources { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseInMemoryDatabase();
    }
}

At this point, finish defining the required objects for the data store, now we will start to use the extensible point by implementing the following interfaces IStringLocalizer, IStringLocalizer <T>, IStringLocalizerFactory.

public class EFStringLocalizer : IStringLocalizer
{
    private readonly LocalizationDbContext _db;

    public EFStringLocalizer(LocalizationDbContext db)
    {
        _db = db;
    }

    public LocalizedString this[string name]
    {
        get
        {
            var value = GetString(name);
            return new LocalizedString(name, value ?? name, resourceNotFound: value == null);
        }
    }

    public LocalizedString this[string name, params object[] arguments]
    {
        get
        {
            var format = GetString(name);
            var value = string.Format(format ?? name, arguments);
            return new LocalizedString(name, value, resourceNotFound: format == null);
        }
    }

    public IStringLocalizer WithCulture(CultureInfo culture)
    {
        CultureInfo.DefaultThreadCurrentCulture = culture;
        return new EFStringLocalizer(_db);
    }

    public IEnumerable GetAllStrings(bool includeAncestorCultures)
    {
        return _db.Resources
            .Include(r => r.Culture)
            .Where(r => r.Culture.Name == CultureInfo.CurrentCulture.Name)
            .Select(r => new LocalizedString(r.Key, r.Value, true));
    }

    private string GetString(string name)
    {
        return _db.Resources
            .Include(r => r.Culture)
            .Where(r => r.Culture.Name == CultureInfo.CurrentCulture.Name)
            .FirstOrDefault(r => r.Key == name)?.Value;
    }
}

As we saw from the previous code, all the needed functions are implemented to fetch the localization values from the memory using an object of LocalizationDbContext class that we defined previously.

In the same way, we can implement the generic version of the IStringLocalizer.

public class EFStringLocalizer : IStringLocalizer
{
    private readonly LocalizationDbContext _db;

    public EFStringLocalizer(LocalizationDbContext db)
    {
        _db = db;
    }

    public LocalizedString this[string name]
    {
        get
        {
            var value = GetString(name);
            return new LocalizedString(name, value ?? name, resourceNotFound: value == null);
        }
    }

    public LocalizedString this[string name, params object[] arguments]
    {
        get
        {
            var format = GetString(name);
            var value = string.Format(format ?? name, arguments);
            return new LocalizedString(name, value, resourceNotFound: format == null);
        }
    }

    public IStringLocalizer WithCulture(CultureInfo culture)
    {
        CultureInfo.DefaultThreadCurrentCulture = culture;
        return new EFStringLocalizer(_db);
    }

    public IEnumerable GetAllStrings(bool includeAncestorCultures)
    {
        return _db.Resources
            .Include(r => r.Culture)
            .Where(r => r.Culture.Name == CultureInfo.CurrentCulture.Name)
            .Select(r => new LocalizedString(r.Key, r.Value, true));
    }

    private string GetString(string name)
    {
        return _db.Resources
            .Include(r => r.Culture)
            .Where(r => r.Culture.Name == CultureInfo.CurrentCulture.Name)
            .FirstOrDefault(r => r.Key == name)?.Value;
    }
}

The IStringLocalizerFactory interface is responsible to create an object of IStringLocalizer.

public class EFStringLocalizerFactory : IStringLocalizerFactory
{
    private readonly LocalizationDBContext _db;

    public EFStringLocalizerFactory()
    {
        _db = new LocalizationDBContext();
        _db.AddRange(
            new Culture
            {
                Name = "en-US",
                Resources = new List() 
                { new Resource { Key = "Hello", Value = "Hello" } }
            },
            new Culture
            {
                Name = "fr-FR",
                Resources = new List() 
                { new Resource { Key = "Hello", Value = "Bonjour" } }
            },
            new Culture
            {
                Name = "es-ES",
                Resources = new List() 
                { new Resource { Key = "Hello", Value = "Hola" } }
            },
            new Culture
            {
                Name = "jp-JP",
                Resources = new List() 
                { new Resource { Key = "Hello", Value = "?????" } }
            },
            new Culture
            {
                Name = "zh",
                Resources = new List() 
                { new Resource { Key = "Hello", Value = "??" } }
            },
            new Culture
            {
                Name = "zh-CN",
                Resources = new List() 
                { new Resource { Key = "Hello", Value = "??" } }
            }
        );
        _db.SaveChanges();
    }

    public IStringLocalizer Create(Type resourceSource)
    {
        return new EFStringLocalizer(_db);
    }

    public IStringLocalizer Create(string baseName, string location)
    {
        return new EFStringLocalizer(_db);
    }
}

Last but not least, instantiation of EFStringLocalizerFactory is required in the localization middleware, to let the localization use the customized localization resource.

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton();
}

Finally, using the created object via Dependency Injection.

public void Configure(IApplicationBuilder app, IStringLocalizerFactory localizerFactory)
{
    var localizer = localizerFactory.Create(null);
    ...
}

You can find the source of the above sample on the ASP.NET Entropy repository.

For more information about localization, you can have a look at the source code on the ASP.NET Localization repository.

Twitter Facebook Google + LinkedIn


5 Comments

Basel Juma (12/10/2016 10:35:33 AM)

I feel that here:

options.RequestCultureProviders.Insert(0, new JsonRequestCultureProvider());

should be :


options.RequestCultureProviders.Insert(0, new ConfigurationRequestCultureProvider());

Basel Juma (12/10/2016 1:55:03 PM)

can we use the appsettings.json as dependency injection instead of using config.json

Phil Jollans (2/18/2017 11:49:25 PM)

Hi Hisham,

I downloaded the Entropy project, but I cannot load it into Visual Studio 2015 or 2017 RC.

The project files appear to be in a new format, which starts with


Does this require an extension to Visual Studio?

(I'm not really an ASP expert, so this is unfamiliar to me.)

Phil

Phil Jollans (2/19/2017 8:40:03 AM)

Hi Hasham,
I have installed a more recent version of Visual Studio 2017 RC and I can now open the Entropy project in Visual Studio 2017.
I probably reacted too quickly, adding a comment to your page. I was not sure (and I still am not sure) who is responsible for this repository. I had assumed that it was your repository, but now I guess that was incorrect.
Phil

Jack (3/28/2017 7:31:47 AM)

Hi! To speed up things with localization, my suggestion is to have a look at this online localization service https://poeditor.com. It is integrated with Github and Bitbucket, and has many automation features.


Leave a Comment