Localizing ASP.NET Core MVC Applications from Database
Tags: localization, aspnet, dotnet
Published: 2022-12-05 20:46:12

## Introduction

Traditionally, .NET applications are localized using (compiled) resource files. Developer tools are used to author those files and an update of the localization strings typically requires an update of the application.

Storing localization data in a database gives you much more flexibility as to by whom and when translations are entered and updated. You can deploy an application before all translations are entered and you can still fix translations after deployment. And this can be done remotely, by any kind of user interface on top of the database.

But there is also a backdraw with storing localization data in a database: database access is slow. Especially if you compare it to resource files that are loaded in memory and operate from there.

The component I present you here – **MvcDasboardLocalize** – encompasses a database driven localization solution that uses a memory-loaded cache for performance, as well as a dashboard to enter and maintain localization data. It also offers some unique features not found in resource file based localization.

> The **MvcDashboardLocalize** component provides a very complete solution to localize ASP.NET Core (MVC) applications

But before looking to those features, let’s create an ASP.NET Core application and add localization to it.

## Building the Application

With Visual Studio 2022, we choose to create a new project of type “**ASP.NET Core Web App (Model-View-Controller)**” in C#. We choose for .NET 6.0 and authentication type None and leave the default to use top-level statements. I named my application “LocalizationDemo”. That’s the name that Visual Studio will also use as root namespace.

If all went well, we now have a web application with one controller (`HomeController`) and two views in the _Views\Home_ folder (_Index.cshtml_ and _Privacy.cshtml_).

To make the localization demo a bit more challenging, we will add a model type and web form so we can edit a model and get – or not – ModelState errors.

To the Models folder, we add a `PersonModel` class with some fields as `FirstName`, `LastName`, `Age` and `EmailAddress`. We also add data annotation attributes in different flavours. The result is the class in **Listing 1**:

```c#
using System.ComponentModel.DataAnnotations;

namespace LocalizationDemo.Models
{
    public class PersonModel
    {
        [Display(Name = "First Name")]
        [Required]
        public string? FirstName { get; set; }

        [Required]
        public string? LastName { get; set; }

        [Range(6, 140, ErrorMessage = "Please enter your age.")]
        public int Age { get; set; }

        [EmailAddress]
        public string? EmailAddress { get; set; }
    }
}
```
_Listing 1: A model class for our application_

To the HomeController, we add a single action – _UpdatePerson_ – that receives a _PersonModel_ and returns a view for that model. The result is a very simple action method shown in **Listing 2**:

```c#
// UpdatePerson action on HomeController:
public IActionResult UpdatePerson(PersonModel model)
{
    return View(model);
}
```
_Listing 2: HomeController UpdatePerson action_

The view is somewhat more elaborate and is rendered as **Listing 3**:

```html
@model PersonModel

<style>
    .validation-summary-valid {
        display: none;
    }
</style>

<form method="post">

    <div asp-validation-summary="All" class="alert alert-danger mb-3">
        <strong>Following errors have occured:</strong>
    </div>

    <div class="mb-3">
        <label asp-for="FirstName" class="form-label"></label>
        <input asp-for="FirstName" type="text" class="form-control">
        <span asp-validation-for="FirstName" class="text-danger"></span>
    </div>

    <div class="mb-3">
        <label asp-for="LastName" class="form-label"></label>
        <input asp-for="LastName" type="text" class="form-control">
        <span asp-validation-for="LastName" class="text-danger"></span>
    </div>

    <div class="mb-3">
        <label asp-for="Age" class="form-label"></label>
        <input asp-for="Age" type="text" class="form-control">
        <span asp-validation-for="Age" class="text-danger"></span>
    </div>

    <div class="mb-3">
        <label asp-for="EmailAddress" class="form-label"></label>
        <input asp-for="EmailAddress" type="text" class="form-control">
        <span asp-validation-for="EmailAddress" class="text-danger"></span>
    </div>

    <button type="submit" class="btn btn-primary">Submit</button>

</form>
```
_Listing 3: The UpdatePerson view_

You may also want to update the __Layout.cshtml_ view in the _Shared_ folder to add a link to the `UpdatePerson` action so you can access the page by clicking on the link instead of typing over the URL each time.

That’s it for the application! It doesn’t do anything useful, but it already includes some localization challenges. When we run the application and navigate to the <u>/Home/UpdatePerson</u> URL, we will see that every field except _EmailAddress_ has a validation error message. See **Figure 1**. We now will see several ways to localize these error messages.

![The application](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ddpqan8zs1ce8b5pbo72.png)_Figure 1: The application_

## Adding Localization

We can now start adding localization to the application by using the **Arebis.MvcDashboardLocalize** .NET Core Template Package found on GitHub:

[https://www.nuget.org/packages/Arebis.MvcDashboardLocalize/](https://www.nuget.org/packages/Arebis.MvcDashboardLocalize/)

To do so in Visual Studio, go to the _Tools_ menu, _NuGet Package Manager_ and open the _Package Manager Console_.

First, we must install the package on our machine. We do so with the following command in the Package Manager Console:

`dotnet new --install Arebis.MvcDashboardLocalize`

Then we install the MvcDashboardLocalize template with the following command:

`dotnet new MvcDashboardLocalize -n LocalizationDemo`

“LocalizationDemo” is the name of my ASP.NET Core project. If you named your project differently, you have to change the name here too.

If all went well, you received the message:

`The template "ASP.NET MVC Dashboard Localize" was created successfully.`

You will also notice a few additions to your project: three folders (_Areas_, _Data_ and _Localize_) and one additional file (_ModelStateLocalization.json_) have been added.

The _Areas_ folder contains an _MvcDashboardLocalize_ folder which contains a _ReadMe-MvcDashboardLocalize.html_ file. This file contains the remaining setup instructions which we will also follow here:

### Step 1: Add Package Dependencies

To our project, we must add the following NuGet package dependencies:

- Arebis.Core.AspNet.Mvc.Localization
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Tools

We add those dependencies by right-clicking the _Dependencies_ node in our project, choose _Manage NuGet Packages_, and install these three packages from the _Browse_ tab.

### Step 2: Create a Database

Since we are storing localization data in a database, we need to create a SQL Server database. The database can be created from the SQL Server Object Explorer in Visual Studio, or from SQL Server Management Studio, whatever you prefer. I named my database “LocalizationDemoDb”.

No need to add any tables. Those will be added in Step 6 by running a migration.

### Step 3: Configure the Database Connection String

In the _appsettings.json_ of our project, we add a connection string to the database we just created:

```json
"ConnectionStrings": {

  "DefaultConnection": "Server=(local);Database=LocalizationDemoDb;Trusted_Connection=True;MultipleActiveResultSets=true"

}
```

### Step 4: Configure the Application and Application Services

Now that we have our database setup, we can configure our application services. This is done by adding code to the _Program.cs_ file (since we are using top-level statements). The complete updated _Program.cs_ file is found in Listing 4:

```c#
using Arebis.Core.AspNet.Mvc.Localization;
using Arebis.Core.Localization;
using LocalizationDemo.Localize;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

#region Localization

builder.Services
    .AddDbContext<LocalizationDemo.Data.Localize.LocalizeDbContext>(
    optionsAction: options => options.UseSqlServer(
      builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services
    .AddTransient<ILocalizationSource, DbContextLocalizationSource>();

builder.Services
    .AddLocalizationFromSource(builder.Configuration, options => {
    options.Domains = new string[] { "DemoApp" };
    options.CacheFileName = "LocalizationCache.json";
    options.UseOnlyReviewedLocalizationValues = false;
    options.AllowLocalizeFormat = true;
});

builder.Services.AddModelBindingLocalizationFromSource();

builder.Services.AddControllers(config =>
{
    config.Filters.Add<ModelStateLocalizationFilter>();
});

#endregion

// Add services to the container.
builder.Services.AddControllersWithViews()
    .AddDataAnnotationsLocalizationFromSource();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days...
    app.UseHsts();
}

#region Request Localization

var supportedCultures =
    new[] { "en", "en-US", "en-GB", "fr", "fr-CA" };
var localizationOptions = new RequestLocalizationOptions()
    .SetDefaultCulture(supportedCultures[0])
    .AddSupportedCultures(supportedCultures)
    .AddSupportedUICultures(supportedCultures);

app.UseRequestLocalization(localizationOptions);

#endregion

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "area",
    pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
```
_Listing 4: Program.cs_

A “Localization” region was added containing most of the localization service configuration.

One point to notice outside the region: the call to the `AddDataAnnotationsLocalizationFromSource()` method chained to the `AddControllersWithViews()` method call. Take care to call the AddDataAnnotationsLocalization**FromSource**() method and not the regular AddDataAnnotationsLocalization() method. The “FromSource” method suffix indicates we are using localization from some configurable source, here from our database.

There is also a “Request Localization” region where request localization is configured in a common way. The supported cultures are those that will be supported for requests. As we will see, our localization database does not need to match those cultures exactly.

Finally, there is also a controller route named “area” added. This is required to access the localization dashboard that is implemented as an MVC area.

### Step 5: Unsecure the Localization Dashboard

Talking about the localization dashboard, it comes out of the box with rolebased security granting access only to users in the roles _Administrator_ or _LocalizeAdministrator_.

Since we have not set up authentication for this demo application, we won’t have authenticated users, let alone users in a role.

The easiest for now is to remove this security. Edit the _BaseController.cs_ file in the _Areas/MvcDashboardLocalize/Controllers_ folder and comment the code line containing the `[Authorize]` attribute (that would be line 11).

### Step 6: Build the Database

As the last step, we have to build the database: execute database migrations so the tables to hold localization data are created.

Go back to the _Package Manager Console_ and execute the following command:

`Update-Database -Context LocalizeDbContext`

## Entering Localization Data

After all these steps, our application is now not yet localized, but ready to be localized.

Nevertheless, with the right data, some parts of the application would already be localized. Let’s run the application and navigate once more to the /Home/UpdatePerson page. Nothing really changed on the page, but if you look at the console output (**Figure 2**), you will see several warnings indicating that a localization key was probed.

![Console output showing probed localization keys](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hfvygg4wywcmeneijm5p.png)_Figure 2: Console output showing probed localization keys_

We can define those keys in the localization dashboard: navigate to <u>/MvcDashboardLocalize</u>. Again, you may choose to add a link to this URL in __Layout.cshtml_ for your convenience. You should now see the dashboard home page (**Figure 3**):

![MvcDashboardLocalize home page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n6s9a46vj6deg66asybw.png)_Figure 3: MvcDashboardLocalize home page_

On top of the page, you have links to the three concepts of this database localization component: **domains**, **keys** and **queries**. In this article, we will cover domains and keys.

## Localization Domains

Localization Domains are sets of localization keys and queries. Several domains can be combined to localize an application. And domains can be shared over applications. You could for instance create a “Base” domain containing generic keys as “Yes”, “No”, “OK”, “Cancel”, etc. and have more specific domains tight to specific applications.

Looking back to **Listing 4**, you will notice we have defined a Domains option value as a strings array containing the single string “DemoApp”. So that is the domain we will use in this application.

When multiple domains are listed, succeeding domains can override keys of preceding domains and the last domain determines the supported cultures. The default culture is the first culture of the last domain.

Notice that request localization options also define supported cultures and a default culture. These are about the culture set as current on the thread when executing a web request.

The cultures on domains are the cultures or languages for which the domain provides translations.

To define our “DemoApp” domain, click on _Domains_ in the navigation bar of the localization dashboard, then click on the _New_ link. Enter the name of the domain as well as the list of supported cultures (comma separated) as in **Figure 4**. And press _Save_:

![Creating a domain in MvcDashboardLocalize](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qy4thnljti90xrgfifqc.png)_Figure 4: Creating a domain in MvcDashboardLocalize_

Domains can also be exported and imported as JSON file, making it easy to share localization data over systems.

## Localization Keys

Now that we have our domain created, we can start adding keys to it. Click on Keys, then press the _New_ button (or the <kbd>+</kbd> key on your keyboard).

We will have to enter a name as well as a value for the “en” and “fr” culture. We can also enter a value for the “fr-CA” culture, but the localization component will always fallback to parent cultures when a value is not defined, so we must only provide a value for the “fr-CA” culture if it is different from that of the “fr” culture.

Leave the fields for path, Argument names, etc. empty for now.

From Figure 2, it appears that keys for “First Name” (with a space) and “LastName” (without space) are already queried. Let us create those keys to start with, according to the values in this table:

| Key name   | "en" Value | "fr" Value     |
| ---------- | -----------| -------------- |
| First Name | First name | Prénom         |
| LastName   | Last name  | Nom de famille |

When both keys are created, we go back to the dashboard home page (by clicking on _MvcDashboardLocalize_ in the navigation bar) and we press the _Publish_ button.

We can now test the result: click on the exit icon to the right of the navigation bar to go back to the root web application, and navigate to the <u>/Home/UpdatePerson</u> page.

To see the page in a specific culture, for instance French, add the “<u>?culture=fr</u>” query string to the URL.

You will now see that all occurrences of _FirstName_ and _LastName_ are localized, except for one, the label of the _LastName_ field. That is because the `LastName` property on the `PersonModel` class does not have a `[Display]` attribute defining a localizable name. We will fix that later.

You will also notice that languages are mixed-up resulting in error messages as “_The Prénom field is required_”. If you look back to **Figure 2** (or look at the current console output), you will see that “_The {0} field is required._” is also a key for which translations can be provided.

You can go back to the localization dashboard and define keys with values for all attempted messages. Make sure the name of the key matches the message exactly, including the period at the end of the sentence. Don’t forget to publish before testing. And if you do create all missing keys, you may notice a warning on the console for trying to translate “_Prénom_” (French for First Name). I’ll explain...

## Under the Hood

So far, we haven’t changed our view to add localization and yet several labels and error messages are now localized. Now look at the _FirstName_ field label. It is described in the view as:

`<label asp-for="FirstName" class="form-label"></label>`

The tag has no content. The content is generated by the tag itself based on the `asp-for` expression which points to a property (`FirstName`) that has a `[Display]` attribute setting the property display name. This is important as .NET will never try to localize a property name, but will try to localize the display name of a property.

This is why the _First Name_ label gets localized while the other labels don’t.

For error messages, the explanation is a little more complex. We must first understand that there are three sources of error messages that can appear in the `ModelState` (apart from the ones we add ourselves from code):

1. Error messages defined in the `ErrorMessage` property of a data annotation attribute.
   For instance the “_Please enter your age._” message defined in the `[Range]` attribute on the `Age` property of the `PersonModel` class.
2. Error messages originating from data annotation attributes that do not have an `ErrorMessage` property set. For instance, the `[Required]` and `[EmailAddress]` attributes on the `PersonModel` class.
3. Error messages generated by the model binder, for instance, if you leave the _Age_ field empty, or enter a non-numeric value.

Messages from the first source are the easiest ones: the `ErrorMessage` property value is also the localization key.

Messages from the third source are originating from the model binder. These are a fixed, limited set of (11) translatable messages. You can find the list of messages at [this link](https://www.waretec.at/localizing-asp-net-core-5-apis-statically-typed/).

Messages from the second source are different. These messages, generated by data annotation attributes, are not translatable. Not if you are using resource files and not if you are using database localization. .NET simply does not allow their localization.

So some trickery is needed. You may have noticed that a `ModelStateLocalizationFilter` controller filter was added in the application setup (**Listing 4**). This filter will search the `ModelState` for error messages and try to find a matching pattern from the _ModelStateLocalization.json_ file that was added to the root of our project. If a (regular expression pattern) match is found, the associated localization key is used.

And now comes the complex part. Most of these localization keys contain positional arguments (such as “{0}” or “{1}”). Some positional arguments stand for literal user input and should not be localized. Other positional arguments stand for the field name and may or may not already be localized, we don’t know. If the property related to this field has a `[Display]` attribute, it will already be localized. Otherwise not.

In the default setup, the assumption is made that if data annotations do not contain `ErrorMessage` property values, the model properties would also not have `[Display]` attributes. And thus whenever a field name is expected, it still must be localized. This explains the attempt to localize “_Prénom_”.

One solution would therefore be to remove the `[Display]` attribute on the `FirstName` property.

Another option is to have a `[Display]` attribute on every property, and tell the system that all field names are already localized. You do so by changing all true values in false values in the `ArgLocalization` properties of the _ModelStateLocalization.json_ file.

A last option is to remove the positional argument referring to the field name from the translation all together. The translation for “_The {0} field..._” would then become “_This field..._”. (You should then also switch validation summaries to “`ModelOnly`” mode.)

But we aren’t there yet. The `[EmailAddress]` data annotation attribute isn’t playing to those rules. Though it has no `ErrorMessage` property value, its error message will still be localized as if it had one. But because the `EmailAddress` property has no `[Display]` attribute, the localized error message contains the unlocalized field name.

To solve this problem, we best simply disable `DataAnnotationLocalizationFromSource`. We do this by removing the call to this method from the application configuration (**Listing 4**). The data annotation error message will then not be localized. And the fallback `ModelStateLocalizationFilter` will have its chance to localize the message, including a localized version of the field name.

And yet, this causes another problem: custom error messages on data annotations won’t get localized anymore. As a result, the error message for the `[Range]` annotation on the `Age` property will not be localized.

It is quite a Gordian knot...

Luckily, there is a solution that works in every situation:

- Do not use `[Display]` attributes on model properties.
- Do not enable `DataAnnotationLocalizationFromSource`.
- Do not call `AddModelBindingLocalizationFromSource()` either.
- Extend the _ModelStateLocalization.json_ file with the error messages that are not yet localized (such as those from the `[Range]` annotation on the `Age` property).
- Provide explicit content to `<label>` elements (see Localizing Views).

With this solution, we disable localization of error messages upfront and wait from them to appear (unlocalized) in the `ModelState`. We then use the `ModelStateLocalizationFilter` to localize all messages.

## More on Keys

While entering keys, you probably noticed the option to switch between Plain text and HTML. This really only impacts automatic translation APIs which are informed of the text format to translate. But switching to HTML also activates some HTML helpers on the value editors as well as a preview feature.

The localization value fields allow multiple lines of text. They will automatically expand in height if needed.

You also noticed the “_Is reviewed_” checkbox underneath each translation value. This allows you to keep track of which translations have been reviewed and which may still need reviewing. In our application, this setting does not currently matter as we have set the option `UseOnlyReviewedLocalizationValues` to **false** in the application setup (Listing 4) but if left to true (its default), non-reviewed values are not published.

Also blank localization values are considered non-existent. If a localization value is meant to be just blank, it must be marked as reviewed to be taken into consideration.

## Localizing Views

Localizing error messages is really only one part of the localization effort for views. Gross of the effort will be to localize each text, caption and message. Whether the localization data comes from resource files or from database makes no difference to the localization code: we use a `ViewLocalizer` either way.

To localize the _UpdatePerson.cshtml_ view, we add code on top of the file to inject a `ViewLocalizer`:

```html
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
```

From then on, we replace every static text into a call to the indexer property of the `Localizer`. For instance:

`<strong>Following errors have occured:</strong>`

becomes:

`<strong>@Localizer["Following errors have occured:"]</strong>`

For field labels that stand for model properties that do not have `[Display]` attributes and therefore are not localized, we can provide an explicit label content. And if we make use of this occasion to add a semi-colon and an asterisk for mandatory fields, it is even a good argument to do this for already localized fields too (such as _FirstName_) eliminating the need to have `[Display]` attributes on our model properties all together.

Our _FirstName_ field label now becomes:

```html
<label asp-for="FirstName" class="form-label">
   @Localizer["First Name"]*:
</label>
```

(Or use the localization key “_FirstName_” (without space) if you remove the `[Display]` attribute and also rename the key definition in the database.)

Do not forget to define the localization keys in the database by using the dashboard. Then publish for the changes to take effect.

## Localizing Controllers

You can also access localizers from controllers. Here again, little difference between localization with resource files or with a database as source: we inject `StringLocalizers` and/or `HtmlLocalizers` in the constructor method of the controller and then consume them from the action methods.

One difference however: localization from database does not support segregation by type: localization keys and values cannot be related to a .NET class as can be done with resource files.

Therefore, there is also no need to use the generic variants of `IStringLocalizer` and `IHtmlLocalizer` (though you can, but it makes no difference), and there is also no need to inject multiple localizers (as is often done with resource files: a localizer for shared resources and one for type-related resources).

To inject an `IStringLocalizer` in the `HomeController` of our application, we extend the parameter list of the `HomeController` constructor method and define an instance field to hold the injected object. The existing `_logger` field, added extra field and updated constructor method will look like:

```c#
private readonly ILogger<HomeController> _logger;
private readonly IStringLocalizer _localizer;

public HomeController(ILogger<HomeController> logger, IStringLocalizer localizer)
{
    _logger = logger;
    _localizer = localizer;
}
```

We could now for instance localize the Welcome message in the `Index` action controller method:

```c#
public IActionResult Index()
{
    ViewBag.Message = _localizer["Welcome"];
    return View();
}
```

And in the _Index.cshtml_ view, we replace the word “Welcome” by a call into the `ViewBag`:

```html
<code><h1 class="display-4">@ViewBag.Message</h1></code>
```

No need to inject and use an `IViewLocalizer` as the message is already localized by the controller.

## Named vs. Positional Arguments

In .NET Core, localizer indexers can take additional arguments that will substitute positional arguments in the localized message. Take for instance:

`ViewBag.Message = _localizer["Welcome", "Jane"];`

If the “Welcome” key translates into the string “Welcome {0}!”, then the positional argument {0} will be replaced by the value “Jane”.

Within the localization dashboard, we can create values containing positional arguments, as in:

`Welcome {0}!`

But we can also choose to use named arguments instead:

`Welcome {{username}}!`

(Notice that named arguments are surrounded by doubled curly braces.)

When doing so, we need to declare the list of named arguments in the _Named Arguments_ field of the key, in the order of their corresponding positional arguments. So we will have to declare “username” as first (and only) named argument.

Named arguments can ease the work for translators (especially when a value contains several arguments). Named arguments are a thing of the dashboard and the database, they are not exposed to the localized application and they cannot be used in the name of the localization key.

## Accessing Culture, Model, ViewData...

Another feature unique to localization with the **MvcDashboardLocalize** component, is the ability for localization values to refer to the current culture name, route segments, model properties, ViewData properties and the name of the currently logged in user.

> Another unique feature is the ability to refer to the current culture name, route segments, model properties and ViewData properties.

To illustrate this, let’s rewrite the `Index` action method of the `HomeController` into:

```c#
public IActionResult Index()
{
    ViewBag.User = new PersonModel { FirstName = "Jane" };
    return View();
}
```

Next, in the _Index.cshtml_ view, we will inject a `localizer` and use it to retrieve the Welcome message:

```html
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
  <h1 class="display-4">@Localizer["Welcome"]</h1>
  <p>@Localizer["Learn about ASP.NET Core"]</p>
</div>
```

Now we update the translation values of the “Welcome” key to be (for English):

`Welcome {{view:User.FirstName}}!`

After publishing the changes in localization data, the home page will now display “Welcome Jane!”.

The “`User.FirstName`” expression is a property path: an expression made of property calls. The `localizer` will not search for a `ViewData["User.FirstName"]` value, but for a `ViewData["User"]` value, and will then invoke the `FirstName` property on it. Only properties are supported, not methods.

Similarly, model properties can be accessed. For instance, if the view had a model of type `PersonModel`, localization values could embed the first name of the person using “`{{model:FirstName}}`”.

The view and model expressions can also include a formatting string, i.e., a label on a _Pay_ button:

`Pay {{view:Price:#,##0.00}}`

`ViewData` and model data are usable only from within views by means of a `ViewLocalizer`.

The following references can be included in translation values:

- **`{{culture:name}}`**
  Refers to the current request culture name, i.e., “en-US”.

- **`{{uiculture:name}}`**
  Refers to the current request UI culture name, i.e., “en-US”.

- **`{{route:<segment>}}`**
  Refers to a segment of the current request route (i.e., “`{{route:controller}}`” refers to the controller route segment.

- **`{{user:name}}`**
  Refers to the name of the currently logged in identity.

- **`{{localizer:<localizationkey>}}`**
  Recursively refers to another localization key.

- **`{{model:<propertypath>}}`**
  Refers to a view model property. (*)

- **`{{view:<propertypath>}}`**
  Refers to a ViewData property. (*)

(*) _available only in views using a ViewLocalizer._

## ForPath

The ability to segregate localization keys by request path is another powerful feature of the **MvcDashboardLocalize** component.

> The ability to segregate localization keys by request path is another powerful feature.

Imagine we want pages on our site to have a title on top. Of course, the title has to be localized. Since it is something common to all pages, we can define a “_PageTitle_” localizer section in the shared __Layout.cshtml_ page. For instance, just above the `@RenderBody()` call:

```html
<div class="container">
  <main role="main" class="pb-3">
    @Localizer["PageTitle"]
    @RenderBody()
  </main>
</div>
```

We’ll have to inject an `IViewLocalizer` too.

When running our application, we now see a literal “_PageTitle_” on top of every page as expected: when no matching keys are found in the database, the key text itself is rendered.

So let us go to the localization dashboard and create a key “_PageTitle_” with all values empty and all “Is reviewed” checkboxes checked. We’ll save it and republish. And see, the literal “_PageTitle_” has disappeared as it renders an empty string.

From now on, we can create new localization keys with the name “_PageTitle_” specific to certain URLs. Create, for instance, a “_PageTitle_” key with the value “<u>/Home/UpdatePerson</u>” in the _ForPath_ field, of type HTML, with as value (for English):

`<h1>Update Person</h1>`

We can create another key named “_PageTitle_” with ForPath “<u>/Home/Privacy</u>” and (English) value:

`<h1>{{view:Title:localize}}</h1>
<p>Use this page to detail your site's privacy policy.</p>`

Save and publish. And remove the corresponding code from the _Privacy.cshtml_ file so it now only contains:

```html
@{
    ViewData["Title"] = "Privacy Policy";
}
```

Notice the special “_localize_” format string in the `{{view:Title:localize}}` expression: it tells the localizer to also localize the outcome of ViewData["Title"], returning a localized version of the “Privacy Policy” string.

With this, we can manage our whole privacy policy page from the localization dashboard.

Whenever a same key exists with _ForPath_ values, the system will choose the key with the longest path that matches the start of the current request URL. Matching is case-insensitive.

If the route contains a “_culture_” or “_uiculture_” segment, as is the case when using the `RouteDataRequestCultureProvider`, that route segment will be ignored.

## The Cache File

By now, you have run the web application several times and you probably noticed that the application does not hit the database each time it needs to localize strings. In fact, on the next run, the application will probably not hit the database at all.

> On the next run, the application will probably not hit the database at all.

This is possible thanks to the cache file of which we have configured the name with the `CacheFileName` option in **Listing 4**.

When the application starts, it first tries to read localization data from the _LocalizationCache.json_ cache file. If the file is found, the data is loaded in memory and consumed from there for the remaining lifetime of the application.

Only if the cache file is not found will the application read all localization data (for the domains of the current application) into memory. It will then also write the cache file for the next start.

When we hit the _Publish_ button in the localization dashboard, something similar happens: the localization data is flushed from memory, data is reloaded from the database into memory and also written out to the cache file.

This means we can deploy localization in several modes:

1. The localized application also hosts the localization dashboard. This is the “full” mode we have been using in this sample.
2. The application hosts the dashboard but may itself not be localized. The dashboard is hosted to allow maintaining localized data for other applications. The “dashboard only” mode.
3. The application is localized, but does not host the dashboard: the “localized without dashboard” mode.
   To refresh localization data, the localization cache file is deleted and the application(pool) recycled.
4. The application is localized, hosts no dashboard and has no database access: the “localized without database access” mode.
   To refresh localization data, a new cache file is dropped in place and the application(pool) is recycled.

If we do not provide a `CacheFileName`, the localization data is retrieved from database at each start. Have the application(pool) restarts once a day and you are sure the application does refresh its localization data every day...

## Translation Services

Last but not least, the **MvcDashboardLocalize** component also offers integration with the automatic translation APIs of Bing (Microsoft), Google and DeepL.

> The **MvcDashboardLocalize** component also offers integration with the automatic translation APIs of Bing (Microsoft), Google and DeepL.

All three APIs require you to create an account and get an API key. And all three APIs offer free translation volumes. Bing for instance, offers to translate up to 2 million characters per month for free at the time of writing.

Once you have created an account on Azure and created an authentication key for the Translator service, you can integrate the translation service as follows:

To the application configuration code (**Listing 4**), add the following additional service registration:

```c#
builder.Services.AddTransient<ITranslationService,
                              BingTranslationService>();
```

Then right-click the project file and choose “_Manager User Secrets_”. If you have no user secrets for this application yet, this opens a JSON file with only curly braces. Between those braces enter following configuration:

```json
"BingApi": {
   "SubscriptionKey": "0a123bc45678d90efa12345bc6789...",
   "Region": "eastus"
}
```

Overwrite the subscription key with the authentication key you have obtained from Microsoft, and set the region code (not name) where your Translator service is running.

Next time you edit a localization key in the dashboard, you will notice a panel offering to generate translations for all empty non-reviewed values from the language of your choice.

The _ReadMe-MvcDashboardLocalize.html_ file located in the _Area\MvcDashboardLocalize_ folder provides details about how to integrate with Google and DeepL APIs as well.

## Videos

On the Nuget homepage of the **MvcDashboardLocalize** component, you will also find links to two tutorial videos where the dashboard component is installed and used to localize an ASP.NET Core MVC application, including translation service:

[https://www.nuget.org/packages/Arebis.MvcDashboardLocalize/](https://www.nuget.org/packages/Arebis.MvcDashboardLocalize/)

## Summary

The **MvcDashboardLocalize** component provides a very complete solution to localize ASP.NET Core (MVC) applications. Localization data is stored in a database which makes it accessible, while high performance is guaranteed by means of a cache file.

In addition, the whole solution is open source. The dashboard code is embedded in your project and can easily be tailored to your needs, while the source code of the referred NuGet components is available on GitHub.

## More

To manage the local identity store of ASP.NET Core projects, you may also be interested in using the MvcDashboardIdentity:

[https://www.nuget.org/packages/Arebis.MvcDashboardIdentity](https://www.nuget.org/packages/Arebis.MvcDashboardIdentity)