ASP.NET Core 6: Implementing a Localization in ASP.NET Core 6
In this article, we will implement Localization in ASP.NET Core 6. In web application development if the website has multilingual support then it can reach a wider audience. One of the best and most important features of the ASp.NET Core is the availability of middlewares for localizing the application into different languages and cultures. The application localization involves the following features
- Provide localized resources for the languages and cultures
- Provide a strategy to select the culture and language for every request
- Making the application content localizable
A Resource file is a useful mechanism for defining the localizable strings for the text-based values that will be shown on the User Interface. Resource file has a .resx extension. We can store the translated strings in this file. We need to create these resource files for each localized language.
ASP.NET Core provides middleware and service to use the localization using Microsoft.Extensions.Localization namespace. The IStringLocalizer interface represents a service that provides a localized string using the Resource File. The RequestLocalizationMiddleware class enables automatic setting on the culture based on the request sent by the client. This class is used in the request pipeline using RequestLocalizationOptions class to set the culture.
Creating the Application
Step 1: Open Visual Studio 2022 and create a new ASP.NET Core MVC Application. Set the target framework as .NET 6. Name this application Core6_Internationalization.
Step 2: In the Models folder, add a new class file and name it as Product.cs. In this class file, add the code as shown in listing 1
using System.ComponentModel.DataAnnotations; namespace Core6_Internationalization.Models { public class Product { [Required(ErrorMessage = "Product Id is Required")] [Display(Name = "ProductId")] public int? ProductId { get; set; } [Required(ErrorMessage = "Product Name is Required")] public string? ProductName { get; set; } [Required(ErrorMessage = "Category Name is Required")] public string CategoryName { get; set; } = string.Empty; [Required(ErrorMessage = "Manufacturer is Required")] public string Manufacturer { get; set; } = String.Empty; [Required(ErrorMessage = "Price is Required")] public int? Price { get; set; } } public class Products : List<Product> { public Products() { Add(new Product() { ProductId=101,ProductName="Laptop",CategoryName="Electronics",Manufacturer="MS-Electronics", Price=456000 }); Add(new Product() { ProductId = 102, ProductName = "Iron", CategoryName = "Electrical", Manufacturer = "MS-Electical", Price = 5000 }); Add(new Product() { ProductId = 103, ProductName = "Biscuts", CategoryName = "Food", Manufacturer = "MS-Food", Price = 60 }); Add(new Product() { ProductId = 104, ProductName = "Router", CategoryName = "Electronics", Manufacturer = "LS-Electronics", Price = 5600 }); Add(new Product() { ProductId = 105, ProductName = "Mixer", CategoryName = "Electrical", Manufacturer = "LS-Electrical", Price = 4000 }); Add(new Product() { ProductId = 106, ProductName = "Chips", CategoryName = "Food", Manufacturer = "LS-Food", Price = 40 }); Add(new Product() { ProductId = 107, ProductName = "Mouse", CategoryName = "Electronics", Manufacturer = "TS-Electronics", Price = 500 }); Add(new Product() { ProductId = 108, ProductName = "Dryer", CategoryName = "Electrical", Manufacturer = "TS-Electrical", Price = 4600 }); Add(new Product() { ProductId = 109, ProductName = "Soya Milk", CategoryName = "Food", Manufacturer = "TS-Food", Price = 45 }); Add(new Product() { ProductId = 110, ProductName = "Keyboard", CategoryName = "Electronics", Manufacturer = "MS-Electronics", Price = 1600 }); Add(new Product() { ProductId = 111, ProductName = "Charger", CategoryName = "Electrical", Manufacturer = "LS-Electrical", Price = 600 }); Add(new Product() { ProductId = 112, ProductName = "Yoghurt", CategoryName = "Food", Manufacturer = "TS-Foods", Price = 40 }); } } }
Listing 1: The Product can Products class
As shown in Listing 1, we have a Product class with public properties that are applied with validation rules and messages and a Products class that is derived from the List of Products. We have hardcoded Products information. (You can use EF Core here instead).
Step 3: In the project add a new folder and name it as Services. In this folder add a new class file and name it as ProductService.cs. In this class file, we will add ProductService class which will be used to perform read/write operations on the Products class that we have added in listing 1. The code for ProductService class is provided in listing 1
using Core6_Internationalization.Models; namespace Core6_Internationalization.Services { public class ProductService { private Products products; public ProductService() { products = new Products(); } public List<Product> GetProducts() { return products; } public List<Product> AddProducts(Product prd) { products.Add(prd); return products; } } }
Listing 2: The ProductService class
Step 4: To configure the application to support localization we need to add localization service and the middleware in the application startup. Modify the Program.cs by adding code as shown in listing 3
using Core6_Internationalization.Services; using Microsoft.AspNetCore.Mvc.Razor; var builder = WebApplication.CreateBuilder(args); // Add services to the container. // 1. builder.Services.AddLocalization(options=>options.ResourcesPath= "Resources"); // 2. builder.Services.AddControllersWithViews() .AddViewLocalization (LanguageViewLocationExpanderFormat.SubFolder) .AddDataAnnotationsLocalization(); // 3. builder.Services.Configure<RequestLocalizationOptions>(options => { var supportedCultures = new[] { "en-US", "fr", "de" }; options.SetDefaultCulture(supportedCultures[0]) .AddSupportedCultures(supportedCultures) .AddSupportedUICultures(supportedCultures); }); builder.Services.AddScoped<ProductService>(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); // The default HSTS value is 30 days. You may want to
// change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); // 4. var supportedCultures = new[] { "en-US", "fr", "de" }; // 5. // Culture from the HttpRequest var localizationOptions = new RequestLocalizationOptions() .SetDefaultCulture(supportedCultures[0]) .AddSupportedCultures(supportedCultures) .AddSupportedUICultures(supportedCultures); app.UseRequestLocalization(localizationOptions); app.UseRouting(); app.UseAuthorization(); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); app.Run();
Listing 3: Adding Services and Middleware for Localization support
The code in Listing 3 has the following specifications, (Note: Following numbering matches with numbers applied on the code comments)
- Here we are adding the localization service and we are configuring the application to read resources from the Resources folder
- We are adding the View Localization Service using AddViewLocalization() method. This method accepts the localization view expander format which configures the application to read the localization strings from resource files in the subfolders of the Resources folder. In the resource folder, we will add Controllers, Models, and Views folders. We will add resource files in these folders for localized strings for Controllers, Models, and Views of the application. Using the AddDataAnnotationsLocalization() method we are configuring the service for the localization for Data Annotation Error messages.
- We also need to configure the default Culture and UI Culture using the RequestLocationOptions class. We are configuring the application for en-US, fr (French), and (German) de cultures. We are setting the default culture to en-US.
- We are setting localization cultures that will be used by the middleware.
- Using the UseRequestLocation() middleware method we have provides the default culture is en-US. We are configuring the HTTP request pipeline using the UseRequestLocation() middleware method to support r (French), and (German) de cultures.
Adding Resource Files
To add resource files in the project we need to add a Resources folder in the application and this folder must have sub-folders for having localization strings for Views, Controllers, and Models. The name of the Resource file must be as follows
Resources/Controllers/ControllerName.[culture].resx e.g. for the HomeController the resource file name we be Resources/Controllers/HomeController.en.resx.
In the project, we have HomeController by default with the Index and Privacy methods. In the Views folder, we have views for Index and Privacy. We will create resource files for Index and Privacy views.
Step 5: In the Views sub-folder, add a new folder and name it Home. In this folder, add a new resource file and name it as Index.en.resx. In this file, we need to add the String name and its value. To add the resource strings, open the Index.cshtml from the Views/Home folder and check the constant values, here we have values like Webcome, Learn about, etc. We will use Google Translator to add the localized strings.
Resources/Views/Home/Index.en.resx is shown in figure 1
Figure 1: The Resource file for Index View of Home Controller
Resources/Views/Home/Index.fr.resx is shown in figure 2
Figure 2: The Resource file for Index View of Home Controller in french
Similarly, add a resource file for Index.de.resx and for Privacy.cshtml add resource files as Privacy.en.resx, Privacy.fr.resx, and Privacy.de.resx.
Step 6: We need to provide a facility to the end-user to select Culture. To do that let's add a new partial view in the Views/Shared folder of the application. Name this file as SelectCulture.cshtml. In this file add the markup and the code as shown in listing 4
@using Microsoft.AspNetCore.Builder @using Microsoft.AspNetCore.Http.Features @using Microsoft.AspNetCore.Localization @using Microsoft.AspNetCore.Mvc.Localization @using Microsoft.Extensions.Options @inject IViewLocalizer Localizer @inject IOptions<RequestLocalizationOptions> locOptions @{ var currentRequestCulture = Context.Features.Get<IRequestCultureFeature>(); var cultureItems = locOptions.Value.SupportedUICultures .Select(c => new SelectListItem { Value = c.Name, Text = c.DisplayName }) .ToList(); var responseUrl = string.IsNullOrEmpty(Context.Request.Path) ? "~/" : $"~{Context.Request.Path.Value}"; }
<div title="@Localizer["Request the culture provider:"] @currentRequestCulture?.Provider?.GetType().Name"> <form id="selectLanguage" asp-controller="Home" asp-action="SetAppLanguage" asp-route-returnUrl="@responseUrl" method="post" class="form-horizontal" role="form"> <label asp-for="@currentRequestCulture.RequestCulture.UICulture.Name"> @Localizer["Select Language:"]</label> <select name="culture" onchange="this.form.submit();" asp-for="@currentRequestCulture.RequestCulture.UICulture.Name" asp-items="cultureItems"> </select> </form> </div>
Listing 4: The Culture Selection View using SelectCulture.cshtml
As shown in Listing 4, the view is injected IViewLocalizer interface. This interface represents a service that represents the HTML-aware localization for Views. Since we have already added the localization middleware using UseRequestLocalization() method, we have set the default culture to en-US and we are allowing the user to select the culture by injecting the RequestLocationOptions class using IOptions interface.
Step 7: Let's modify the _Layout.cshtml by adding the SelectCulture partial view in it as shown in listing 5
<div class="container"> <main role="main" class="pb-3"> @RenderBody() <div class="col-md-6 text-right"> @await Html.PartialAsync("SelectCulture") </div> </main> </div>
Listing 5: The _Layout.cshtml
Step 7: In the Resources/Views folder add a new sub-folder and name it as Shared. In this folder add a new resource file and name it as SelectCulture.en.resx with string and value as shown in figure 3
Figure 3: The SelectCulture.en.resx
Figure 4:The SelectCulture.fr.resx
Similarly, add the _Layout.en.resx, _Layout.fr.resx, and _Layout.de.resx in the Resources/Views/Shared folder and create resource string for French and German culture.
Since we have added the required resources to the project for the HomeController Views, it's time for us to add resources for the Product entity class.
Step 8: We have already added the Product class in the Models folder with validations using Data Annotations. To define resource strings for Data Annotations, add a new resource file in the Models sub-folder of the Resources folder and name it as Product.fr.resx. In this file add a resource string for Frech culture as shown in figure 5
Figure 5: The roduct.fr.resx
As shown in Figure 5, we have added a French resource string for error messages on the properties of the Product class. Similarly, the Product.de.resx can be added with German language resource strings.
Step 9: We will be using the ProductController for performing the Product CRUD operation, so we need the following strings for creating new products and displaying a list of products
- ProductId, ProductName, CategoryName. Manufacturer, Price.
- We also need values for CategoryName like Electronics, Electrical, Food and various manufacturer like MS-Electronics, MS-Electrical, MS-Food, etc.
Figure 6: The productController.en.resx
In the same folder add a new resource file and name it as ProductController.fr.resx and add the following resource string in it as shown in figure 7
Figure 7: The French resource string for ProductController.fr.resx
Similarly, add the ProductController.de.resx for German local strings.
Step 10: In the Controllers folder of the application, add a new Empty MVC Controller and name it as ProductController.cs. In this controller add the code as shown in listing 6
using Microsoft.AspNetCore.Mvc; using Core6_Internationalization.Models; using Core6_Internationalization.Services; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.Extensions.Localization; namespace Core6_Internationalization.Controllers { public class ProductController : Controller { private readonly ProductService productService; private List<SelectListItem> categories; private List<SelectListItem> manufactureres; private readonly IStringLocalizer<ProductController>? _localizer; public ProductController(ProductService productService,
IStringLocalizer<ProductController>? localizer) { _localizer = localizer; this.productService = productService; categories = new List<SelectListItem>() { new SelectListItem(localizer["Electronics"], localizer["Electronics"]), new SelectListItem(localizer["Electrical"], localizer["Electrical"]), new SelectListItem(localizer["Food"], localizer["Food"]) }; manufactureres = new List<SelectListItem>() { new SelectListItem(localizer["MS-Eletronics"], localizer["MS-Eletronics"]), new SelectListItem(localizer["MS-Electrical"], localizer["MS-Electrical"]), new SelectListItem(localizer["MS-Food"], localizer["MS-Food"]), new SelectListItem(localizer["LS-Eletronics"], localizer["LS-Eletronics"]), new SelectListItem(localizer["LS-Electrical"], localizer["LS-Electrical"]), new SelectListItem(localizer["LS-Food"], localizer["LS-Food"]), new SelectListItem(localizer["TS-Eletronics"], localizer["TS-Eletronics"]), new SelectListItem(localizer["TS-Electrical"], localizer["TS-Electrical"]), new SelectListItem(localizer["TS-Food"], localizer["TS-Food"]) }; } public IActionResult Index() { var products = productService.GetProducts(); return View(products); } public IActionResult Create() { var product = new Product(); ViewBag.CategoryName = categories; ViewBag.Manufacturer = manufactureres; return View(product); } [HttpPost] public IActionResult Create(Product product) { if (ModelState.IsValid) { var products = productService.AddProducts(product); return View("Index", products); } else { ViewBag.CategoryName = categories; ViewBag.Manufacturer = manufactureres; return View(product); } } } }
Listing 6: The ProductController
As shown in Listing 6, the ProductController is the constructor injected with ProductController and the IStringLocalizer(). The IStringLocalizer is used to read the localized string from the resource files and used them on views that are responded with requests to the ProductController action methods. The important part of the controller class is the ProductController constructor. This constructor has code to declare SelectItem List object. This list object contains each item as SelectListItem which reads values from the localized strings from ProductController.en.resx, ProductController.fr.resx, ProductController.de.resx based on the culture selected by the end-user. The Index() action method will return the Index view showing a list of Products and Create() action method returns a Create view for accepting the Product information.
Step 11: Since we need to Products data in index View we will have to add Index.cshtml for ProductController that will show Product information in the table with columns such as ProductId, ProductNamee, CategoryName, Manufacturer, Price, etc. Similarly, Create.cshtml will also use the same properties of the Product to accept the Product information. In the Views sub-folder of the Resources folder add a new folder and name it as Product. In this folder, we will add Index.en.resx, Index.fr.resx, Created.en.resx, and Create.fr.resx as shown in figure 8
Index.en-resx
Index.fr.resx
Create.en-resx
Create.fr.resx
Figure 8: Index.en.resx, Index.fr.resx, Create.en.resx, and Create.fr.resx
We can also add Index.de.resx and Create.de.resx for German strings culture. As shown in figure 8 we have created resource strings for the English and French cultures.
Step 12: Generate Index and Create Views using the Index and Create action methods of the ProductController. Once these views are generated, we need to modify them to show the rendering based on the selected culture by the end-user. Listing 7 shows the markup for Index.cshtml with localized settings
@using Microsoft.AspNetCore.Builder @using Microsoft.AspNetCore.Http.Features @using Microsoft.AspNetCore.Localization @using Microsoft.AspNetCore.Mvc.Localization @using Microsoft.Extensions.Options @inject IViewLocalizer Localizer @inject IOptions<RequestLocalizationOptions> LocOptions @model IEnumerable<Core6_Internationalization.Models.Product> @{ ViewData["Title"] = Localizer["Index"]; } <h1>@Localizer["Index"]</h1> <p> <a asp-action="Create">@Localizer["Create New"]</a> </p> <table class="table"> <thead> <tr> <th> @Localizer["ProductId"] </th> <th> @Localizer["ProductName"] </th> <th> @Localizer["CategoryName"] </th> <th> @Localizer["Manufacturer"] </th> <th> @Localizer["Price"] </th> <th></th> </tr> </thead> <tbody> @foreach (var item in Model) { <tr> <td> @Html.DisplayFor(modelItem => item.ProductId) </td> <td> @Html.DisplayFor(modelItem => item.ProductName) </td> <td> @Html.DisplayFor(modelItem => item.CategoryName) </td> <td> @Html.DisplayFor(modelItem => item.Manufacturer) </td> <td> @Html.DisplayFor(modelItem => item.Price) </td> <td> </td> </tr> } </tbody> </table>
Listing 7: The Index.cshtml
Listing 8 shows the markup for Create.cshtml with localized settings
@using Microsoft.AspNetCore.Builder @using Microsoft.AspNetCore.Http.Features @using Microsoft.AspNetCore.Localization @using Microsoft.AspNetCore.Mvc.Localization @using Microsoft.Extensions.Options @using System.ComponentModel.DataAnnotations; @inject IViewLocalizer Localizer @inject IOptions<RequestLocalizationOptions> LocOptions @model Core6_Internationalization.Models.Product @{ ViewData["Title"] = Localizer["Create"]; List<SelectListItem> categories = (List<SelectListItem>)ViewBag.CategoryName; List<SelectListItem> manufactureres = (List<SelectListItem>)ViewBag.Manufacturer; } <h1>@Localizer["Create"]</h1> <h4>@Localizer["Product"]</h4> <hr /> <div class="row"> <div class="col-md-4"> <form asp-action="Create"> <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="ProductId" class="control-label">@Localizer["ProductId"]</label> <input asp-for="ProductId" class="form-control" /> <span asp-validation-for="ProductId" class="text-danger"> </span> </div> <div class="form-group"> <label asp-for="ProductName" class="control-label">@Localizer["ProductName"]</label> <input asp-for="ProductName" class="form-control" /> <span asp-validation-for="ProductName" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="CategoryName" class="control-label">@Localizer["CategoryName"]</label> @* <input asp-for="CategoryName" class="form-control" />*@ <select asp-for="CategoryName" class="form-control" asp-items='@categories' > <option value="">@Localizer["Select Category Name"]</option> </select> <span asp-validation-for="CategoryName" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Manufacturer" class="control-label">@Localizer["Manufacturer"]</label> <select asp-for="Manufacturer" class="form-control" asp-items="@manufactureres"> <option value="">@Localizer["Select Manufacturer"] </option> </select> <span asp-validation-for="Manufacturer" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Price" class="control-label">@Localizer["Price"]</label> <input asp-for="Price" class="form-control" /> <span asp-validation-for="Price" class="text-danger"></span> </div> <div class="form-group"> <input type="submit" value="@Localizer["Create"]" class="btn btn-primary" /> </div> </form> </div> </div> <div> <a asp-action="Index">Back to List</a> </div> @section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} }
Listing 8: Create.cshtml
Carefully read the code of listing 7 and 8. These views are injected with IViewLocalizer to localize the view. Index.cshtml uses the Index.en.resx, Index.fr.resx, and Index.de.resx to show strings for rendering Index view based on selected culture. Similarly, the Create.cshtml uses Create.en.resx, Create.fr.resx, and Create.de.cshtml to show accept Product Information and show error messages based on culture selected by the end-user. The Create.cshtml will show the list of Categories and Manufacturers using the resource strings defined in ProductController.en.resx, ProductController.fr.resx, and ProductController.de.resx.
Step 13: Modify the _Layout.cshtml from the View/Shared folder that uses the _Layout.en.resx, _Layout.fr.resx, and _Layout.de.resx (Please note you need to add these resources files based on the discussion we had so far.) The markup is shown in listing 9
.... some code omitted here <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index" >@Localizer["Home"]</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy" >@Localizer["Privacy"]</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Product" asp-action="Index" >@Localizer["Product"]</a> </li> ... Sode code omitted here
Listing 9: The _Layout.cshtml
Run the application and it will be loaded in the browser as shown in figure 9
Figure 9: Selecting the culture
Select the Language as french the Home page will show the output for all strings French as shown in figure 10
Figure 10: The French Culture
Click on Product (French translation for Product), this will show the list of Products in the table with column headers in French, as shown in figure 11
Figure 11: Products List with French Strings for Column headers
Click on Créer un nouveau link (the French translation of Create New), this will show create product view in the French language as shown in figure 12:
Figure 12: The Product Create View in French Culture
Click on the Nom de catégorie, the categories will be displayed in the dropdown as shown in figure 13
Figure 13: Categories with French Culture
Click on the Créer (translation of the Create in French) button without entering anything in Textboxes, the data validation errors will be shown in figure 14
Figure 14: Error Messages
The code for this article can be downloaded here.
Conclusion: The Services and Middleware make it easy to implement localization in ASP.NET Core . The best feature provided in ASP.NET core is View and Data Annotation Localization. This makes the UI more interactive to the end-user.