Localization in ASP.NET 5 & MVC 6

Localization in ASP.NET 5 & MVC 6

ASP.NET 5 is a pretty revolutionary version - it's a complete rewrite, and an open source effort. They say that the reason for the ground-up redesign is the fact that the world has changed, and it has. We now have micro-services, on-premise servers that do not have to be IIS, and it can even run on Linux & Mac. In essence, Microsoft had to remove all dependencies to System.Web, and once they started doing that, well... I guess it was just easier to rewrite it from scratch :-).

And, because the entire framework is new, there are a lot of new concepts, and localization is just one of them.

I'll talk about the insides of ASP.NET 5 and MVC 6 at Cancel Conference on the 29th and 30th September in Šempeter pri Gorici. Come listen!

Why a new localization approach?

At the moment, ASP.NET localization approach is quite old, and it is based on Resources. There is nothing inherently wrong with this, but a lot of projects I have seen so far actually use other sources for localized strings. For example, most products I've worked on recently use a database as the source. Also, the concepts in place right now work badly with Dependency Injection.

So, enter the new localization.

This is all pre-release code, based on Beta 7, so it might yet change drastically, making this blog post irrelevant.

The basic Concepts - Middleware & Feature

In the new ASP.NET framework, interaction with the hosting component (server), is done through abstractions called Features. The first major part is IRequestCultureFeature . The default implementation of that uses an implementation of IRequestCultureProvider. There are a couple implementations built-in:

  • CookieRequestCultureProvider
  • QueryStringRequestCultureProvider
  • AcceptLanguageHeaderRequestCultureProvider

All of those look at different things each, and provides the proper culture to the localization feature. The providers are listed in RequestLocalizationOptions , which provides an ordered list that can be modified. The first provider that returns a non-null result for a given request "wins" and that result is used. This process is performed by RequestLocalizationMiddleware . The middleware is activated by calling the UseRequestLocalization  extension method on top of an instance of IApplicationBuilder .

Additionally, the middleware also sets the thread's culture (and UI Culture).

You can access the culture from mapped calbacks throughout the pipeline:

var requestCultureFeature = context.Features.Get<IRequestCultureFeature>();
var requestCulture = requestCultureFeature.RequestCulture;

The Basic Concepts - The Factory and Provider

Of course, none of this really matters, if you can't access the resources (translations). Asp.net localization framework introduces IStringLocalizerFactory which is a way to create (hence the name - factory) actual IStringLocalizer  instances. This gives us a simple option to extend this.

The factory interface is really simple:

public interface IStringLocalizerFactory
{
    IStringLocalizer Create(Type resourceSource);
    IStringLocalizer Create(string baseName, string location);
}

The actual localizer is also simple:

public interface IStringLocalizer
{
    LocalizedString this[string name] { get; }
    LocalizedString this[string name, params object[] arguments] { get; }
    IEnumerable<LocalizedString> GetAllStrings(bool includeAncestorCultures);
    IStringLocalizer WithCulture(CultureInfo culture);
}

The factory then, only knows how to create an instance of the localizer. This is invoked by the framework itself (the DI framework underneath requires the factory to be registered, but not the localizer) and then passed onward to the components that require it.

What is much better now is the fact that we can override the localizer and access our translations from the database. Because the factory is instantiated through DI, we can even use our services and repositories for accessing the database - and pas this to the localizers when we instantiate it.

The Next Step - MVC

Now we need to use all of this on the actual UI. MVC introduces a couple more classes, most importantly IHtmlLocalizer  and the corresponding factory IHtmlLocalizerFactory. This is registered with the DI and services container with the extension method AddViewLocalization() on top of IMvcBuilder. This actually does a couple other things that we'll get to later.

The provided implementation of this interface has a dependency to the aforementioned IStringLocalizerFactory . The html variant actually provides a way to get HTML strings back. The intended usage is to be injected either into the controller, like this:

private IHtmlLocalizer<HomeController> _localizer;


public HomeController(IHtmlLocalizer<HomeController> localizer)
{
    _localizer = localizer;
}


public ActionResult Index()
{
    ViewData["Message"] = _localizer["This is a message."];
    return View();
}

or it can be injected into a view (or all of them). For example, we can add the following into _ViewImports.cshtml.

@inject IViewLocalizer LocString

The @inject  keyword is amazing. It basically lets us use DI services in the View. But in this case, it gives us access to LocString across all our views. By the way, the interface is simply a derivative of the IHtmlLocalizer. I'm assuming it's a new one only to enable expanding it with additional info later down the line.

Anyway, inside our views, we can now do:

@LocString["Hello Cancel Conference"]Update: I asked on Twitter what is the purpose of the IViewLocalizer, and this is the answer:

https://twitter.com/DamianEdwards/status/645681836086063104

The View Localization Problem

Sometimes, simply changing the message is not enough localization. You actually want to display a different "view". This was really hard to achieve without a custom view engine in the past. With the introduction of View Location Expanders (more on this, hopefully in a later post), this is now much easier, and much more efficient.

When you call the AddViewLocalization()  extension method, this also happens:

services.Configure<RazorViewEngineOptions>(
options =>
{
    options.ViewLocationExpanders.Add(new LanguageViewLocationExpander(format));
},
DefaultOrder.DefaultFrameworkSortOrder);

This enables the view engine to look for views that are suffixed with the culture information, so for example, I can have Index.cshtml, and Index.sl-SI.cshtml. If the request localization middleware and feature set the culture to sl-SI, the view engine will try and find the view suffixed with that and load it - or fallback to the default view.

Tying it all together

By default, MVC, is implemented to use the default resource providers, so it will, as Damian answered above, look for resource files that are named like the view. So, for example, you can have Views.Home.Index.cshtml.en-GB.resx . This will be picked up by default. So, this is the easiest way to try it out.

Another way is to implement your own IStringLocalizerFactory  and IStringLocalizer . Just remember to register the factory in the ConfigureServices  method:

services.AddTransient<IStringLocalizerFactory, TestStringLocalizerFactory>();

I will publish some demos and samples after the Cancel conference.

Show Comments
Mastodon Verification