<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: codetuner</title>
    <description>The latest articles on DEV Community by codetuner (@codetuner).</description>
    <link>https://dev.to/codetuner</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F983648%2F01706d13-e8a5-472d-9b8f-25d166f2434b.jpeg</url>
      <title>DEV Community: codetuner</title>
      <link>https://dev.to/codetuner</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/codetuner"/>
    <language>en</language>
    <item>
      <title>Build Rich Web Apps with ASP.NET Core and Sircl — Part 1</title>
      <dc:creator>codetuner</dc:creator>
      <pubDate>Mon, 23 Oct 2023 13:18:01 +0000</pubDate>
      <link>https://dev.to/codetuner/build-rich-web-apps-with-aspnet-core-and-sircl-part-1-2d2a</link>
      <guid>https://dev.to/codetuner/build-rich-web-apps-with-aspnet-core-and-sircl-part-1-2d2a</guid>
      <description>&lt;p&gt;In this series we will see how to build interactive web applications in ASP.NET Core with the help of Sircl.&lt;/p&gt;

&lt;p&gt;In this first article of the series we will cover simple dynamic forms and see how we can write them in a better way using Sircl. We'll introduce Sircl and Event-Actions and tell you how to install and get started with this library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In this series we will see how to build great interactive web applications – applications that typically require extensive amounts of JavaScript code or are written in JavaScript frameworks – easily with only ASP.NET Core and Sircl.&lt;/p&gt;

&lt;p&gt;Sircl is an open-source client-side library that extends HTML to provide partial updating and common behaviours and makes it easy to write rich applications relying on server-side rendering.&lt;/p&gt;

&lt;p&gt;In each part of this series we will cover a “programming problem” typical to rich web applications using server-side technology, and see how we can solve this problem in ASP.NET Core using Sircl.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dynamic Forms
&lt;/h2&gt;

&lt;p&gt;For this first part I’ve chosen a very frequent problem: dynamically changing an input form based on entered data. Take for instance the checkout page of a web shop asking for your billing address and delivery address. But where you only need to enter one if both are the same. This form could look like:&lt;/p&gt;

&lt;p&gt;When the user checks the “Deliver to same address” checkbox, the delivery address fields are hidden.&lt;/p&gt;

&lt;p&gt;A classic approach is to use a JavaScript event handler to hide or show. The following code is an example of such a handler written using jQuery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$(function () {
    $("#IsSameAddress").on("change", function (e) {
        if ($("#IsSameAddress").prop("checked"))
            $("#DeliverySection").hide();
        else
            $("#DeliverySection").show();
    });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In addition, if the initial state of the form depends on existing data, we must code it’s initial state which is typically done in the ASP.NET Razor view. For instance, on the DeliverySection element, we set the display style to none if the addresses are the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;fieldset id="DeliverySection" style="@(Model.IsSameAddress ? "display: none;" : null)"&amp;gt;
    &amp;lt;legend&amp;gt;Delivery address&amp;lt;/legend&amp;gt;
    ...
&amp;lt;/fieldset&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This could be the complete code of our view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@model CheckoutModel

@section Scripts
{
    &amp;lt;script&amp;gt;
        $(function () {
            $("#@Html.IdFor(m =&amp;gt; m.IsSameAddress)").on("change", function (e) {
                if ($("#@Html.IdFor(m =&amp;gt; m.IsSameAddress)").prop("checked")) {
                    $("#DeliverySection").hide();
                } else {
                    $("#DeliverySection").show();
                }
            });
        });
    &amp;lt;/script&amp;gt;
}

&amp;lt;form method="post" asp-action="Next"&amp;gt;
    &amp;lt;fieldset&amp;gt;
        &amp;lt;legend&amp;gt;Billing address&amp;lt;/legend&amp;gt;
        &amp;lt;div class="mb-3"&amp;gt;
            &amp;lt;label asp-for="BillingName" class="form-label"&amp;gt;Name: *&amp;lt;/label&amp;gt;
            &amp;lt;input type="text" class="form-control" asp-for="BillingName"&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class="mb-3"&amp;gt;
            &amp;lt;label asp-for="BillingAddress" class="form-label"&amp;gt;Address&amp;lt;/label&amp;gt;
            &amp;lt;textarea asp-for="BillingAddress" class="form-control" rows="3"&amp;gt;&amp;lt;/textarea&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class="form-check"&amp;gt;
            &amp;lt;input class="form-check-input" type="checkbox" asp-for="IsSameAddress"&amp;gt;
            &amp;lt;label class="form-check-label" asp-for="IsSameAddress"&amp;gt;
                Deliver to same address
            &amp;lt;/label&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/fieldset&amp;gt;

    &amp;lt;fieldset id="DeliverySection" style="@(Model.IsSameAddress ? "display: none;" : null)"&amp;gt;
        &amp;lt;legend&amp;gt;Delivery address&amp;lt;/legend&amp;gt;
        &amp;lt;div class="mb-3"&amp;gt;
            &amp;lt;label asp-for="DeliveryName" class="form-label"&amp;gt;Name: *&amp;lt;/label&amp;gt;
            &amp;lt;input type="text" class="form-control" asp-for="DeliveryName"&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class="mb-3"&amp;gt;
            &amp;lt;label asp-for="DeliveryAddress" class="form-label"&amp;gt;Address&amp;lt;/label&amp;gt;
            &amp;lt;textarea asp-for="DeliveryAddress" class="form-control" rows="3"&amp;gt;&amp;lt;/textarea&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/fieldset&amp;gt;

    &amp;lt;button type="submit" class="btn btn-primary"&amp;gt;Next&amp;lt;/button&amp;gt;

&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Somehow we need to write the logic to determine whether the DeliverySection is to be visible twice: we combine Razor code and CSS syntax (because the jQuery show/hide plays with the display style property) to set the initial state of the DeliverySection (visible or not) by means of a style attribute.&lt;br&gt;
And we write JavaScript/jQuery code to transition the state from visible to hidden and back when the checkbox changes.&lt;/p&gt;

&lt;p&gt;Not only are those pieces of code written in different languages with different semantics. Both pieces of code are also placed in separate locations. Partly because best practices dictate not to place the transition code as inline event handler(1), but also because one piece of code is to be placed on the trigger element (the checkbox) while the other piece is to be placed on the dynamic element (the section). And one piece of code runs on the server while the other runs on the client.&lt;/p&gt;

&lt;p&gt;In this simple, yet common example we trample on 2 best practices: that of writing code once (DRY versus WET code)(2), and that of striving for high cohesion and low coupling(3) (by putting code with a same purpose on different locations).&lt;/p&gt;

&lt;p&gt;Moreover, the code we wrote is not reusable because it is intertwined with Id’s and CSS selectors to specific elements. Bet next time similar functionality is required your fellow developer (not you of course) will copy &amp;amp; paste the code, resulting in large amounts of repetitive code hard to maintain.(4)&lt;/p&gt;

&lt;p&gt;Imagine writing a large application frontend with many complex user interface element dependencies in this way...&lt;/p&gt;
&lt;h2&gt;
  
  
  Introducing Sircl
&lt;/h2&gt;

&lt;p&gt;Sircl is a free and open-source library written in JavaScript (you just install it by adding a reference to the script file(s)) that extends HTML with extra attributes and classes. The attributes usually take a CSS selector or URL as value. No need to learn a new language or syntax.&lt;/p&gt;

&lt;p&gt;The idea behind Sircl is to let the rendering be done server-side, by your Razor views (or PHP pages, JSP/JSF views, NodeJS views, etc. as Sircl does not depend on the server-side technology you choose). It does so by using AJAX to send HTML-over-the-wire behind the scenes.&lt;/p&gt;

&lt;p&gt;However, to avoid excessive server roundtrips, Sircl also comes with an extensive (and extensible) library of default client-side behaviours.&lt;/p&gt;

&lt;p&gt;Ultimately, Sircl allows you to write full Single-Page Applications with routing and deep-linking support.&lt;/p&gt;

&lt;p&gt;It comes with extensions for Bootstrap, Toastr and SortableJS support, libraries that may come in handy when building rich web applications.&lt;/p&gt;

&lt;p&gt;You can find all about Sircl on the following site:&lt;br&gt;
&lt;a href="https://www.getsircl.com/"&gt;https://www.getsircl.com/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Installing Sircl
&lt;/h2&gt;

&lt;p&gt;If you created an “ASP.NET Core Web App (Model-View-Controller)”, you have to edit the _Layout.cshtml file in the &lt;em&gt;Views\Shared&lt;/em&gt; folder.&lt;/p&gt;

&lt;p&gt;If you created an “ASP.NET Core Web App” (without the MVC part), you are using RazorPages and find the _Layout.cshtml file in the &lt;em&gt;Pages\Shared&lt;/em&gt; folder.&lt;/p&gt;

&lt;p&gt;To the &lt;strong&gt;_Layout.cshtml&lt;/strong&gt; file, you add the following lines of code to add the Sircl bundled CSS and Sircl bundled Javascript files. The Sircl Bootstrap file is not required for now, but since the Visual Studio already references Bootstrap and we’ll use it later, you might as well include it right now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;link href="https://cdn.jsdelivr.net/npm/sircl@2.3.3/sircl-bundled.min.css" rel="stylesheet" /&amp;gt;
&amp;lt;script src="https://cdn.jsdelivr.net/npm/sircl@2.3.3/sircl-bundled.min.js"&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script src="https://cdn.jsdelivr.net/npm/sircl@2.3.3/sircl-bootstrap5.min.js"&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case we refer to the files on a CDN (Content Delivery Network), so you do not need to download the files locally. But you can. Find the different ways to install Sircl on:&lt;br&gt;
&lt;a href="https://www.getsircl.com/Doc/v2/GetStarted"&gt;https://www.getsircl.com/Doc/v2/GetStarted&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Event-Actions
&lt;/h2&gt;

&lt;p&gt;As said previously, Sircl extends HTML with additional attributes and classes. Most of those attributes and classes are so called “Event-Actions”. It are attributes that follow the naming schema “&lt;strong&gt;&lt;em&gt;event-action&lt;/em&gt;&lt;/strong&gt;”. For instance the &lt;code&gt;onclick-show&lt;/code&gt; attribute will perform a “&lt;strong&gt;show&lt;/strong&gt;” action when the “&lt;strong&gt;click&lt;/strong&gt;” event is raised (or bubbles) on it’s element. Who or what is to be shown is determined by the value of the attribute which is a CSS selector to the element to show.&lt;/p&gt;

&lt;p&gt;An example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;button type="button" onclick-show="#secret"&amp;gt;Show my secret&amp;lt;/button&amp;gt;
&amp;lt;div id="secret" hidden&amp;gt;I am a Gummy Bear&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can try this out in this CodePen that comes with Sircl already installed:&lt;br&gt;
&lt;a href="https://codepen.io/codetuner-the-lessful/pen/VwgwNWj"&gt;https://codepen.io/codetuner-the-lessful/pen/VwgwNWj&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The naming of Event-Actions makes them easy to remember and to guess. I’m sure you can guess what &lt;code&gt;onclick-hide&lt;/code&gt;, &lt;code&gt;onclick-enable&lt;/code&gt; and &lt;code&gt;onclick-addclass&lt;/code&gt; do.&lt;/p&gt;

&lt;p&gt;Or what to think about &lt;code&gt;onchange-hide&lt;/code&gt;, &lt;code&gt;onsubmit-disable&lt;/code&gt; or &lt;code&gt;onhover-show&lt;/code&gt; ?&lt;/p&gt;

&lt;p&gt;But back to our case. In the JavaScript code we listened to the change event. We could use the &lt;code&gt;onchange-show&lt;/code&gt; or &lt;code&gt;onchange-hide&lt;/code&gt; Event-Actions. But which of them, and how do we know whether the checkbox is checked or not ?&lt;/p&gt;

&lt;p&gt;Therefore we would rather need &lt;code&gt;onchecked-*&lt;/code&gt; and &lt;code&gt;onunchecked-*&lt;/code&gt; event handlers.&lt;/p&gt;

&lt;p&gt;The bad news is there is unfortunately no &lt;code&gt;onchecked-show&lt;/code&gt; or &lt;code&gt;-hide&lt;/code&gt; Event-Action.&lt;/p&gt;

&lt;p&gt;The good news is, there’s better! For our case we best use &lt;code&gt;ifchecked-*&lt;/code&gt; or &lt;code&gt;ifunchecked-*&lt;/code&gt; Event-Actions. The “&lt;strong&gt;if&lt;/strong&gt;” prefix means the action is taken when the event takes place, but also on initialization (when the page is loaded), and that means we will be able to replace our two pieces of code by a single Event-Action!&lt;/p&gt;

&lt;p&gt;In addition, most of the &lt;code&gt;ifchecked-*&lt;/code&gt; Event-Actions are bidirectional: they perform the reverse action when their element is unchecked. And that is exactly what we need here.&lt;/p&gt;

&lt;p&gt;Can you guess the name of the Event-Action we will use ?&lt;/p&gt;

&lt;p&gt;Event-Actions are documented here:&lt;br&gt;
&lt;a href="https://www.getsircl.com/Doc/v2/EventActions"&gt;https://www.getsircl.com/Doc/v2/EventActions&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Dynamic Forms with Sircl
&lt;/h2&gt;

&lt;p&gt;We are now ready to update our web page to implement the dynamic behaviour using Sircl.&lt;/p&gt;

&lt;p&gt;We have installed Sircl and know what Event-Actions are, so we can start updating our view:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Remove the whole Scripts section&lt;/li&gt;
&lt;li&gt;Remove the style attribute on the DeliverySection element&lt;/li&gt;
&lt;li&gt;To the IsSameAddress checkbox element, add following attribute:
&lt;code&gt;ifchecked-hide="#DeliverySection"&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s it. We have removed all procedural JavaScript code and got rid of the double specification that the DeliverySection is to be hidden when both addresses are the same. Instead we have a single declarative specification consisting of a HTML attribute and a CSS selector.&lt;/p&gt;

&lt;p&gt;I’ll get to the full code. But first one more thing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Adding Validation
&lt;/h2&gt;

&lt;p&gt;When both addresses are different, they probably are both &lt;em&gt;required&lt;/em&gt;. If they are both the same, only the billing address is required.&lt;/p&gt;

&lt;p&gt;Basically, all four input fields are required when they are visible. We can use HTML Validation and add a &lt;code&gt;required&lt;/code&gt; attribute on the four fields.&lt;/p&gt;

&lt;p&gt;However, the fact a field is not visible, does not mean its &lt;code&gt;required&lt;/code&gt; attribute will be ignored. Submitting the form will fail if no delivery address is entered, even if the user checked that it is the same as the billing address...&lt;/p&gt;

&lt;p&gt;In the first version using JavaScript code, we could have added code to remove the &lt;code&gt;required&lt;/code&gt; attributes, and put them back, depending on whether the checkbox is checked or not. In JavaScript you can dynamically add, remove or change attributes and tags.&lt;/p&gt;

&lt;p&gt;Sircl behaviours cannot change the document (add or remove attributes or tags) without roundtripping to the server. It relies on the server for the rendering and has very limited capabilities to change the HTML code of the page.&lt;/p&gt;

&lt;p&gt;However there is another option here. While having the &lt;code&gt;required&lt;/code&gt; attributes in place, we can disable the input elements (or the whole fieldset at once for a matter of fact). Because when an input element (or its parent fieldset) is disabled, then the HTML validation attributes (including the required attribute) are ignored.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In fact, disabling the elements is even more appropriate to this situation because of the side-effect its values (would it have some) are not submitted with the form.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So we can safely mark all input elements required provided we add the following Event-Action attribute to the checkbox:&lt;br&gt;
&lt;code&gt;ifchecked-disable="#DeliverySection"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The full code now looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@model CheckoutModel

&amp;lt;form method="post" asp-action="Next"&amp;gt;
    &amp;lt;fieldset&amp;gt;
        &amp;lt;legend&amp;gt;Billing address&amp;lt;/legend&amp;gt;
        &amp;lt;div class="mb-3"&amp;gt;
            &amp;lt;label asp-for="BillingName" class="form-label"&amp;gt;Name: *&amp;lt;/label&amp;gt;
            &amp;lt;input type="text" class="form-control" asp-for="BillingName" required&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class="mb-3"&amp;gt;
            &amp;lt;label asp-for="BillingAddress" class="form-label"&amp;gt;Address&amp;lt;/label&amp;gt;
            &amp;lt;textarea asp-for="BillingAddress" class="form-control" rows="3" required&amp;gt;&amp;lt;/textarea&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div class="form-check"&amp;gt;
            &amp;lt;input class="form-check-input" type="checkbox" asp-for="IsSameAddress"
                   ifchecked-hide="#DeliverySection"
                   ifchecked-disable="#DeliverySection"&amp;gt;
            &amp;lt;label class="form-check-label" asp-for="IsSameAddress"&amp;gt;
                Deliver to same address
            &amp;lt;/label&amp;gt;
        &amp;lt;/div&amp;gt;

    &amp;lt;/fieldset&amp;gt;

    &amp;lt;fieldset id="DeliverySection" hidden&amp;gt;
        &amp;lt;legend&amp;gt;Delivery address&amp;lt;/legend&amp;gt;
        &amp;lt;div class="mb-3"&amp;gt;
            &amp;lt;label asp-for="DeliveryName" class="form-label"&amp;gt;Name: *&amp;lt;/label&amp;gt;
            &amp;lt;input type="text" class="form-control" asp-for="DeliveryName" required&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class="mb-3"&amp;gt;
            &amp;lt;label asp-for="DeliveryAddress" class="form-label"&amp;gt;Address&amp;lt;/label&amp;gt;
            &amp;lt;textarea asp-for="DeliveryAddress" class="form-control" rows="3" required&amp;gt;&amp;lt;/textarea&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/fieldset&amp;gt;

    &amp;lt;button type="submit" class="btn btn-primary"&amp;gt;Next&amp;lt;/button&amp;gt;

&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;You may notice I marked the DeliverySection as initially hidden. Since Sircl Event-Actions are run on the client while rendering has already started, without the hidden attribute, the section would be visible for a fraction of a second if the checkbox was initially checked. Reversely, if the checkbox is initially unchecked, the hidden attribute will cause the Delivery section to appear a fraction of a second later, but this is usually found less disturbing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The complete code of this example (including controller and model) can be downloaded here:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://www.dropbox.com/scl/fi/yfs6b75biop50lqpiwozw/AspNetAndSircl-01-DynamicForms.zip?rlkey=ta6ftnlldg0kmnb38up0m7oos&amp;amp;dl=0"&gt;AspNetAndSircl-01-DynamicForms.zip&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The net result of introducing Sircl is that we can write our dynamic form with less to no JavaScript code, keep the dynamic behaviour, and replace procedural code by simple declarative attributes understandable by any web designer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We didn’t have to change the controller code or the model. Nor did we have to write the view in a different way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next time
&lt;/h2&gt;

&lt;p&gt;In next articles I will disclose more Sircl capabilities for dynamic forms and pages using both client-side behaviours and server-side rendering. We will also see how to handle drag &amp;amp; drop, use Bootstrap modals or native HTML5 dialogs and write Single-Page Applications. All in ASP.NET Core, with Sircl, and without JavaScript code.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;(1) &lt;a href="https://raygun.com/blog/js-security-vulnerabilities-best-practices/#avoidinline"&gt;https://raygun.com/blog/js-security-vulnerabilities-best-practices/#avoidinline&lt;/a&gt;&lt;br&gt;
The JavaScript code should not even be in a Scripts section inside the view but in a separate file, even further from the related elements.&lt;br&gt;
(2) &lt;a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself"&gt;https://en.wikipedia.org/wiki/Don%27t_repeat_yourself&lt;/a&gt;&lt;br&gt;
“The DRY principle is stated as "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system".”&lt;br&gt;
(3) &lt;a href="https://stackoverflow.com/a/14000957/323122"&gt;https://stackoverflow.com/a/14000957/323122&lt;/a&gt;&lt;br&gt;
“... related code should be close to each other ...”&lt;br&gt;
(4) &lt;a href="https://en.wikipedia.org/wiki/Copy-and-paste_programming"&gt;https://en.wikipedia.org/wiki/Copy-and-paste_programming&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@headwayio?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash"&gt;Headway&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/man-sitting-in-front-of-laptop-NWmcp5fE_4M?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aspnet</category>
      <category>html</category>
      <category>sircl</category>
    </item>
    <item>
      <title>Localizing ASP.NET Core MVC Applications from Database</title>
      <dc:creator>codetuner</dc:creator>
      <pubDate>Mon, 05 Dec 2022 20:46:12 +0000</pubDate>
      <link>https://dev.to/codetuner/localizing-aspnet-core-mvc-applications-from-database-59pf</link>
      <guid>https://dev.to/codetuner/localizing-aspnet-core-mvc-applications-from-database-59pf</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The component I present you here – &lt;strong&gt;MvcDasboardLocalize&lt;/strong&gt; – 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.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;strong&gt;MvcDashboardLocalize&lt;/strong&gt; component provides a very complete solution to localize ASP.NET Core (MVC) applications&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But before looking to those features, let’s create an ASP.NET Core application and add localization to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Application
&lt;/h2&gt;

&lt;p&gt;With Visual Studio 2022, we choose to create a new project of type “&lt;strong&gt;ASP.NET Core Web App (Model-View-Controller)&lt;/strong&gt;” 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.&lt;/p&gt;

&lt;p&gt;If all went well, we now have a web application with one controller (&lt;code&gt;HomeController&lt;/code&gt;) and two views in the &lt;em&gt;Views\Home&lt;/em&gt; folder (&lt;em&gt;Index.cshtml&lt;/em&gt; and &lt;em&gt;Privacy.cshtml&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;To the Models folder, we add a &lt;code&gt;PersonModel&lt;/code&gt; class with some fields as &lt;code&gt;FirstName&lt;/code&gt;, &lt;code&gt;LastName&lt;/code&gt;, &lt;code&gt;Age&lt;/code&gt; and &lt;code&gt;EmailAddress&lt;/code&gt;. We also add data annotation attributes in different flavours. The result is the class in &lt;strong&gt;Listing 1&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.ComponentModel.DataAnnotations&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;LocalizationDemo.Models&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PersonModel&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Display&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"First Name"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Required&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;FirstName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Required&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;LastName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;140&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrorMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Please enter your age."&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Age&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;EmailAddress&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;EmailAddress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Listing 1: A model class for our application&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To the HomeController, we add a single action – &lt;em&gt;UpdatePerson&lt;/em&gt; – that receives a &lt;em&gt;PersonModel&lt;/em&gt; and returns a view for that model. The result is a very simple action method shown in &lt;strong&gt;Listing 2&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// UpdatePerson action on HomeController:&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;UpdatePerson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PersonModel&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;View&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Listing 2: HomeController UpdatePerson action&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The view is somewhat more elaborate and is rendered as &lt;strong&gt;Listing 3&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@model PersonModel

&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
    &lt;span class="nc"&gt;.validation-summary-valid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;asp-validation-summary=&lt;/span&gt;&lt;span class="s"&gt;"All"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"alert alert-danger mb-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;Following errors have occured:&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mb-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;asp-for=&lt;/span&gt;&lt;span class="s"&gt;"FirstName"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-label"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;asp-for=&lt;/span&gt;&lt;span class="s"&gt;"FirstName"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-control"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;asp-validation-for=&lt;/span&gt;&lt;span class="s"&gt;"FirstName"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-danger"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mb-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;asp-for=&lt;/span&gt;&lt;span class="s"&gt;"LastName"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-label"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;asp-for=&lt;/span&gt;&lt;span class="s"&gt;"LastName"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-control"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;asp-validation-for=&lt;/span&gt;&lt;span class="s"&gt;"LastName"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-danger"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mb-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;asp-for=&lt;/span&gt;&lt;span class="s"&gt;"Age"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-label"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;asp-for=&lt;/span&gt;&lt;span class="s"&gt;"Age"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-control"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;asp-validation-for=&lt;/span&gt;&lt;span class="s"&gt;"Age"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-danger"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mb-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;asp-for=&lt;/span&gt;&lt;span class="s"&gt;"EmailAddress"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-label"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;asp-for=&lt;/span&gt;&lt;span class="s"&gt;"EmailAddress"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-control"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;asp-validation-for=&lt;/span&gt;&lt;span class="s"&gt;"EmailAddress"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-danger"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-primary"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Submit&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Listing 3: The UpdatePerson view&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You may also want to update the _&lt;em&gt;Layout.cshtml&lt;/em&gt; view in the &lt;em&gt;Shared&lt;/em&gt; folder to add a link to the &lt;code&gt;UpdatePerson&lt;/code&gt; action so you can access the page by clicking on the link instead of typing over the URL each time.&lt;/p&gt;

&lt;p&gt;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 &lt;u&gt;/Home/UpdatePerson&lt;/u&gt; URL, we will see that every field except &lt;em&gt;EmailAddress&lt;/em&gt; has a validation error message. See &lt;strong&gt;Figure 1&lt;/strong&gt;. We now will see several ways to localize these error messages.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--aaT1tMIK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ddpqan8zs1ce8b5pbo72.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--aaT1tMIK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ddpqan8zs1ce8b5pbo72.png" alt="The application" width="848" height="772"&gt;&lt;/a&gt;&lt;em&gt;Figure 1: The application&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Localization
&lt;/h2&gt;

&lt;p&gt;We can now start adding localization to the application by using the &lt;strong&gt;Arebis.MvcDashboardLocalize&lt;/strong&gt; .NET Core Template Package found on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.nuget.org/packages/Arebis.MvcDashboardLocalize/"&gt;https://www.nuget.org/packages/Arebis.MvcDashboardLocalize/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To do so in Visual Studio, go to the &lt;em&gt;Tools&lt;/em&gt; menu, &lt;em&gt;NuGet Package Manager&lt;/em&gt; and open the &lt;em&gt;Package Manager Console&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;First, we must install the package on our machine. We do so with the following command in the Package Manager Console:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dotnet new --install Arebis.MvcDashboardLocalize&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then we install the MvcDashboardLocalize template with the following command:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dotnet new MvcDashboardLocalize -n LocalizationDemo&lt;/code&gt;&lt;/p&gt;

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

&lt;p&gt;If all went well, you received the message:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;The template "ASP.NET MVC Dashboard Localize" was created successfully.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You will also notice a few additions to your project: three folders (&lt;em&gt;Areas&lt;/em&gt;, &lt;em&gt;Data&lt;/em&gt; and &lt;em&gt;Localize&lt;/em&gt;) and one additional file (&lt;em&gt;ModelStateLocalization.json&lt;/em&gt;) have been added.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;Areas&lt;/em&gt; folder contains an &lt;em&gt;MvcDashboardLocalize&lt;/em&gt; folder which contains a &lt;em&gt;ReadMe-MvcDashboardLocalize.html&lt;/em&gt; file. This file contains the remaining setup instructions which we will also follow here:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Add Package Dependencies
&lt;/h3&gt;

&lt;p&gt;To our project, we must add the following NuGet package dependencies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Arebis.Core.AspNet.Mvc.Localization&lt;/li&gt;
&lt;li&gt;Microsoft.EntityFrameworkCore.SqlServer&lt;/li&gt;
&lt;li&gt;Microsoft.EntityFrameworkCore.Tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We add those dependencies by right-clicking the &lt;em&gt;Dependencies&lt;/em&gt; node in our project, choose &lt;em&gt;Manage NuGet Packages&lt;/em&gt;, and install these three packages from the &lt;em&gt;Browse&lt;/em&gt; tab.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Create a Database
&lt;/h3&gt;

&lt;p&gt;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”.&lt;/p&gt;

&lt;p&gt;No need to add any tables. Those will be added in Step 6 by running a migration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Configure the Database Connection String
&lt;/h3&gt;

&lt;p&gt;In the &lt;em&gt;appsettings.json&lt;/em&gt; of our project, we add a connection string to the database we just created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"ConnectionStrings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"DefaultConnection"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Server=(local);Database=LocalizationDemoDb;Trusted_Connection=True;MultipleActiveResultSets=true"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Configure the Application and Application Services
&lt;/h3&gt;

&lt;p&gt;Now that we have our database setup, we can configure our application services. This is done by adding code to the &lt;em&gt;Program.cs&lt;/em&gt; file (since we are using top-level statements). The complete updated &lt;em&gt;Program.cs&lt;/em&gt; file is found in Listing 4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Arebis.Core.AspNet.Mvc.Localization&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Arebis.Core.Localization&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;LocalizationDemo.Localize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.EntityFrameworkCore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="cp"&gt;#region Localization
&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LocalizationDemo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Localize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LocalizeDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;optionsAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSqlServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DefaultConnection"&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddTransient&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ILocalizationSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DbContextLocalizationSource&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddLocalizationFromSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Domains&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"DemoApp"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CacheFileName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"LocalizationCache.json"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseOnlyReviewedLocalizationValues&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AllowLocalizeFormat&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddModelBindingLocalizationFromSource&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddControllers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Filters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ModelStateLocalizationFilter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="cp"&gt;#endregion
&lt;/span&gt;
&lt;span class="c1"&gt;// Add services to the container.&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddControllersWithViews&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDataAnnotationsLocalizationFromSource&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Configure the HTTP request pipeline.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsDevelopment&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseExceptionHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/Home/Error"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// The default HSTS value is 30 days...&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseHsts&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cp"&gt;#region Request Localization
&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;supportedCultures&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"en-US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"en-GB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"fr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"fr-CA"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;localizationOptions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RequestLocalizationOptions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetDefaultCulture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;supportedCultures&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSupportedCultures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;supportedCultures&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSupportedUICultures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;supportedCultures&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseRequestLocalization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;localizationOptions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="cp"&gt;#endregion
&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseHttpsRedirection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseStaticFiles&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseRouting&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseAuthorization&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapControllerRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"area"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{area:exists}/{controller=Home}/{action=Index}/{id?}"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapControllerRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{controller=Home}/{action=Index}/{id?}"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Listing 4: Program.cs&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A “Localization” region was added containing most of the localization service configuration.&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Unsecure the Localization Dashboard
&lt;/h3&gt;

&lt;p&gt;Talking about the localization dashboard, it comes out of the box with rolebased security granting access only to users in the roles &lt;em&gt;Administrator&lt;/em&gt; or &lt;em&gt;LocalizeAdministrator&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Since we have not set up authentication for this demo application, we won’t have authenticated users, let alone users in a role.&lt;/p&gt;

&lt;p&gt;The easiest for now is to remove this security. Edit the &lt;em&gt;BaseController.cs&lt;/em&gt; file in the &lt;em&gt;Areas/MvcDashboardLocalize/Controllers&lt;/em&gt; folder and comment the code line containing the &lt;code&gt;[Authorize]&lt;/code&gt; attribute (that would be line 11).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Build the Database
&lt;/h3&gt;

&lt;p&gt;As the last step, we have to build the database: execute database migrations so the tables to hold localization data are created.&lt;/p&gt;

&lt;p&gt;Go back to the &lt;em&gt;Package Manager Console&lt;/em&gt; and execute the following command:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Update-Database -Context LocalizeDbContext&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Entering Localization Data
&lt;/h2&gt;

&lt;p&gt;After all these steps, our application is now not yet localized, but ready to be localized.&lt;/p&gt;

&lt;p&gt;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 (&lt;strong&gt;Figure 2&lt;/strong&gt;), you will see several warnings indicating that a localization key was probed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--V_-QSm9S--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hfvygg4wywcmeneijm5p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--V_-QSm9S--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hfvygg4wywcmeneijm5p.png" alt="Console output showing probed localization keys" width="880" height="479"&gt;&lt;/a&gt;&lt;em&gt;Figure 2: Console output showing probed localization keys&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We can define those keys in the localization dashboard: navigate to &lt;u&gt;/MvcDashboardLocalize&lt;/u&gt;. Again, you may choose to add a link to this URL in _&lt;em&gt;Layout.cshtml&lt;/em&gt; for your convenience. You should now see the dashboard home page (&lt;strong&gt;Figure 3&lt;/strong&gt;):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--I8hQ5fQo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n6s9a46vj6deg66asybw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--I8hQ5fQo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n6s9a46vj6deg66asybw.png" alt="MvcDashboardLocalize home page" width="880" height="457"&gt;&lt;/a&gt;&lt;em&gt;Figure 3: MvcDashboardLocalize home page&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On top of the page, you have links to the three concepts of this database localization component: &lt;strong&gt;domains&lt;/strong&gt;, &lt;strong&gt;keys&lt;/strong&gt; and &lt;strong&gt;queries&lt;/strong&gt;. In this article, we will cover domains and keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Localization Domains
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Looking back to &lt;strong&gt;Listing 4&lt;/strong&gt;, 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The cultures on domains are the cultures or languages for which the domain provides translations.&lt;/p&gt;

&lt;p&gt;To define our “DemoApp” domain, click on &lt;em&gt;Domains&lt;/em&gt; in the navigation bar of the localization dashboard, then click on the &lt;em&gt;New&lt;/em&gt; link. Enter the name of the domain as well as the list of supported cultures (comma separated) as in &lt;strong&gt;Figure 4&lt;/strong&gt;. And press &lt;em&gt;Save&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ZX5Dkraw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qy4thnljti90xrgfifqc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZX5Dkraw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qy4thnljti90xrgfifqc.png" alt="Creating a domain in MvcDashboardLocalize" width="550" height="374"&gt;&lt;/a&gt;&lt;em&gt;Figure 4: Creating a domain in MvcDashboardLocalize&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Domains can also be exported and imported as JSON file, making it easy to share localization data over systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Localization Keys
&lt;/h2&gt;

&lt;p&gt;Now that we have our domain created, we can start adding keys to it. Click on Keys, then press the &lt;em&gt;New&lt;/em&gt; button (or the + key on your keyboard).&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Leave the fields for path, Argument names, etc. empty for now.&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key name&lt;/th&gt;
&lt;th&gt;"en" Value&lt;/th&gt;
&lt;th&gt;"fr" Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First Name&lt;/td&gt;
&lt;td&gt;First name&lt;/td&gt;
&lt;td&gt;Prénom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LastName&lt;/td&gt;
&lt;td&gt;Last name&lt;/td&gt;
&lt;td&gt;Nom de famille&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When both keys are created, we go back to the dashboard home page (by clicking on &lt;em&gt;MvcDashboardLocalize&lt;/em&gt; in the navigation bar) and we press the &lt;em&gt;Publish&lt;/em&gt; button.&lt;/p&gt;

&lt;p&gt;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 &lt;u&gt;/Home/UpdatePerson&lt;/u&gt; page.&lt;/p&gt;

&lt;p&gt;To see the page in a specific culture, for instance French, add the “&lt;u&gt;?culture=fr&lt;/u&gt;” query string to the URL.&lt;/p&gt;

&lt;p&gt;You will now see that all occurrences of &lt;em&gt;FirstName&lt;/em&gt; and &lt;em&gt;LastName&lt;/em&gt; are localized, except for one, the label of the &lt;em&gt;LastName&lt;/em&gt; field. That is because the &lt;code&gt;LastName&lt;/code&gt; property on the &lt;code&gt;PersonModel&lt;/code&gt; class does not have a &lt;code&gt;[Display]&lt;/code&gt; attribute defining a localizable name. We will fix that later.&lt;/p&gt;

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

&lt;p&gt;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 “&lt;em&gt;Prénom&lt;/em&gt;” (French for First Name). I’ll explain...&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the Hood
&lt;/h2&gt;

&lt;p&gt;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 &lt;em&gt;FirstName&lt;/em&gt; field label. It is described in the view as:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;label asp-for="FirstName" class="form-label"&amp;gt;&amp;lt;/label&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The tag has no content. The content is generated by the tag itself based on the &lt;code&gt;asp-for&lt;/code&gt; expression which points to a property (&lt;code&gt;FirstName&lt;/code&gt;) that has a &lt;code&gt;[Display]&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;This is why the &lt;em&gt;First Name&lt;/em&gt; label gets localized while the other labels don’t.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;ModelState&lt;/code&gt; (apart from the ones we add ourselves from code):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Error messages defined in the &lt;code&gt;ErrorMessage&lt;/code&gt; property of a data annotation attribute.
For instance the “&lt;em&gt;Please enter your age.&lt;/em&gt;” message defined in the &lt;code&gt;[Range]&lt;/code&gt; attribute on the &lt;code&gt;Age&lt;/code&gt; property of the &lt;code&gt;PersonModel&lt;/code&gt; class.&lt;/li&gt;
&lt;li&gt;Error messages originating from data annotation attributes that do not have an &lt;code&gt;ErrorMessage&lt;/code&gt; property set. For instance, the &lt;code&gt;[Required]&lt;/code&gt; and &lt;code&gt;[EmailAddress]&lt;/code&gt; attributes on the &lt;code&gt;PersonModel&lt;/code&gt; class.&lt;/li&gt;
&lt;li&gt;Error messages generated by the model binder, for instance, if you leave the &lt;em&gt;Age&lt;/em&gt; field empty, or enter a non-numeric value.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Messages from the first source are the easiest ones: the &lt;code&gt;ErrorMessage&lt;/code&gt; property value is also the localization key.&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://www.waretec.at/localizing-asp-net-core-5-apis-statically-typed/"&gt;this link&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;So some trickery is needed. You may have noticed that a &lt;code&gt;ModelStateLocalizationFilter&lt;/code&gt; controller filter was added in the application setup (&lt;strong&gt;Listing 4&lt;/strong&gt;). This filter will search the &lt;code&gt;ModelState&lt;/code&gt; for error messages and try to find a matching pattern from the &lt;em&gt;ModelStateLocalization.json&lt;/em&gt; file that was added to the root of our project. If a (regular expression pattern) match is found, the associated localization key is used.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;[Display]&lt;/code&gt; attribute, it will already be localized. Otherwise not.&lt;/p&gt;

&lt;p&gt;In the default setup, the assumption is made that if data annotations do not contain &lt;code&gt;ErrorMessage&lt;/code&gt; property values, the model properties would also not have &lt;code&gt;[Display]&lt;/code&gt; attributes. And thus whenever a field name is expected, it still must be localized. This explains the attempt to localize “&lt;em&gt;Prénom&lt;/em&gt;”.&lt;/p&gt;

&lt;p&gt;One solution would therefore be to remove the &lt;code&gt;[Display]&lt;/code&gt; attribute on the &lt;code&gt;FirstName&lt;/code&gt; property.&lt;/p&gt;

&lt;p&gt;Another option is to have a &lt;code&gt;[Display]&lt;/code&gt; 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 &lt;code&gt;ArgLocalization&lt;/code&gt; properties of the &lt;em&gt;ModelStateLocalization.json&lt;/em&gt; file.&lt;/p&gt;

&lt;p&gt;A last option is to remove the positional argument referring to the field name from the translation all together. The translation for “&lt;em&gt;The {0} field...&lt;/em&gt;” would then become “&lt;em&gt;This field...&lt;/em&gt;”. (You should then also switch validation summaries to “&lt;code&gt;ModelOnly&lt;/code&gt;” mode.)&lt;/p&gt;

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

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

&lt;p&gt;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 &lt;code&gt;[Range]&lt;/code&gt; annotation on the &lt;code&gt;Age&lt;/code&gt; property will not be localized.&lt;/p&gt;

&lt;p&gt;It is quite a Gordian knot...&lt;/p&gt;

&lt;p&gt;Luckily, there is a solution that works in every situation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not use &lt;code&gt;[Display]&lt;/code&gt; attributes on model properties.&lt;/li&gt;
&lt;li&gt;Do not enable &lt;code&gt;DataAnnotationLocalizationFromSource&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Do not call &lt;code&gt;AddModelBindingLocalizationFromSource()&lt;/code&gt; either.&lt;/li&gt;
&lt;li&gt;Extend the &lt;em&gt;ModelStateLocalization.json&lt;/em&gt; file with the error messages that are not yet localized (such as those from the &lt;code&gt;[Range]&lt;/code&gt; annotation on the &lt;code&gt;Age&lt;/code&gt; property).&lt;/li&gt;
&lt;li&gt;Provide explicit content to &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt; elements (see Localizing Views).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this solution, we disable localization of error messages upfront and wait from them to appear (unlocalized) in the &lt;code&gt;ModelState&lt;/code&gt;. We then use the &lt;code&gt;ModelStateLocalizationFilter&lt;/code&gt; to localize all messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  More on Keys
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The localization value fields allow multiple lines of text. They will automatically expand in height if needed.&lt;/p&gt;

&lt;p&gt;You also noticed the “&lt;em&gt;Is reviewed&lt;/em&gt;” 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 &lt;code&gt;UseOnlyReviewedLocalizationValues&lt;/code&gt; to &lt;strong&gt;false&lt;/strong&gt; in the application setup (Listing 4) but if left to true (its default), non-reviewed values are not published.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Localizing Views
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;ViewLocalizer&lt;/code&gt; either way.&lt;/p&gt;

&lt;p&gt;To localize the &lt;em&gt;UpdatePerson.cshtml&lt;/em&gt; view, we add code on top of the file to inject a &lt;code&gt;ViewLocalizer&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From then on, we replace every static text into a call to the indexer property of the &lt;code&gt;Localizer&lt;/code&gt;. For instance:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;strong&amp;gt;Following errors have occured:&amp;lt;/strong&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;becomes:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;strong&amp;gt;@Localizer["Following errors have occured:"]&amp;lt;/strong&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For field labels that stand for model properties that do not have &lt;code&gt;[Display]&lt;/code&gt; 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 &lt;em&gt;FirstName&lt;/em&gt;) eliminating the need to have &lt;code&gt;[Display]&lt;/code&gt; attributes on our model properties all together.&lt;/p&gt;

&lt;p&gt;Our &lt;em&gt;FirstName&lt;/em&gt; field label now becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;asp-for=&lt;/span&gt;&lt;span class="s"&gt;"FirstName"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-label"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
   @Localizer["First Name"]*:
&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Or use the localization key “&lt;em&gt;FirstName&lt;/em&gt;” (without space) if you remove the &lt;code&gt;[Display]&lt;/code&gt; attribute and also rename the key definition in the database.)&lt;/p&gt;

&lt;p&gt;Do not forget to define the localization keys in the database by using the dashboard. Then publish for the changes to take effect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Localizing Controllers
&lt;/h2&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Therefore, there is also no need to use the generic variants of &lt;code&gt;IStringLocalizer&lt;/code&gt; and &lt;code&gt;IHtmlLocalizer&lt;/code&gt; (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).&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HomeController&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IStringLocalizer&lt;/span&gt; &lt;span class="n"&gt;_localizer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;HomeController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HomeController&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IStringLocalizer&lt;/span&gt; &lt;span class="n"&gt;localizer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;_localizer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;localizer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We could now for instance localize the Welcome message in the &lt;code&gt;Index&lt;/code&gt; action controller method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ViewBag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_localizer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Welcome"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;View&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in the &lt;em&gt;Index.cshtml&lt;/em&gt; view, we replace the word “Welcome” by a call into the &lt;code&gt;ViewBag&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;code&amp;gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"display-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;@ViewBag.Message&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&amp;lt;/code&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No need to inject and use an &lt;code&gt;IViewLocalizer&lt;/code&gt; as the message is already localized by the controller.&lt;/p&gt;

&lt;h2&gt;
  
  
  Named vs. Positional Arguments
&lt;/h2&gt;

&lt;p&gt;In .NET Core, localizer indexers can take additional arguments that will substitute positional arguments in the localized message. Take for instance:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ViewBag.Message = _localizer["Welcome", "Jane"];&lt;/code&gt;&lt;/p&gt;

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

&lt;p&gt;Within the localization dashboard, we can create values containing positional arguments, as in:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Welcome {0}!&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;But we can also choose to use named arguments instead:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Welcome {{username}}!&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;(Notice that named arguments are surrounded by doubled curly braces.)&lt;/p&gt;

&lt;p&gt;When doing so, we need to declare the list of named arguments in the &lt;em&gt;Named Arguments&lt;/em&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessing Culture, Model, ViewData...
&lt;/h2&gt;

&lt;p&gt;Another feature unique to localization with the &lt;strong&gt;MvcDashboardLocalize&lt;/strong&gt; 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.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Another unique feature is the ability to refer to the current culture name, route segments, model properties and ViewData properties.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To illustrate this, let’s rewrite the &lt;code&gt;Index&lt;/code&gt; action method of the &lt;code&gt;HomeController&lt;/code&gt; into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ViewBag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PersonModel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;FirstName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Jane"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;View&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, in the &lt;em&gt;Index.cshtml&lt;/em&gt; view, we will inject a &lt;code&gt;localizer&lt;/code&gt; and use it to retrieve the Welcome message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
@{
    ViewData["Title"] = "Home Page";
}

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"display-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;@Localizer["Welcome"]&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;@Localizer["Learn about ASP.NET Core"]&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we update the translation values of the “Welcome” key to be (for English):&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Welcome {{view:User.FirstName}}!&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;After publishing the changes in localization data, the home page will now display “Welcome Jane!”.&lt;/p&gt;

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

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

&lt;p&gt;The view and model expressions can also include a formatting string, i.e., a label on a &lt;em&gt;Pay&lt;/em&gt; button:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Pay {{view:Price:#,##0.00}}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ViewData&lt;/code&gt; and model data are usable only from within views by means of a &lt;code&gt;ViewLocalizer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The following references can be included in translation values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;{{culture:name}}&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Refers to the current request culture name, i.e., “en-US”.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;{{uiculture:name}}&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Refers to the current request UI culture name, i.e., “en-US”.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;{{route:&amp;lt;segment&amp;gt;}}&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Refers to a segment of the current request route (i.e., “&lt;code&gt;{{route:controller}}&lt;/code&gt;” refers to the controller route segment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;{{user:name}}&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Refers to the name of the currently logged in identity.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;{{localizer:&amp;lt;localizationkey&amp;gt;}}&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Recursively refers to another localization key.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;{{model:&amp;lt;propertypath&amp;gt;}}&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Refers to a view model property. (*)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;{{view:&amp;lt;propertypath&amp;gt;}}&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Refers to a ViewData property. (*)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(*) &lt;em&gt;available only in views using a ViewLocalizer.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  ForPath
&lt;/h2&gt;

&lt;p&gt;The ability to segregate localization keys by request path is another powerful feature of the &lt;strong&gt;MvcDashboardLocalize&lt;/strong&gt; component.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The ability to segregate localization keys by request path is another powerful feature.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;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 “&lt;em&gt;PageTitle&lt;/em&gt;” localizer section in the shared _&lt;em&gt;Layout.cshtml&lt;/em&gt; page. For instance, just above the &lt;code&gt;@RenderBody()&lt;/code&gt; call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"container"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;main&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"main"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"pb-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    @Localizer["PageTitle"]
    @RenderBody()
  &lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ll have to inject an &lt;code&gt;IViewLocalizer&lt;/code&gt; too.&lt;/p&gt;

&lt;p&gt;When running our application, we now see a literal “&lt;em&gt;PageTitle&lt;/em&gt;” on top of every page as expected: when no matching keys are found in the database, the key text itself is rendered.&lt;/p&gt;

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

&lt;p&gt;From now on, we can create new localization keys with the name “&lt;em&gt;PageTitle&lt;/em&gt;” specific to certain URLs. Create, for instance, a “&lt;em&gt;PageTitle&lt;/em&gt;” key with the value “&lt;u&gt;/Home/UpdatePerson&lt;/u&gt;” in the &lt;em&gt;ForPath&lt;/em&gt; field, of type HTML, with as value (for English):&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;h1&amp;gt;Update Person&amp;lt;/h1&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We can create another key named “&lt;em&gt;PageTitle&lt;/em&gt;” with ForPath “&lt;u&gt;/Home/Privacy&lt;/u&gt;” and (English) value:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;h1&amp;gt;{{view:Title:localize}}&amp;lt;/h1&amp;gt;&lt;br&gt;
&amp;lt;p&amp;gt;Use this page to detail your site's privacy policy.&amp;lt;/p&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Save and publish. And remove the corresponding code from the &lt;em&gt;Privacy.cshtml&lt;/em&gt; file so it now only contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@{
    ViewData["Title"] = "Privacy Policy";
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the special “&lt;em&gt;localize&lt;/em&gt;” format string in the &lt;code&gt;{{view:Title:localize}}&lt;/code&gt; expression: it tells the localizer to also localize the outcome of ViewData["Title"], returning a localized version of the “Privacy Policy” string.&lt;/p&gt;

&lt;p&gt;With this, we can manage our whole privacy policy page from the localization dashboard.&lt;/p&gt;

&lt;p&gt;Whenever a same key exists with &lt;em&gt;ForPath&lt;/em&gt; values, the system will choose the key with the longest path that matches the start of the current request URL. Matching is case-insensitive.&lt;/p&gt;

&lt;p&gt;If the route contains a “&lt;em&gt;culture&lt;/em&gt;” or “&lt;em&gt;uiculture&lt;/em&gt;” segment, as is the case when using the &lt;code&gt;RouteDataRequestCultureProvider&lt;/code&gt;, that route segment will be ignored.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cache File
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On the next run, the application will probably not hit the database at all.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is possible thanks to the cache file of which we have configured the name with the &lt;code&gt;CacheFileName&lt;/code&gt; option in &lt;strong&gt;Listing 4&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When the application starts, it first tries to read localization data from the &lt;em&gt;LocalizationCache.json&lt;/em&gt; cache file. If the file is found, the data is loaded in memory and consumed from there for the remaining lifetime of the application.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;When we hit the &lt;em&gt;Publish&lt;/em&gt; 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.&lt;/p&gt;

&lt;p&gt;This means we can deploy localization in several modes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The localized application also hosts the localization dashboard. This is the “full” mode we have been using in this sample.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If we do not provide a &lt;code&gt;CacheFileName&lt;/code&gt;, 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...&lt;/p&gt;

&lt;h2&gt;
  
  
  Translation Services
&lt;/h2&gt;

&lt;p&gt;Last but not least, the &lt;strong&gt;MvcDashboardLocalize&lt;/strong&gt; component also offers integration with the automatic translation APIs of Bing (Microsoft), Google and DeepL.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;strong&gt;MvcDashboardLocalize&lt;/strong&gt; component also offers integration with the automatic translation APIs of Bing (Microsoft), Google and DeepL.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;To the application configuration code (&lt;strong&gt;Listing 4&lt;/strong&gt;), add the following additional service registration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddTransient&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ITranslationService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                              &lt;span class="n"&gt;BingTranslationService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then right-click the project file and choose “&lt;em&gt;Manager User Secrets&lt;/em&gt;”. 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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"BingApi"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"SubscriptionKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0a123bc45678d90efa12345bc6789..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"Region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eastus"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;ReadMe-MvcDashboardLocalize.html&lt;/em&gt; file located in the &lt;em&gt;Area\MvcDashboardLocalize&lt;/em&gt; folder provides details about how to integrate with Google and DeepL APIs as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Videos
&lt;/h2&gt;

&lt;p&gt;On the Nuget homepage of the &lt;strong&gt;MvcDashboardLocalize&lt;/strong&gt; 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:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.nuget.org/packages/Arebis.MvcDashboardLocalize/"&gt;https://www.nuget.org/packages/Arebis.MvcDashboardLocalize/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;MvcDashboardLocalize&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  More
&lt;/h2&gt;

&lt;p&gt;To manage the local identity store of ASP.NET Core projects, you may also be interested in using the MvcDashboardIdentity:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.nuget.org/packages/Arebis.MvcDashboardIdentity"&gt;https://www.nuget.org/packages/Arebis.MvcDashboardIdentity&lt;/a&gt;&lt;/p&gt;

</description>
      <category>localization</category>
      <category>aspnet</category>
      <category>dotnet</category>
    </item>
  </channel>
</rss>
