📜 ⬆️ ⬇️

ASP.NET MVC Lesson C. Multilingual Website

The purpose of the lesson. Learn to create multilingual sites. DB structure. Site resources. Definition of language. Switch between languages. Work in admin panel.

Multilingual site issues

So, the customer asks to make the site multilingual, i.e. so that in Russian, and in French, and in English. It can be a simple multilingual blog, a hotel website, a real estate website and much more.
First, let's define what we will translate:



There are several options for solving this problem. Consider them.

')
Immediately consider the third option and determine how we organize it in the application:


We will create 2 localizations - Russian and English, with Russian being the default.

Routing


In the DefaultAreaRegistration, add the lang processing (/Areas/Default/DefaultAreaRegistration.cs):
 context.MapRoute( name: "lang", url: "{lang}/{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, constraints : new { lang = @"ru|en" }, namespaces: new[] { "LessonProject.Areas.Default.Controllers" } ); context.MapRoute( name : "default", url : "{controller}/{action}/{id}", defaults : new { controller = "Home", action = "Index", id = UrlParameter.Optional, lang = "ru" }, namespaces : new [] { "LessonProject.Areas.Default.Controllers" } ); 

So, if the line starts with lang, then we use the “lang” route processing. Pay attention to contstrains (restrictions), here it is set that the language can only be ru or en. If this condition is not fulfilled, then we proceed to the next processing of the route - “default”, where by default lang = ru.
Use this to initialize to DefaultController to change the flow culture (Thread.Current.CurrentCulture) (/Areas/Default/DefaultController.cs):
 public class DefaultController : BaseController { public string CurrentLangCode { get; protected set; } public Language CurrentLang { get; protected set; } protected override void Initialize(System.Web.Routing.RequestContext requestContext) { if (requestContext.HttpContext.Request.Url != null) { HostName = requestContext.HttpContext.Request.Url.Authority; } if (requestContext.RouteData.Values["lang"] != null && requestContext.RouteData.Values["lang"] as string != "null") { CurrentLangCode = requestContext.RouteData.Values["lang"] as string; CurrentLang = Repository.Languages.FirstOrDefault(p => p.Code == CurrentLangCode); var ci = new CultureInfo(CurrentLangCode); Thread.CurrentThread.CurrentUICulture = ci; Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name); } base.Initialize(requestContext); } } 


Naturally, in BaseController we remove the initialization of the flow culture through the configuration file (/Controllers/BaseController.cs):
 protected override void Initialize(System.Web.Routing.RequestContext requestContext) { if (requestContext.HttpContext.Request.Url != null) { HostName = requestContext.HttpContext.Request.Url.Authority; } base.Initialize(requestContext); } 

Run, and check how the date output changes:


The first stage is over. Go to the management of site resources.

Site Resources

Site resources are all static strings that need to be translated:


We have four such lines on the main page: roles, users, login and registration. Create resource files:



Run, check:



Let us turn to the task of validation messages using the example LoginView:


Check in the page version of localhost / en / Login :


But for the popup entry, these warnings will remain in Russian, because we use the default url to call the popup block. Accordingly, by setting the lang parameter, we can change this:


Checking:


Database

We proceed to the most important section, working with the database. For example, we have an object of type Post (blog post), which, naturally, should be in two languages:
IDUnique record number
UseridPost author
HeaderHeadlineRequires translation
UrlUrl records
ContentRecord ContentRequires translation
AddedDateDate added


So, how it will be organized:




Ok, now add this to LessonProject.Model (LessonProject.Model / IRepository.cs):
 #region Language IQueryable<Language> Languages { get; } bool CreateLanguage(Language instance); bool UpdateLanguage(Language instance); bool RemoveLanguage(int idLanguage); #endregion #region Post IQueryable<Post> Posts { get; } bool CreatePost(Post instance); bool UpdatePost(Post instance); bool RemovePost(int idPost); #endregion 


Create models using already created snippets /Proxy/Language.cs:
 namespace LessonProject.Model { public partial class Language { } } 


/Proxy/Post.cs:
 namespace LessonProject.Model { public partial class Post { } } 


/SqlRepository/Language.cs:
 public partial class SqlRepository { public IQueryable<Language> Languages { get { return Db.Languages; } } public bool CreateLanguage(Language instance) { if (instance.ID == 0) { Db.Languages.InsertOnSubmit(instance); Db.Languages.Context.SubmitChanges(); return true; } return false; } public bool UpdateLanguage(Language instance) { Language cache = Db.Languages.Where(p => p.ID == instance.ID).FirstOrDefault(); if (cache != null) { cache.Code = instance.Code; cache.Name = instance.Name; Db.Languages.Context.SubmitChanges(); return true; } return false; } public bool RemoveLanguage(int idLanguage) { Language instance = Db.Languages.Where(p => p.ID == idLanguage).FirstOrDefault(); if (instance != null) { Db.Languages.DeleteOnSubmit(instance); Db.Languages.Context.SubmitChanges(); return true; } return false; } } 


/SqlRepository/Post.cs:
 public partial class SqlRepository { public IQueryable<Post> Posts { get { return Db.Posts; } } public bool CreatePost(Post instance) { if (instance.ID == 0) { Db.Posts.InsertOnSubmit(instance); Db.Posts.Context.SubmitChanges(); return true; } return false; } public bool UpdatePost(Post instance) { Post cache = Db.Posts.Where(p => p.ID == instance.ID).FirstOrDefault(); if (cache != null) { //TODO : Update fields for Post Db.Posts.Context.SubmitChanges(); return true; } return false; } public bool RemovePost(int idPost) { Post instance = Db.Posts.Where(p => p.ID == idPost).FirstOrDefault(); if (instance != null) { Db.Posts.DeleteOnSubmit(instance); Db.Posts.Context.SubmitChanges(); return true; } return false; } } 


So, we have a set of PostLangs in the object of the class Post, where various translations are stored. Moreover, the translation into English or Russian may be, so it may not be. But at least one language should be there. What you need to do for this:


Let us turn to the implementation (/Proxy/Post.cs):
 public partial class Post { private int _currentLang; public int CurrentLang { get { return _currentLang; } set { _currentLang = value; var currentLang = PostLangs.FirstOrDefault(p => p.LanguageID == value); if (currentLang == null) { IsCorrectLang = false; var anyLang = PostLangs.FirstOrDefault(); if (anyLang != null) { SetLang(anyLang); } } else { IsCorrectLang = true; SetLang(currentLang); } } } private void SetLang(PostLang postLang) { Header = postLang.Header; Content = postLang.Content; } public bool IsCorrectLang { get; protected set; } public string Header { get; set; } public string Content { get; set; } } 


It is important to note here that if the necessary translation is not present, then the first one is taken, and IsCorrectLang = false is set. This is in order to better show the user at least some information than not to show anything.
Creating / modifying a Post object (/SqlRepository/Post.cs):
 public bool CreatePost(Post instance) { if (instance.ID == 0) { instance.AddedDate = DateTime.Now; Db.Posts.InsertOnSubmit(instance); Db.Posts.Context.SubmitChanges(); var lang = Db.Languages.FirstOrDefault(p => p.ID == instance.CurrentLang); if (lang != null) { CreateOrChangePostLang(instance, null, lang); return true; } } return false; } public bool UpdatePost(Post instance) { Post cache = Db.Posts.Where(p => p.ID == instance.ID).FirstOrDefault(); if (cache != null) { cache.Url = instance.Url; Db.Posts.Context.SubmitChanges(); var lang = Db.Languages.FirstOrDefault(p => p.ID == instance.CurrentLang); if (lang != null) { CreateOrChangePostLang(instance, cache, lang); return true; } return true; } return false; } private void CreateOrChangePostLang(Post instance, Post cache, Language lang) { PostLang postLang = null; if (cache != null) { postLang = Db.PostLangs.FirstOrDefault(p => p.PostID == cache.ID && p.LanguageID == lang.ID); } if (postLang == null) { var newPostLang = new PostLang() { PostID = instance.ID, LanguageID = lang.ID, Header = instance.Header, Content = instance.Content, }; Db.PostLangs.InsertOnSubmit(newPostLang); } else { postLang.Header = instance.Header; postLang.Content = instance.Content; } Db.PostLangs.Context.SubmitChanges(); } 


Consider how the CreateOrChangePostLang function works:

When you delete a Post entry, all PostLang are deleted by the OnDelete = cascade link (follow this)
Records for the required languages ​​should already be added to the database:

oneRuRussian
2EnEnglish


Admin panel
Now, in order to demonstrate all this, we will create an admin panel. The action plan is as follows (we will further refine and voice it later):


Add the User LanguageID to the table:


Add to IRepository.cs:
 bool ChangeLanguage(User instance, string LangCode); 

Implementing in /SqlRepository/User.cs:
  public bool ChangeLanguage(User instance, string LangCode) { var cache = Db.Users.FirstOrDefault(p => p.ID == instance.ID); var newLang = Db.Languages.FirstOrDefault(p => p.Code == LangCode); if (cache != null && newLang != null) { cache.Language = newLang; Db.Users.Context.SubmitChanges(); return true; } return false; } 

Create a model /Models/ViewModel/PostView.cs:
 public class PostView { public int ID { get; set; } public int UserID { get; set; } public bool IsCorrectLang { get; set; } public int CurrentLang { get; set; } [Required(ErrorMessage = " ")] public string Header { get; set; } [Required] public string Url { get; set; } [Required(ErrorMessage = " ")] public string Content { get; set; } } 

Validation lines do not need to be inserted into GlobalRes, since here we work only in the admin panel and this is of no use to us (since administrators are modest people). But if there are other requirements, then we know what to do.
Create /Areas/Admin/Controller/AdminController.cs:
 public abstract class AdminController : BaseController { public Language CurrentLang { get { return CurrentUser != null ? CurrentUser.Language : null; } } protected override void Initialize(RequestContext requestContext) { CultureInfo ci = new CultureInfo("ru"); Thread.CurrentThread.CurrentCulture = ci; base.Initialize(requestContext); } } 


And /Areas/Admin/Controller/HomeController.cs:
 [Authorize(Roles="admin")] public class HomeController : AdminController { public ActionResult Index() { return View(); } public ActionResult AdminMenu() { return View(); } public ActionResult LangMenu() { if (CurrentLang == null) { var lang = repository.Languages.FirstOrDefault(); repository.ChangeLanguage(currentUser, lang.Code); } var langProxy = new LangAdminView(repository, CurrentLang.Code); return View(langProxy); } [HttpPost] public ActionResult ChangeLanguage(string SelectedLang) { repository.ChangeLanguage(currentUser, SelectedLang); return Redirect("~/admin"); } } 


So, AdminController selects and sets in which language we are currently working. If this language is not set, the first one is selected, and in HomeController.cs: LangMenu is set for the user. Create LangAdminView.cs (/Models/ViewModel/LangAdminView.cs):
 public class LangAdminView { private IRepository Repository { get { return DependencyResolver.Current.GetService<IRepository>(); } } public string SelectedLang {get; set; } public List<SelectListItem> Langs { get; set; } public LangAdminView(string currentLang) { currentLang = currentLang ?? ""; Langs = new List<SelectListItem>(); foreach (var lang in Repository.Languages) { Langs.Add(new SelectListItem() { Selected = (string.Compare(currentLang, lang.Code, true) == 0), Value = lang.Code, Text = lang.Name }); } } } 

We describe all the View (+ js-files):
/Areas/Admin/Views/Shared/_Layout.cshtml:
 @{ var currentUser = ((LessonProject.Controllers.BaseController)ViewContext.Controller).CurrentUser; } <!DOCTYPE html> <html> <head> <title>@ViewBag.Title</title> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" /> @Styles.Render("~/Content/css/jqueryui") @Styles.Render("~/Content/css") @RenderSection("styles", required: false) @Scripts.Render("~/bundles/modernizr") </head> <body> <div class="navbar navbar-fixed-top"> <div class="navbar-inner"> <div class="container-fluid"> <div class="btn-group pull-right"> <a class="btn dropdown-toggle" data-toggle="dropdown" href="#"><i class="icon-user"> </i> @currentUser.Email<span class="caret"></span> </a> <ul class="dropdown-menu"> <li><a href="/"> </a></li> <li class="divider"></li> <li><a href="@Url.Action("Logout", "Login", new { area = "Default" })"></a> </li> </ul> </div> <a class="brand" href="@Url.Action("Index", "Home")">LessonProject</a> </div> </div> </div> <div class="container-fluid"> <div class="row-fluid"> <div class="span3"> <div class="well sidebar-nav"> <ul class="nav nav-list"> @Html.Action("LangMenu", "Home") @Html.Action("AdminMenu", "Home") </ul> </div> </div> <div class="span9"> @RenderBody() </div> </div> </div> @Scripts.Render("~/bundles/jquery") @Scripts.Render("~/bundles/jqueryui") @Scripts.Render("~/bundles/bootstrap") @Scripts.Render("~/bundles/common") @Scripts.Render("/Scripts/admin/common.js") @RenderSection("scripts", required: false) </body> </html> 


Index.cshtml (/Areas/Admin/Views/Home/Index.cshtml):
 @{ ViewBag.Title = "Index"; Layout = "~/Areas/Admin/Views/Shared/_Layout.cshtml"; } <h2></h2> AdminMenu.cshtml (/Areas/Admin/Views/Home/AdminMenu.cshtml): <li> @Html.ActionLink("", "Index", "Home") </li> <li> @Html.ActionLink("", "Index", "Post") </li> 


LangMenu.cshtml (/Areas/Admin/Views/Home/LangMenu.cshtml):
 @model LessonProject.Models.ViewModels.LangAdminView <li> @using (Html.BeginForm("ChangeLanguage", "Home", FormMethod.Post, new { id = "SelectLangForm" })) { @Html.DropDownList("SelectedLang", Model.Langs) } </li> 


And the SelectedLang handler (/Scripts/admin/common.js):
 function AdminCommon() { _this = this; this.init = function () { $("#SelectedLang").change(function () { $("#SelectLangForm").submit(); }); } } var adminCommon = null; $().ready(function () { adminCommon = new AdminCommon(); adminCommon.init(); }); 


We go under the admin (I have it chernikov@gmail.com) and go to the localhost / admin page:



If you could not log in and dropped to / Login, then check the UserRole connection in the database so that the current user has the role with the “admin” code.

Open the drop-down list of languages. It shows the language in which we are currently working.
Add the PostController.cs controller (/Areas/Admin/Controllers/PostController.cs):
 public class PostController : AdminController { public ActionResult Index(int page = 1) { var list = Repository.Posts.OrderByDescending(p => p.AddedDate); var data = new PageableData<Post>(list, page); data.List.ForEach(p => p.CurrentLang = CurrentLang.ID); return View(data); } [HttpGet] public ActionResult Create() { var postView = new PostView { CurrentLang = CurrentLang.ID }; return View("Edit", postView); } [HttpGet] public ActionResult Edit(int id) { var post = Repository.Posts.FirstOrDefault(p => p.ID == id); if (post != null) { post.CurrentLang = CurrentLang.ID; var postView = (PostView)ModelMapper.Map(post, typeof(Post), typeof(PostView)); return View(postView); } return RedirectToNotFoundPage; } [HttpPost] [ValidateInput(false)] public ActionResult Edit(PostView postView) { if (ModelState.IsValid) { var post = (Post)ModelMapper.Map(postView, typeof(PostView), typeof(Post)); post.CurrentLang = CurrentLang.ID; if (post.ID == 0) { post.UserID = CurrentUser.ID; Repository.CreatePost(post); } else { Repository.UpdatePost(post); } TempData["Message"] = "!"; return RedirectToAction("Index"); } return View(postView); } public ActionResult Delete(int id) { Repository.RemovePost(id); TempData["Message"] = " "; return RedirectToAction("Index"); } } 


Modify PageableData so that Foreach can be made (/Models/Info/PageableData.cs):
  public class PageableData<T> where T : class { protected static int ItemPerPageDefault = 20; public List<T> List { get; set; } … public PageableData(IQueryable<T> queryableSet, int page, int itemPerPage = 0) { … List = queryableSet.Skip((PageNo - 1) * itemPerPage).Take(itemPerPage).ToList(); } } 


Index.cshtml (/Areas/Admin/Views/Post/Index.cshtml):
 @model LessonProject.Models.Info.PageableData<LessonProject.Model.Post> @{ ViewBag.Title = ""; Layout = "~/Areas/Admin/Views/Shared/_Layout.cshtml"; } <h2>  </h2> @Html.ActionLink("", "Create", "Post", null, new { @class = "btn" }) <table class="table"> <thead> <tr> <th> # </th> <th>  </th> <th>  </th> <th> </th> </tr> </thead> @foreach (var item in Model.List) { <tr> <td> @item.ID </td> <td> @(item.IsCorrectLang ? "" : " ") </td> <td> @item.Header </td> <td> @Html.ActionLink("", "Edit", "Post", new { id = item.ID }, new { @class = "btn btn-mini" }) @Html.ActionLink("", "Delete", "Post", new { id = item.ID }, new { @class = "btn btn-mini btn-danger" }) </td> </tr> } </table> 

When initialized in ForEach, language fields are already initialized in each object. The language is the one with which we are currently working in the admin panel.
View for editing is trivial, since we do all the work in the Controller, and our PostView already uses language settings. (/Areas/Admin/Views/Post/Edit.cshtml):
 @model LessonProject.Models.ViewModels.PostView @{ ViewBag.Title = Model.ID == 0 ? " " : " "; Layout = "~/Areas/Admin/Views/Shared/_Layout.cshtml"; } <h2>@(Model.ID == 0 ? " " : " ")</h2> <p> </p> @using (Html.BeginForm("Edit", "Post", FormMethod.Post)) { @Html.Hidden("ID", Model.ID) <fieldset> <div class="control-group"> <label class="control-label"> @(!Model.IsCorrectLang && Model.ID != 0 ? " " : "") </label> </div> <div class="control-group"> <label class="control-label"> </label> <div class="controls"> @Html.TextBox("Header", Model.Header, new { @class = "input-xlarge" }) @Html.ValidationMessage("Header") </div> </div> <div class="control-group"> <label class="control-label"> Url</label> <div class="controls"> @Html.TextBox("Url", Model.Url, new { @class = "input-xlarge" }) @Html.ValidationMessage("Url") </div> </div> <div class="control-group"> <label class="control-label"> </label> <div class="controls"> @Html.TextArea("Content", Model.Content, new { @class = "input-xlarge" }) @Html.ValidationMessage("Content") </div> </div> <div class="form-actions"> <button type="submit" class="btn btn-primary"> </button> @Html.ActionLink("", "Index", null, null, new { @class = "btn" }) </div> </fieldset> } 


Pay attention to the hint about the need for translation. In this case, the fields will already be filled in, and they need to be translated and saved. This will add the translation.
Add a couple of posts and translate them:


Ok, posts are created.
Create a PostController in the Default Area and display the posts (/Areas/Default/Controller/PostController.cs):
 public class PostController : DefaultController { public ActionResult Index(int page = 1) { var list = Repository.Posts.OrderByDescending(p => p.AddedDate); var data = new PageableData<Post>(list, page); data.List.ForEach(p => p.CurrentLang = CurrentLang.ID); return View(data); } } 


Index.cshtml (/Areas/Default/Views/Post/Index.cshtml):
 @model LessonProject.Models.Info.PageableData<LessonProject.Model.Post> @{ ViewBag.Title = "Index"; Layout = "~/Areas/Default/Views/Shared/_Layout.cshtml"; } <div class="item"> @foreach (var post in Model.List) { <h3>@post.Header</h3> <p> @post.Content.NlToBr() </p> <span>@post.AddedDate.ToString("d")</span> } </div> <div class="pagination"> @Html.PageLinks(Model.PageNo, Model.CountPage, x => Url.Action("Index", new {page = x})) </div> 

And check:



Super!

Switch between languages

Create a switch ru / en in the client part. Add the LangHelper.cs class (/Helper/LangHelper.cs):
 public static class LangHelper { public static MvcHtmlString LangSwitcher(this UrlHelper url, string Name, RouteData routeData, string lang) { var liTagBuilder = new TagBuilder("li"); var aTagBuilder = new TagBuilder("a"); var routeValueDictionary = new RouteValueDictionary(routeData.Values); if (routeValueDictionary.ContainsKey("lang")) { if (routeData.Values["lang"] as string == lang) { liTagBuilder.AddCssClass("active"); } else { routeValueDictionary["lang"] = lang; } } aTagBuilder.MergeAttribute("href", url.RouteUrl(routeValueDictionary)); aTagBuilder.SetInnerText(Name); liTagBuilder.InnerHtml = aTagBuilder.ToString(); return new MvcHtmlString(liTagBuilder.ToString()); } } 

Add Partial to _Layout.cshtml (/Areas/Default/Views/Shared/_Layout.cshtml):
<
 div class="container"> <ul class="nav nav-pills pull-right"> @Html.Partial("LangMenu") </ul> 

+ LangMenu.cshtml:
 @Url.LangSwitcher("en", ViewContext.RouteData, "en") @Url.LangSwitcher("ru", ViewContext.RouteData, "ru") 


We start. Voila! Beauty.



Invalid format, translation into Russian
Sometimes, when we enter a text value in the expected numeric field, we can get the following message:
 The value 'one hundred dollars' is not valid for Price. 

But how to display this message in Russian. The following steps will help to do this:


Total

Work in a multilingual site is as follows:

Finally, I’ll say that you shouldn’t start making a multilingual website, if the customer clearly doesn’t say it, but if this opportunity is used in the foreseeable future, then you should start building the website using multilingualism, at least at the database level.

All sources are located at https://bitbucket.org/chernikov/lessons

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


All Articles