Introduction
As we know, Globalization is the process of designing and developing applications that function for multiple cultures, and Localization is the process of customizing your application for a given culture and locale.
In this article, we will dig into the extensible points that localization in ASP.NET Core 1.0 offers for the developers. As much as I know about the localization in the new version of ASP.NET, there are two main extensible points that I will describe underneath, localization Culture Providers & Localization Resources.
Background
Localization, and ASP.NET Core 1.0 knowledge required.
Using the Code
1. 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<ProviderCultureResult>
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<startup> localizer)
{
var supportedCultures = new List<cultureinfo>
{
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.
2. 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<Resource> 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<culture> Cultures { get; set; }
public DbSet<resource> 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<LocalizedString> 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<T> : IStringLocalizer<T>
{
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<LocalizedString> 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<Resource>()
{ new Resource { Key = "Hello", Value = "Hello" } }
},
new Culture
{
Name = "fr-FR",
Resources = new List<Resource>()
{ new Resource { Key = "Hello", Value = "Bonjour" } }
},
new Culture
{
Name = "es-ES",
Resources = new List<Resource>()
{ new Resource { Key = "Hello", Value = "Hola" } }
},
new Culture
{
Name = "jp-JP",
Resources = new List<Resource>()
{ new Resource { Key = "Hello", Value = "?????" } }
},
new Culture
{
Name = "zh",
Resources = new List<Resource>()
{ new Resource { Key = "Hello", Value = "??" } }
},
new Culture
{
Name = "zh-CN",
Resources = new List<Resource>()
{ 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<IStringLocalizerFactory, EFStringLocalizerFactory>();
}
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.
Points of Interest
Working with localization APIs in ASP.NET Core 1.0 is quite cool, because the new ASP.NET world simplify the localization stuff in such a way that we have never seen before, it's more simple, clean and straightforward. I find myself enjoying when I wrote the code.