In this episode, we’ll use ASP.NET Core internationalization support to make our authentication service available in multiple languages.
For the walk-through you can check out the next video, but if you prefer a quick read, skip to the written synthesis.
The playlist for the whole series is here.
Since we’re playing around with Razor Pages, it’s a good opportunity to explore how we can prepare the application to support multiple languages. This also applies to regular MVC.
Quick note: when I use i18n in the post, it means internationalization. That’s because it starts with an
i, ends with an
n and has 18 characters in between. Not my idea! 😛
Configure services and middlewares for i18n
The first thing we need to do is to configure the services and middlewares to handle internationalization.
Let’s begin with the services. In
Startup.ConfigureServices, we’ll add 3 things:
- Add localization services
- Tell MVC we want to use certain aspects of localization (which also stands for Razor Pages, as its coupled to MVC)
- Add a configuration for the languages/cultures we’ll use
(1), we register the required services by calling the extension method. In the options of the method, we’re indicating the location of our resource files, which we’ll put in the
Resources folder, on the project’s root.
(2), we’re providing MVC with some extra info regarding localization.
AddViewLocalization configures the requirements to use localization in views (for instance, being able to use
IViewLocalizer as we’ll see in a bit) and
AddDataAnnotationsLocalization has the same goal, but regarding data annotations on our view models.
(3), we’re adding to the configuration a
RequestLocalizationOptions instance. This object is used by the middleware we’ll be registering in a moment, so we could just pass it there, but since we need a list of available cultures to present to the user, we can just register it as a configuration and use it as the source of those cultures.
On the middleware side, we just need to register a new one, responsible for setting the culture of the request based on some of its properties.
By default, the middleware checks for the culture in the query string, cookies and accept language header, in this order. This order can be changed and we can even add custom providers to get the request’s culture. The ones that come out of the box (and correspond to what the middleware uses by default) are
Using resource files
To store our translated strings we’ll use resource files (
*.resx), as it’s what has support out of the box. Resource files are
XML that keep the strings associated with a key so we can fetch them.
Here is an example of an entry in a resource file:
We can create custom resource providers, and maybe in the future we should take a look at that, to create something simpler, maybe with
JSON. Not that I have a problem with
XML, but these resource files are a bit convoluted and a pain to work with outside of Visual Studio (for instance in Rider or VS Code) where there is a dedicated editor for
resx file name convention
Not mandatory, but normally we associate a resource file with a specific view/controller/page/etc, to keep things organized and avoid massive resource files, but we’ll see this in a moment. Besides that, part of the file name should indicate the culture the resource represents.
If we don’t specify a culture, the resource file will be treated as the default one, being used when a supported culture is not matched to a specific file. If we want to specify the culture we can do something like
SomeResource.pt.resx. The first one will match specifically requests for portuguese from Portugal, while the second one is more generic, so both regular portuguese and brazilian portuguese (pt-BR) will use that file.
View specific resources
Like briefly mentioned, the common approach is to use multiple resource files, normally associating them with specific views/pages/controllers/etc. To make the association we use naming conventions.
Let’s start by creating adding i18n support to our login page, starting with the view. In the root of the project we should have a folder named
Resources, as mentioned when adjusting the
ConfigureServices method. In this folder we will reproduce the folder structure of our pages, so we add another folder called
Pages. In here we can create a couple of resource files, to support the cultures we configured, so
Login.pt.resx, matching the view’s name, which is
In the login view, we have a
Forgot your password? string we can extract to the resource file. In the resource files, we add a new entry with the key
ForgotPassword and the text
Forgot your password? for the english resource,
Esqueceu a palavra passe? for the portuguese one.
Note: Another approach is to keep the english text in the page (as it’s the default culture) and create only resource files for the alternate cultures, using the default culture’s text as the key. I’m not a fan of that approach because if we want to adjust the text, we then need to adjust the keys in all the resource files (or worse, we forget we have to do that). Using a dedicated key, that’s less of a problem.
Now we need to make the view use this new resource. At the top of
Login.cshtml, we add a new line “injecting” an
IViewLocalizer we can use to fetch the localized strings.
Where the text is, we replace with
@Localizer["ForgotPassword"]. Now if we make a request we’ll get the english text (unless you have the accept language set to portuguese). To see the portuguese text show up, we can add
?culture=pt to the query string (we’ll take care of allowing the user to change the culture later.).
Page model specific resources
Page model specific resources have much in common with the view resources. In
Resources/Pages we create a couple of new resource files, named
LoginModel.pt.resx, which match the name of our page model class,
LoginModel. In these new files, for now, we can add a single entry, with key
InvalidLoginAttempt and values
Invalid login attempt. for english,
Tentativa de login inválida. for portuguese.
To make use of this, in the
LoginModel constructor we add a new
IStringLocalizer<LoginModel> parameter. This injected parameter will be associated with the created resource files, so we can use it to fetch our strings.
Now where we used the
Invalid login attempt. string, we can replace with the usage of the injected localizer.
Page model inner classes specific resources (with DataAnnotations)
As you’re probably starting to see by now, there’s a pattern to get the resource files and classes/views to match up. This is not different for inner classes of page models, but there was something about the naming that took me a while to figure out. We’ll get there in a minute, first let’s see the class, one of those
InputModels we created for the pages, in this case for the
The only change to the class is the text was
Remember me? and now is
RememberMe, so it’s a better key for the resource files.
All we need to do now is create the resource files like in the other cases, so when the page is rendered the correct string is used. The question is, what should be the name of the file? After scouring the web and a lot of trial and error, finally figured out the files should be named
Side note: on this search even bumped into a similar unanswered question on Stack Overflow (as foretold by xkcd) and was able to help out (even if over 6 months later 😛).
With this precious piece of information, we can get it over with, creating the required resource files and adding the text we want for english and portuguese.
Besides associating resource files with specific views/pages/controllers/etc, there may also be cases where we just want some common string we use in multiple places. The setup for this is a bit weird, but it isn’t hard to get working.
Resources folder root, we create a new class called
SharedResource. The class will remain empty, it’s just going to be used so we have a way to reference the resource files, given they’re not associated with a specific item.
Next to the new class’ file, we can create the resource files, named
SharedResource.pt.resx. If you’re using Visual Studio, you’ll notice it groups the files as if it was one in the solution explorer, and you can expand with a click on the little arrow (same as, for instance,
Now we have a bunch of ways to use the shared resource. For the simple test I was doing in this application, I simply used it to log something in the
OnGet method of the
LoginModel class. To access the resource, in the constructor we add an
IStringLocalizer<SharedResource> parameter, so it is injected by the framework. Using it is the same as in the other previous examples, so
_sharedLocalizer["SampleSharedString"] gets us the string we added to the resource file.
Although I didn’t use it in this application, a quick glance at the docs shows us other possibilities, like using it in views and data annotations.
In the case of the views, we get the shared localizer by adding it to the top of the page like
@inject IHtmlLocalizer<SharedResource> SharedLocalizer, then using it as previously shown.
For data annotations it’s a bit more work. As we’ve seen previously, we have the annotations automatically associated with the resource (as long as we get the names right). To use the shared resources, we need to override the way the resources are associated with the annotations.
Startup class, when we call
AddDataAnnotationsLocalization we need do extra configuration (from the docs):
By doing this, we’re overriding the way the data annotations and the resources are paired up, in this case by always using the shared resource, but we could use some other logic if we wanted. It doesn’t seem possible to mix in the same class this and the previous approach though.
Store culture preference
Now that we have i18n mostly configured, played with resource files and used them in different ways, let’s take a look at allowing the user to select the desired culture.
We’re going to create a select box for the user to select the culture, POSTing the selection to a controller that stores it in a cookie which is then used in every request by the
CookieRequestCultureProvider we talked about earlier, to set the culture we should use to render our response. As usual, there are a lot ways to achieve the same result, this is just a simple possibility (maybe if SEO is a concern, having the culture on the route is a better idea?).
Let’s start by creating a new controller named
CultureController. It will have a single action, that’ll receive the user selected culture (and an url to get back to after setting the preference).
Nothing too fancy going on in the controller, where we’re simply adding a cookie to the response with the culture preference. We use
CookieRequestCultureProvider to get the cookie name the provider will look for when parsing the requests, and create the cookie value in the format the provider expects to read. The last argument, is simply setting the cookie duration to one year.
To present the user the possibility to select the culture, we’ll create a partial view with a select box.
Although not complicated, there are a bunch of things in this partial we can take a closer look.
For starters, we’re injecting the
IOptions<RequestLocalizationOptions> we talked about in
Startup.ConfigureServices to get the cultures to show to the user.
With the supported cultures, we can create a list of
SelectListItem which we pass to the select box tag helper as content, using the
asp-items attribute. The select item text is localized, getting the info from
Resources/Pages/Shared/_SelectCulturePartial.en.resx (and its
The rest is a typical form, submitting the culture when the selected value changes.
To wrap up, we head to
_Layout.cshtml file, and use the partial view by adding the line
@await Html.PartialAsync("_SelectCulturePartial") in there.
That does it for this quick look at internationalization in ASP.NET Core. As usual there’s a lot more to explore, being the docs a great place to get more info on the subject.
Links in the post:
The source code for this post is here.
Sharing and feedback always appreciated!
Thanks for stopping by, cyaz!