DEV Community

codetuner
codetuner

Posted on • Originally published at codeproject.com

Localizing ASP.NET Core MVC Applications from Database

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:

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; }
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

// UpdatePerson action on HomeController:
public IActionResult UpdatePerson(PersonModel model)
{
    return View(model);
}
Enter fullscreen mode Exit fullscreen mode

Listing 2: HomeController UpdatePerson action

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

@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>
Enter fullscreen mode Exit fullscreen mode

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 /Home/UpdatePerson 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 applicationFigure 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/

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:

"ConnectionStrings": {

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

}
Enter fullscreen mode Exit fullscreen mode

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:

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();
Enter fullscreen mode Exit fullscreen mode

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 keysFigure 2: Console output showing probed localization keys

We can define those keys in the localization dashboard: navigate to /MvcDashboardLocalize. 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 pageFigure 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 MvcDashboardLocalizeFigure 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 + 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 /Home/UpdatePerson page.

To see the page in a specific culture, for instance French, add the “?culture=fr” 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.

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:

@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
Enter fullscreen mode Exit fullscreen mode

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:

<label asp-for="FirstName" class="form-label">
   @Localizer["First Name"]*:
</label>
Enter fullscreen mode Exit fullscreen mode

(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:

private readonly ILogger<HomeController> _logger;
private readonly IStringLocalizer _localizer;

public HomeController(ILogger<HomeController> logger, IStringLocalizer localizer)
{
    _logger = logger;
    _localizer = localizer;
}
Enter fullscreen mode Exit fullscreen mode

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

public IActionResult Index()
{
    ViewBag.Message = _localizer["Welcome"];
    return View();
}
Enter fullscreen mode Exit fullscreen mode

And in the Index.cshtml view, we replace the word “Welcome” by a call into the ViewBag:

<code><h1 class="display-4">@ViewBag.Message</h1></code>
Enter fullscreen mode Exit fullscreen mode

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:

public IActionResult Index()
{
    ViewBag.User = new PersonModel { FirstName = "Jane" };
    return View();
}
Enter fullscreen mode Exit fullscreen mode

Next, in the Index.cshtml view, we will inject a localizer and use it to retrieve the Welcome message:

@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>
Enter fullscreen mode Exit fullscreen mode

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:

<div class="container">
  <main role="main" class="pb-3">
    @Localizer["PageTitle"]
    @RenderBody()
  </main>
</div>
Enter fullscreen mode Exit fullscreen mode

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 “/Home/UpdatePerson” 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 “/Home/Privacy” 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:

@{
    ViewData["Title"] = "Privacy Policy";
}
Enter fullscreen mode Exit fullscreen mode

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:

builder.Services.AddTransient<ITranslationService,
                              BingTranslationService>();
Enter fullscreen mode Exit fullscreen mode

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:

"BingApi": {
   "SubscriptionKey": "0a123bc45678d90efa12345bc6789...",
   "Region": "eastus"
}
Enter fullscreen mode Exit fullscreen mode

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/

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

Top comments (0)