📜 ⬆️ ⬇️

Localization of an ASP.NET MVC application using a database


This article will be narrowly focused and covers localization through the database, so you can see how to do localization using resource files (resx) in detail, for example, here: MVC 2: Complete localization guide . For localization using views, I also have links there.

To begin, I will briefly talk about options for localizing the site, show an example of creating your ResourceProviderFactory, and then create a small application for demonstration.


Localization options


In many discussions and articles, only two localization options are mentioned, for example, the ASP.NET MVC 3 Internationalization article, to which many references can be found, identifies the following:
- Resource files (resx)
- Use different "Views"
The first method is usually used for statics: field names, validations, and others. The essential disadvantage of using the second is that the need to do a lot of manual work on copying the same code, in case you need to even slightly change the layout, it’s also difficult to imagine the structure of the site with many languages, the number of files will be huge, sometimes the translation is divided into directories Of course it becomes clearer, but the scalability leaves much to be desired. In my case, I needed to translate the dynamic content that is added through the admin panel, I did not consider the resx file editing option from the admin panel, but you can find the implementation yourself, as the saying goes, an amateur, therefore, select the third option:
- Localization using the database
Of course, you can combine all these three options.
')

Implementation example


I’ll say right away that I’m creating an empty MVC 3 project, because I’ll use the Entity Framework Code First, I’ll not rewrite the Membership Provider in this article, you can see how to do this, for example, here: Custom Membership Providers . Just remember that the admin panel will be publicly available, of course, it was possible to implement authorization, through the config file, as Stephen Sanderson demonstrates in his books, but an article about something else.

Make a parody of the warehouse, we will have a table of products with 4 fields:
-Identifier
-Product name (we will translate it using the database)
-Price (we need this field to demonstrate problems with validation during localization)
-Date of delivery (as before)

The next step is to create a Product class and set attributes using Data Annotations (if you don’t like this option, you can use the Fluent API, which you will need to access in a large project anyway) and create a DbContext:

public class Product { public int ProductId { get; set; } [Required] [StringLength(128)] public string Name { get; set; } [Required] public decimal Price { get; set; } [Required] public DateTime ImportDate { get; set; } } public class ProductDbContext : DbContext { public DbSet<Product> Products { get; set; } } 


Now I will generate the controller and all actions (Actions) automatically, it turned out a bit scary, so I’ll have to add styles, though I would recommend using a slightly different approach for generation. Combining the creation and editing in one view, for example, like this: Unlucky Notes on ASP.NET MVC. Part 1 (and only) that will remove one presentation, try to have as little as possible of copy-paste.

As a result, we have such a table:



Go to the ResourceProviderFactory, a little googling, I found a rather old article in MSDN Extending the ASP.NET 2.0 Resource-Provider Model , as well as a description of the ResourceProviderFactory Class with an example implementation, but for the 4th framework. On the same codeproject there is a ready-made example, which can also be taken as a basis: ASP.NET 2.0 Custom SQL Server ResourceProvider .

Now let's create a class for storing translations:

 public class GlobalizationResource { public int GlobalizationResourceId { get; set; } [Required] [StringLength(128)] public string ResourceObject { get; set; } [Required] [StringLength(128)] public string ResourceName { get; set; } [Required] [StringLength(5)] public string Culture { get; set; } [Required] [StringLength(4000)] public string ResourceValue { get; set; } } 


And do not forget to add it to the database context. I got an average implementation between the codeproject and an example from MSDN, the code can be downloaded at the end of the article, since there are about 150 lines. And add the provider to the config:

  <system.web> <globalization enableClientBasedCulture="true" resourceProviderFactoryType="DbLocalizationExample.Models.CustomResourceProviderFactory" uiCulture="auto" culture="auto" /> ... </system.web> 


Everything would be fine, but to check the localization we need the ability to select a language, I’ll use cookies to store the localization (I wouldn’t recommend using the session, as the user is unlikely to be happy to go to the site after 20 minutes (I remember the standard lifetime) that again you need to choose a language). We take the idea from afana.me as a basis and get the following class:

 public static class CultureHelper { private static readonly List<string> Cultures = new List<string> { "ru-RU", // first culture is the DEFAULT "en-US", }; /// <summary> /// Returns a valid culture name based on "name" parameter. If "name" is not valid, it returns the default culture "en-US" /// </summary> /// <param name="name">Culture's name (eg en-US)</param> public static string GetValidCulture(string name) { if (string.IsNullOrEmpty(name)) return GetDefaultCulture(); // return Default culture if (Cultures.Contains(name)) return name; // Find a close match. For example, if you have "en-US" defined and the user requests "en-GB", // the function will return closes match that is "en-US" because at least the language is the same (ie English) foreach (var c in Cultures) if (c.StartsWith(name.Substring(0, 2))) return c; return GetDefaultCulture(); // return Default culture as no match found } public static string GetDefaultCulture() { return Cultures.ElementAt(0); // return Default culture } public static string GetCultureFromCookies(HttpRequest request) { string cultureName = null; // Attempt to read the culture cookie from Request HttpCookie cultureCookie = request.Cookies["_culture"]; if (cultureCookie != null) { cultureName = cultureCookie.Value; } else if (request.UserLanguages != null) { cultureName = request.UserLanguages[0]; // obtain it from HTTP header AcceptLanguages } // Validate culture name return GetValidCulture(cultureName); // This is safe } private static string AcceptLanguage() { return HttpUtility.HtmlAttributeEncode(System.Threading.Thread.CurrentThread.CurrentUICulture.ToString()); } public static IHtmlString MetaAcceptLanguage<T>(this HtmlHelper<T> html) { return new HtmlString(String.Format(@"<meta name=""accept-language"" content=""{0}"" />", AcceptLanguage())); } public static IHtmlString GlobalizationLink<T>(this HtmlHelper<T> html) { return new HtmlString(String.Format(@"<script src=""../../Scripts/globalization/cultures/globalize.culture.{0}.js"" type=""text/javascript""></script>", AcceptLanguage())); } } 


Now we need to add actions for installing and reading cookies:

 public ActionResult SetCulture(string culture) { // Validate input culture = CultureHelper.GetValidCulture(culture); // Save culture in a cookie HttpCookie cookie = Request.Cookies["_culture"]; if (cookie != null) { cookie.Value = culture; // update cookie value } else { cookie = new HttpCookie("_culture"); cookie.HttpOnly = false; // Not accessible by JS. cookie.Value = culture; cookie.Expires = DateTime.Now.AddYears(1); } Response.Cookies.Add(cookie); return RedirectToAction("Index"); } 


And also the logic in Global.asax for setting up a culture and checking GetVaryByCustomString in order to use caching.

 protected void Application_AcquireRequestState(object sender, EventArgs e) { string cultureName = CultureHelper.GetCultureFromCookies(Request); // Modify current thread's culture Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(cultureName); Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(cultureName); } public override string GetVaryByCustomString(HttpContext context, string arg) { // It seems this executes multiple times and early, so we need to extract language again from cookie. if (arg == "culture") // culture name (eg "en-US") is what should vary caching { string cultureName = CultureHelper.GetCultureFromCookies(Request); return cultureName.ToLower();// use culture name as cache key, "es", "en-us", "es-cl", etc. } return base.GetVaryByCustomString(context, arg); } 


A few words about the logic of translation: my default language will be stored in the database, i.e. just a Product object, but when I want to add a translation to it, I will write in the view:

 @(Culture == "ru-RU" ? item.Name : HttpContext.GetLocalResourceObject("/Home/Index", "Product_" + item.ProductId)) 

Which will automatically add the default value to the database. The first parameter is similar to the path only for clarity, there can be any sequence of characters (limited truth 128 in the database for our ad), the second is a unique identifier.

In Layout, add the ability to select a language:
 <div class="language"> <span>@Html.ActionLink("rus", "SetCulture", "Home", new { culture = "ru-RU" }, null)</span> <span>@Html.ActionLink("eng", "SetCulture", "Home", new { culture = "en-US" }, null)</span> </div> 


You can run, but it was not there, I changed the context (added class for resources) and now EF refuses to display data from the table. Go to View -> Other Windows -> Package Manager Console and enter (each line separately):

 Update-Package EntityFramework Enable-Migrations 

Now you can create a migration and update the database:

 Add-Migration AddGlobalizationResources Update-Database 

But here we are upset, the studio says that we have created a database with an older EF, where there is no migration history, therefore, in order not to delete our database with our hands, we will add such a line to the Index (if you are opposed to migration, then it’s more correct to add it to Application_Start, but remember that this deletes all data):

 Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductDbContext>()); 

After compilation and access to it, delete it, because later we will be able to enjoy all the migration buns: EF 4.3 Automatic Migrations Walkthrough .

The result of our work will look like this:



The English version is created in the database automatically, the Russian version is stored in the Product. I will leave the logic of database editing to you, there is nothing complicated there.

Client Validation


When switching language, we have a problem with decimal and datetime. For the Russian language, we have "4.00", and for English it is "4.00". Dates also have problems: "12/21/2012" and "12/21/2012". To solve these problems, we will use globalize and enable jquery ui datapicker to set the format automatically and simplify the input of dates.

Add to Layout the following (“core” of globalization, globalization for a specific language, meta tag for the client part and common scripts for validating numbers and changing jquery ui datapicker):
 <script src="@Url.Content("~/Scripts/globalization/globalize.js")" type="text/javascript"></script> @Html.GlobalizationLink() @Html.MetaAcceptLanguage() <script src="@Url.Content("~/Scripts/common.js")" type="text/javascript"></script> 


This is only a small part of customer validation, an example of localization can be found here: ASP.NET MVC 3 Internationalization - Part 2 (NerdDinner)

Total


I told you how to create your own resource provider, created a small application that demonstrates its work and shared links where you can read more information on this topic. As Jon Skeet writes in his book “C # in Depth” that the code given here is just examples, I do not guarantee that the code you take from here will work for you. I use full translation caching, most likely you will need to download the translation gradually, if there is a large amount of information, set the lifetime, etc. Remember that when editing a translation, it is necessary to clear the cache so that the data is displayed immediately (this is when you will implement the translation editing logic).



The project can be downloaded here (Visual studio 2010): link (2.89 MB) (the example only demonstrates the localization of the dynamics, it is easier to add static translation, therefore the code contains only the one described in the article)

Sources


Links


Extending the ASP.NET 2.0 Resource-Provider Model
ResourceProviderFactory Class
ASP.NET 2.0 Custom SQL Server ResourceProvider
ASP.NET MVC 3 Internationalization
ASP.NET MVC 3 Internationalization - Part 2 (NerdDinner)

Books


Freeman A. Sanderson S. - Pro ASP.NET MVC 3 Framework Third Edition - 2011
Julia Lerman and Miller - Programming Entity Framework: Code First - 2012

Note: If you will do localization for this ASP.NET MVC 3 Internationalization guide, you should remember that ExecuteCore () does not work in MVC 4 ExecuteCore.

Source: https://habr.com/ru/post/143204/


All Articles