Making websites multi-lingual allows users from around the globe to use our applications in their native language. In this article, we are going to see how ASP.NET Core provides us with the ability to enable localization to support different languages and cultures. 

To download the source code for this article, you can visit our GitHub repository.

Let’s get started.

The Difference Between Globalization and Localization

Globalization and Localization are two very closely related terms, so let’s define and understand them.┬á

Globalization is the process of designing applications to support different languages and cultures.

Localization, on the other hand, is the process of adjusting an application for a specific language/culture.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

With these definitions in mind, let’s look at how we can design our applications for┬áglobalization by introducing┬álocalization for specific cultures.

Add Localization to an Application

We’ll start by creating an MVC application. This will allow us to look at the various ways we can localize our applications and serve content based on the user’s locale. Let’s open a command terminal and run dotnet new webapp -n Localization, or we can also use Visual Studio to create a new project.

Resource Files

Resource files are a specific file format (.resx) that allows us to define translations for any plaintext content we want to display in our application, in a key-value pair format. They allow us to separate the localized content from our code, meaning we can easily swap out different resource files for different languages without having to change our code.

File Naming Convention

By convention, resource files should be separated into a separate folder called Resources. When naming these files, they should follow the name of the class that will consume them, as well as include the language they represent.

We can either organize the files into separate folders, such as Resources/Controllers/LocalizationController.es.resx, or leave them in the root Resources folder, ensuring to name them appropriately, in this case Resources/Controllers.LocalizationController.es.resx.

Create Resource Files

For our application, we will follow the folder hierarchy for organizing our resource files. With that in mind, let’s create 2 resource files for the LocalizationController that we will create shortly. One will be in English and another in Spanish.

To start, let’s create a Resources folder, and in that, we’ll add another folder called Controllers.

Now, we add a file called LocalizationController.en.resx, and one called LocalizationController.es.resx.

Let’s open the LocalizationController.en.resx file first and add a new entry:

localization controller english

We’ll do the same for the Spanish resource file, making sure to use the same name:

localization controller spanish

Configure Supported Cultures

Before we can test out the localization of the application, we need to configure our application to register the required services. In the Program class, let’s add localization to our application:

builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");

We need to set the ResourcesPath property to our Resources folder, so ASP.NET Core knows where to look for our resource files.

Also, we can configure the supported cultures for our application:

const string defaultCulture = "en-GB";

var supportedCultures = new[]
{
    new CultureInfo(defaultCulture),
    new CultureInfo("es")
};

builder.Services.Configure<RequestLocalizationOptions>(options => {
    options.DefaultRequestCulture = new RequestCulture(defaultCulture);
    options.SupportedCultures = supportedCultures;
    options.SupportedUICultures = supportedCultures;
});

Here, we define our default culture (en-GB) as well as another supported culture (es).

Finally, we need to tell our application to use these supported cultures:

app.UseRequestLocalization(app.Services.GetRequiredService<IOptions<RequestLocalizationOptions>>().Value);

With our resource files defined and our application configured for localization, let’s look at how we can use them in our application.

IStringLocalizer Interface for Localization

ASP.NET Core provides us with an easy-to-use interface for making our applications localized, IStringLocalizer<T>. This interface uses two classes, ResourceReader and ResourceManager which provides access to culture-specific resources at run-time. We can use dependency injection to gain access to this interface to make the localization of our applications much more straightforward.

Let’s look at how to use this interface in our application.

Controller Localization

Now that we have our resource files created, let’s create the accompanying controller:

public class LocalizationController : Controller
{
    private readonly IStringLocalizer<LocalizationController> _localizer;

    public LocalizationController(IStringLocalizer<LocalizationController> localizer)
    {
        _localizer = localizer;
    }

    public IActionResult Index()
    {
        ViewData["Greeting"] = _localizer["Greeting"];
        return View();
    }
}

We start by injecting the IStringLocalizer<LocalizationController> interface, which will give us access to our English and Spanish resource files.

In the Index() method, we set the ViewData dictionary value for Greeting from our resource file, which will be determined based on the culture of the user. 

If you would like to learn more about ViewData, check out our article on State Management in ASP.NET Core MVC.

Finally, we return to our view, which we’ll create next.

In the Views folder, let’s create a new folder called Localization for our new controller, and create our Index view:

@{
    ViewData["Title"] = "IStringLocalizer";
}

<div class="text-center">
    <p>@ViewData["Greeting"]</p>
</div>

Here, we simply access the Greeting key of the ViewData dictionary.

If we run our application and navigate to /Localization, given that our culture is set to English, we will see the English variation of our Greeting message.

Let’s look at a couple of different methods for changing the culture so we can test whether our localization is working correctly.

Request Culture with Query String

We can manually specify the culture we wish to use by using the culture query string parameter. By default in ASP.NET Core, the QueryStringRequestCultureProvider is registered, allowing us to use this query string parameter in our application.

Let’s try it out. If we navigate to the Localization controller and pass the culture query parameter with the value set to es, we will see the Spanish variation of our greeting:

culture query parameter

Accept-Language Request Header 

The Accept-Language header is another method for specifying the locale the user wishes to use. This is set at a browser level and allows users to set multiple languages, which are weighted based on a quality value from 0-1, letting the browser know which language the user would prefer to use.

IHtmlLocalizer Interface for HTML Localization

What happens if we had some HTML markup in our resource files? Well, if we continued to use the IStringLocalizer interface, we would see the HTML markup unrendered:

raw-html-unrendered

This is not desirable. Fortunately, ASP.NET provides us with a way to render HTML in our resource files correctly, using the IHtmlLocalizer interface.

Let’s change our resource┬áfiles to display our greeting as an h1:

html localization

We need to apply this change to both the English and Spanish resource files.

Now, in our LocalizationController, instead of injecting IStringLocalizer, we inject an IHtmlLocalizer:

public class LocalizationController : Controller
{
    private readonly IHtmlLocalizer<LocalizationController> _localizer;

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

    public IActionResult Index()
    {
        ViewData["Greeting"] = _localizer["Greeting"];
        return View();
    }
}

To use the IHtmlLocalizer interface, we need to register it in our Program class:

builder.Services 
    .AddControllersWithViews()
    .AddViewLocalization();

This time when we navigate to /Localization we will see our greeting rendered as an h1.

View Localization With IViewLocalizer

Instead of localizing at the controller level, we may wish to do so at the View level, which we can do with the IViewLocalizer interface. We can choose to localize strings within the view, similar to how we’ve previously done, or localize the entire view, creating multiple variations for different languages.

Let’s create a new view to demonstrate using the IViewLocalizer interface:

@using Microsoft.AspNetCore.Mvc.Localization

@inject IViewLocalizer ViewLocalizer

<div class="text-center">
    <p>@ViewLocalizer["ViewGreeting"]</p>
</div>

Here, we inject the IViewLocalizer interface from the Microsoft.AspNetCore.Mvc.Localization namespace. Then we retrieve the ViewGreeting value using the ViewLocalizer, which will retrieve it from the required resource file.

Next, we need to create new resource files, matching the name and location of this new view. Let’s create an English and Spanish resource file under the Resources/Views/Localization folder, called LocalizedView.en.resx and LocalizedView.es.resx respectively.

In the English file, we’ll add the ViewGreeting entry with a value of “Hello from a localized view!”. In the Spanish file, we’ll add the same entry, using “┬íHola desde una vista localizada!” for the value.

Running our application and navigating to /Localization/LocalizedView we will see the English variation of our ViewGreeting message. To test the Spanish variation, we can once again append the culture query parameter with a value of es.

Localizing Error Messages With Data Annotations

When we make use of ModelState validation in our applications, we can provide error messages whenever a given input does not meet the property requirements. Fortunately, the ASP.NET Core Localization package accounts for this and allows us to configure localization for our error messages from the Program class:

builder.Services
     .AddControllersWithViews()
     .AddViewLocalization()
     .AddDataAnnotationsLocalization();

Let’s create a model with some data annotations in the Models folder:

public class PersonViewModel
{
    [Display(Name = "FirstName")]
    [Required(ErrorMessage = "{0} is a required field")]
    public string FirstName { get; set; }

    [Display(Name = "LastName")]
    [Required(ErrorMessage = "{0} is a required field")]
    public string LastName { get; set; }

    [Display(Name = "Age")]
    [Range(1, 100, ErrorMessage = "{0} must be a number between {1} and {2}")]
    public int Age { get; set; }
}

Next, we’ll create the Spanish resource file for translating the error messages. In a new folder, Resources/Models, let’s create PersonViewModel.es.resx:

translating error messages

As English is the default language we used when creating the error messages, we don’t need to create an English-specific resource file.

Now we must create the View that will display the PersonViewModel:

@model PersonViewModel
@{
    ViewData["Title"] = "Data Annotations";
}

<div class="card">  
    <div class="card-body">  
        <form asp-action="DataAnnotationView" asp-route-culture=@ViewData["culture"]>  
            <div class="row">  
                <div class="col-md-6">  
                    <div class="form-group">  
                        <label asp-for="FirstName" class="lable-control"></label>  
                        <input asp-for="FirstName" class="form-control" />  
                        <span asp-validation-for="FirstName" class="text-danger"></span>  
                    </div>  
                </div>
            </div>
            <div class="row">
                <div class="col-md-6">
                    <div class="form-group">  
                        <label asp-for="LastName" class="lable-control"></label>  
                        <input asp-for="LastName" class="form-control" />  
                        <span asp-validation-for="LastName" class="text-danger"></span>  
                    </div> 
                </div>  
            </div>
            <div class="row">
                <div class="col-md-6">
                    <div class="form-group">  
                        <label asp-for="Age" class="lable-control"></label>  
                        <input asp-for="Age" class="form-control" />  
                        <span asp-validation-for="Age" class="text-danger"></span>  
                    </div> 
                </div>  
            </div>
            <div class="form-group">  
                <button type="submit" class="btn btn-primary rounded-0 mt-2">Submit</button>  
            </div>  
        </form>  
    </div>  
</div>

Here, we create a simple form that is bound to our PersonViewModel, which will submit to an endpoint we will create next. We include a span for each field with the asp-validation-for attribute, which will display the error message we configured previously if the model is not valid.

As we are using the culture query parameter to determine the locale to use, we need to persist it in ViewData so that we return the same locale after posting the form to the server. We pass this back to the server by defining the asp-route-culture parameter on the form element.

Finally, we need to create the new endpoints that will return this new view:

public IActionResult DataAnnotationView([FromQuery] string? culture)
{
    ViewData["culture"] = culture;
    return View();
}

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult DataAnnotationView(PersonViewModel personViewModel, [FromQuery] string? culture)
{
    ViewData["culture"] = culture;
    if (!ModelState.IsValid)
    {
        return View();
    }

    return View();
}

The first endpoint is a simple GET endpoint. Here we add the culture value to ViewData, so it can be sent in the form submission if present.

Our second endpoint is a POST endpoint that will be used for form submission. Once again we pass the culture parameter to ViewData, and also check if the ModelState is valid. In this instance, we simply return the view, which will display the error messages.

Test Data Annotation Localization

Now we are ready to test this out. If we navigate to /Localization/DataAnnotationView, enter some invalid fields and submit our form, we will see the default, English error messages:

invalid form english

To test out the Spanish version, let’s add the culture query parameter to our URL with the value of es:

empty form spanish

Entering some invalid values once again and submitting, we will see the Spanish variations of our error messages:

invalid form localized

So far we have manually added the culture query parameter to determine the locale of the user. Let’s look at an alternative method using cookies.

Cookie-Based Localization Preferences

We can store a user’s locale in a Cookie which means we don’t have to pass it through the query parameter manually and allows us to set the culture programmatically. This would be the preferred method, as we can store the user’s locale between sessions, meaning they will return to the same locale each time.

Let’s start by creating a dropdown that we will add to our navigation bar to allow the user to select a language:

@using Microsoft.AspNetCore.Localization
@using Microsoft.Extensions.Options

@inject IOptions<RequestLocalizationOptions> LocalizationOptions

@{
    var requestCulture = Context.Features.Get<IRequestCultureFeature>();

    var cultureItems = LocalizationOptions.Value.SupportedUICultures
        .Select(c => new SelectListItem { Value = c.Name, Text = c.Name })
        .ToList();

    var returnUrl = string.IsNullOrEmpty(Context.Request.Path) 
        ? "~/" 
        : $"~{Context.Request.Path.Value}{Context.Request.QueryString}";
}

<div>
    <form asp-controller="Culture" asp-action="SetCulture" asp-route-returnUrl="@returnUrl" 
          class="form-horizontal nav-link text-dark">
          <select name="culture" 
                onchange="this.form.submit();" 
                asp-for="@requestCulture.RequestCulture.UICulture.Name" 
                asp-items="cultureItems">
          </select>
    </form>
</div>

First, we inject IOptions<RequestLocalizationOptions> which allows us to access the configured cultures for our application.

Then, in the code block, we get these supported cultures to bind them to our dropdown. Also, we define a returnUrl route, so that whenever we change the language, we return to the same page the user was currently on, and not the default page.

Finally, we add a form with our support languages dropdown that will submit to the SetCulture method of the Culture controller, which we will define shortly.

We must remember to add this partial view to _Layout.cshtml:

<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
    <ul class="navbar-nav flex-grow-1">
        <li class="nav-item">
            @await Html.PartialAsync("_CulturePartial")
        </li>
    </ul>
</div>

Now it’s time to add the controller that will set the culture in the cookie. We’ll start by creating a new CultureController, along with the single method required:

public class CultureController : Controller
{
    [HttpPost]
    public IActionResult SetCulture(string culture, string returnUrl)
    {
        Response.Cookies.Append(
            CookieRequestCultureProvider.DefaultCookieName,
            CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
            new CookieOptions { Expires = DateTimeOffset.UtcNow.AddDays(30) }
        );

        return LocalRedirect(returnUrl);
    }
}

We have a single POST method, SetCulture() which is used from our dropdown when the value changes. In this method, we add a cookie to the Response with the selected culture, setting the expiry time to 30 days in the future. Then, we redirect the user to the returnUrl, which will be the URL they were currently on.

Test Cookie-Based Localization

Our cookie-based localization is now ready to test. If we run our application, we will see the dropdown on the navigation bar:

culture select dropdown

If we select es from the dropdown and navigating through the application again, we see all the language is returned in Spanish. We no longer have to manually set the culture by using a query parameter. The application remembers our preference when we navigate away from our application and return.

Conclusion

In this article, we’ve learned what Localization is in an ASP.NET application. We understood how to make our application multi-lingual by using resource files and various interfaces ASP.NET Core provides us. Localization is a powerful tool that enables us to reach a wider audience with our applications, providing each culture with a personalized experience suited to them. We’ve covered a lot of MVC features in this article.

If you’d like to learn more about MVC, check out our ASP.NET Core MVC Series.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!