Step-by-step tutorial to build multi-cultural Asp.Net Core web app

Ziya Mollamahmut
The Startup
Published in
10 min readJun 18, 2019

UPDATE — 14.Nov.2020: This tutorial is based on “LazZiya.ExpressLocalization” nuget that was developed in early 2019. Recently, I’ve developed a new localization nuget named “XLocalizer”, it is easier to configure and has more powerful capabilities like localization powered by online translation, auto resource creating and more. Click here to go to the new XLocalizer tutorial, or keep reading below for the older nuget.

Introduction

Localization/globalization in ASP.NET Core requires a lot of infrastructure setup and it consumes time and effort. In this article, I’ll show how to easily localize Asp.Net Core web applications using a nuget package called LazZiya.ExpressLocalization.

Background

Most of the web apps use URL based localization, so we can see the selected culture in the URL, e.g., http://www.example.com/en/Contact. ASP.NET Core provides below request culture providers by default:

  • QueryStringRequestCultureProvider
  • CookieRequestCultureProvider
  • AcceptLanguageHeaderRequestCultureProvider

In order to have route value localization, we will build custom localization provider and define a global route template.

Basically, we need to complete the below steps to build the infrastructure of a localized web application in Asp.Net Core. But using LazZiya.ExpressLocalization we need only few of them (marked with *):

  • *Configure localization in startup
    Define supported cultures, configure localization options and use request localization middleware.
  • Build route value request culture provider
    For request localization according to {culture} route value
  • Define global route template for culture parameter:
    Add {culture} parameter to url, e.g., www.example.com/en-US/
  • Create language navigation
    The language navigation is required to switch between cultures. All supported cultures must be listed.
  • *Setup culture cookie value
    When the selected language is changed, the new culture can be defined via route value, query string value or cookie value. Route value and query string values can be done easily, but the culture cookie value requires a special handler to save the current culture name to a cookie.
  • Setup DataAnnotations localization:
    Localizaton of data annotations like Display names, Required, StringLength, ...etc. DataAnnotations are defined in System.ComponentModel.DataAnnotations namespace.
    In addition to localizing the default DataAnnotations, custom attributes also must be localized using the same logic. See DataAnnotations.
  • Setup ModelBinding error messages localization:
    Validation of input types at server side after submit, e.g., ValueMustBeANumberAccessor, AttemptedValueIsInvalidAccessor, etc.
    For more details, see DefaultModelBindingMessageProvider.
  • Setup IdentityDescriber error messages:
    Localization of all user and role related messages like “User already in role", "User name already exists", ...etc. See IdentityErrorDescriber.
  • *Setup view localization:
    Localize text/html in razor pages. See ViewLocalization.
  • Setup client side validation scripts :
    Localizing of client validation messages is essential, forms will be validated before submit. The client side validation scripts must be localized so the user will see localized messages like: The field is required, Passwords must match, ..etc.

    One more important thing is validating localized decimal numbers, some cultures uses period and other cultures uses comma in decimal numbers like (1,2) and (1.2), this case should be handled carefully in client side validation.

    What is more: Some cultures may use totally different numbering systems; e.g. Arabic cultures are using numbers like “٠١٢٣٤٥٦٧٨٩” and latin cultures are using “0123456789” If the numbering system setup is not correct a validation error will rise. (see this article to learn how to change numbering system for client side validation).
  • *Create localized resource files for each culture :
    Views, DataAnnotations, ModelBinding and IdentityErrors all requires localized resources. This step consumes a lot of time and effort!

All these steps require a lot of work and consume too much time. So, here comes the benefit of LazZiya.ExpressLocalization nuget package that eliminates the localization setup time and effort with simple lines of code.

Creating the Project

Let’s start by creating a basic ASP.NET Core 2.2 web application (I’m using VS2019):

  1. Create a new project by selecting ASP.NET Core Web Application:

2. Click Next, give the project a friendly name and click Create:

3. Select Web Application and make sure you change the authentication to Individual User Accounts.

4. Click Create and wait till the solution creates the basic template, once it is done, you can do a test run by selecting the project name in the solution explorer, then pressing (Ctrl + Shift + B) to build the project, then (Ctrl + Shift + W) to run without debugging in the browser.

Installing ExpressLocalization Nuget Packages

  1. In the solution explorer under the project name, right click on Dependencies and select “Manage Nuget Packages”.

2. Go to Browse tab and search for “LazZiya" and install these packages:

These packages will help us localize our web app in few steps.

Creating Localized Resources

Under the project root, create a new folder and name it “LocalizationResources”:

Under LocalizationResources folder, create new public class and name it "ViewLocalizationResource", this class will be used to group resource files for view localization:

namespace ExpressLocalizationSample.LocalizationResources
{
public class ViewLocalizationResource
{
}
}

Under LocalizationResources folder, create new public class and name it "ExpressLocalizationResource", this class will be used to group resource files for identity, model binding and data annotations.

namespace ExpressLocalizationSample.LocalizationResources
{
public class ExpressLocalizationResource
{
}
}

We will use these two classes to pass resource type to the express localization method.

You may fill the files manually or, download the relevant cultures resources from this repository folder in github. Please notice that you need to download two files for each culture, e.g. (ExpressLocalizationResource.tr.resx and ViewLocalizationResource.tr.resx)

Localizing the application

Finally, we are ready for the localization setup. :)

Open startup.cs file and add LazZiya.ExpressLocalization namespace :

using LazZiya.ExpressLocalization

Then define the cultures list and add one the one step localization setup as below in ConfigureServiecs method:

var cultures = new[]
{
new CultureInfo("tr"),
new CultureInfo("ar"),
new CultureInfo("hi"),
new CultureInfo("en"),
};
services.AddRazorPages()
.AddExpressLocalization<ExpressLocalizationResource, ViewLocalizationResource>(
ops =>
{
ops.ResourcesPath = "LocalizationResources";
ops.RequestLocalizationOptions = o =>
{
o.SupportedCultures = cultures;
o.SupportedUICultures = cultures;
o.DefaultRequestCulture = new RequestCulture("en");
};
});

Then under Configure method, configure the app to use request localization:

app.UseRequestLocalization();

Adding Language Navigation

Under Pages folder, open _ViewImports.cshtml file and add LazZiya.TagHelpers that will help in creating language navigation:

@addTagHelper *, LazZiya.TagHelpers

Then open Pages/Shared/_Layout.cshtml file and add the language navigation tag helper under the _LoginPartial tag as below:

<partial name="_LoginPartial" />
<language-nav></language-nav>

That’s it, we are ready for the first run:

Create Culture Cookie

When we do change the culture via the language navigation, it will generate url’s with culture name included. Additionally, we can configure our language navigation to set a cookie with the current culture value.

First, create a handler in the Index.cshtml.cs page to save the culture cookie value:

public IActionResult OnGetSetCultureCookie(string cltr, string returnUrl)
{
Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(cltr)),
new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
);

return LocalRedirect(returnUrl);
}

Then all we need is to configure language navigation to call the cookie handler as below:

<language-nav cookie-handler-url="@Url.Page("/Index", "SetCultureCookie", new { area="", cltr="{0}", returnUrl="{1}" })"></language-nav>

The place holders “{0}” and “{1}” will be filled by the tag helper with reference to each listed culture.

Localizing Views

So good so far, we have our navigation with supported cultures, but we still need to localize view texts to see the localized versions.

The localized texts for the default project are already provided in “ViewLocalizationResource.xx.resx” in the downloaded files. If you need to add more custom texts for views, add them to the “ViewLocalizationResource.xx.resx” files.

Option 1 (recommended)

ExpressLocalization contains a special tag helper for view localization via simple html tags.

Add ExpressLocalization tag helpers to _ViewImports.cshtml:

@addTagHelper *, LazZiya.ExpressLocalization

Use localize html tag to localize views using html friendly tags, e.g:

<localize>Hellow world!</localize>

or

<h1 localize-content>Hellow world!</h1>

Open Pages/Index.cshtml and use localize tag helper to localize texts/html:

@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="text-center">
<h1 class="display-4" localize-content>Welcome</h1>
<p localize-content>Learn about <a href='https://docs.microsoft.com/aspnet/core'> building Web apps with ASP.NET Core</a>.</p>
</div>

Use the same process to localize all texts in other views as well.

See more details in live demo page and GitHub Wiki.

Option 2

Use the classic localization method that depends on injecting culture localizer to the views and call its method.

Open Pages/_ViewImports.cshtml file and inject ISharedCultureLocalizer that already comes with ExpressLocalization:

@using LazZiya.ExpressLocalization
@inject ISharedCultureLocalizer _loc

Then open Pages/Index.cshtml and use localizer function for texts:

@page
@model IndexModel
@{
ViewData["Title"] = _loc["Home page"];
}
<div class="text-center">
<h1 class="display-4">@_loc["Welcome"]</h1>
<p>@_loc["Learn about <a href='https://docs.microsoft.com/aspnet/core'> building Web apps with ASP.NET Core</a>"].</p>
</div>

Use the same process to localize all texts in other views as well.

Localizing Url’s

Our url’s in the project are still culture free, they don’t have any culture parameter. If you have done the cookie setup step, all links will work even without the culture parameter defined in the url.

But if we didn’t setup the culture cookie, or if we want to share links with culture parameter included then we have to add the culture value to every generated link in the project.

Open Pages/_ViewImports.cshtml and add reference to System.Globalization:

@using System.Globalization

Then open Pages/_LoginPartial.cshtml and add to the top of the page a culture parameter as below:

@{
var culture = CultureInfo.CurrentCulture.Name;
}

Use this parameter to provide culture route value to all links as below:

<a class="nav-link text-dark"    asp-area="Identity"   asp-page="/Account/Register"   asp-route-culture="@culture"   localize-content>Register</a>

Do this to all views in the project.

Localizing Identity Views

Identity related pages like login, register and profile needs to be overridden in order to be modified.

Right click on the project name, select AddNew Scaffolded Item

Select Identity and click Add:

Select “Override all files” and select “ApplicationDbContext":

When you click Add, a new Areas folder will be created including all identity related views:

Identity area has three _ViewImports folders:

  • Areas/Identity/Pages/_ViewImports.cshtml
  • Areas/Identity/Pages/Account/_ViewImports.cshtml

Add the below code to all of them as we did for Pages/_ViewImports.cshtml previously:

@using System.Globalization@addTagHelper *, LazZiya.TagHelpers
@addTagHelper *, LazZiya.ExpressLocalization

Go over the views and use localization steps as we did before for localizing views and add culture route parameter as well. Below is the Register.cshtml page:

@page
@model RegisterModel
@{
ViewData["Title"] = "Register";
var culture = CultureInfo.CurrentCulture.Name;
}
<h1 localize-content>Register</h1><div class="row">
<div class="col-md-4">
<form asp-route-returnUrl="@Model.ReturnUrl"
method="post" asp-route-culture="@culture">
<h4 localize-content>Create a new account.</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Input.Email"></label>
<input asp-for="Input.Email" class="form-control" />
<span asp-validation-for="Input.Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Password"></label>
<input asp-for="Input.Password" class="form-control" />
<span asp-validation-for="Input.Password"
class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.ConfirmPassword"></label>
<input asp-for="Input.ConfirmPassword" class="form-control" />
<span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary" localize-content>Register</button>
</form>
</div>
</div>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}

Localizing DataAnnotations

If you run the page and do enter some invalid inputs, you will notice that the validation messages are in English, so we will need to localize the data annotations messages, e.g., Required, StringLength, etc..

ExpressLocalization offers a collection of built-in data annotation attributes, these attributes produces a localized error messages by default. Add a reference to the express attributes namespace, and use them as below:

Open Areas/Identity/Pages/Account/Register.cshtml.cs file and use express attributes as below:

@using LazZiya.ExpressLocalization.DataAnnotations;public class InputModel
{
[ExRequired]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }
[ExRequired]
[ExStringLength(100, MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[ExCompare("Password")]
public string ConfirmPassword { get; set; }
}

Compile and run the project, you will see localized data annotation error messages:

Notice: Some attributes in the framework are already producing localized error messages. For more details see Express Attributes and default attributes.

Client Side Validation

The server side validation is working well, but we still need to add client side validation as well, so the input fields will be validated on client side before submitting the form.

One major issue with client side validation is validating localized inputs like numbers, dates, etc. For example, if you are using a decimal input, you will see validation error for localized numbers like 1.3 is valid in English culture, but is invalid for Turkish because it should be 1,3 (comma instead of period).

Here, we will use a tag helper component from LazZiya.TagHelpers. First we need to register the component in startup:

services.AddTransient<ITagHelperComponent, LocalizationValidationScriptsTagHelperComponent>();

The name is a bit long, but it will save us more time during validation :)

Open Register.cshtml page and add the tag helper under the validation scripts partial tag:

@section Scripts {    <partial name="_ValidationScriptsPartial" />
<localization-validation-scripts></localization-validation-scripts>
}

That’s all, now the fields will be validated before submitting the form with localized validation messages:

Download Sample Project

You can download a sample project with more than 19 cultures included from GitHub :

References

Read more details about the used nuget packages here:

--

--

Ziya Mollamahmut
The Startup

Software Developer, Regional Training Expert, 3D Modeling Hobbyist