ASP.NET MVC Урок C. Многоязычный сайт

в 18:53, , рубрики: .net, ASP, asp.net mvc, languages, localization, метки: , , , ,

Цель урока. Научиться создавать многоязычные сайты. Структура БД. Ресурсы сайта. Определение языка. Переключение между языками. Работа в админке.

Проблемы многоязычного сайта

Итак, заказчик просит сделать сайт многоязычным, т.е. чтобы и по-русски, и по-французки, и по-английски. Это может быть как просто многоязычный блог, так и гостиничный сайт, сайт по работе с недвижимостью и многое другое.
Для начала определим, что же мы будем переводить:

  • Написание дат, сумм в зависимости от выбранной локализации. С этим справляется класс System.Globalization
  • Встроенные ресурсы сайта — выдача ошибки («Поле не может быть пустым», «The field is required») и другие сообщения.
  • Не встроенные ресурсы, как то логотипы, изображения, js-локализация элементов управления. Для переключения между ними необходимо знать текущее значение языка на странице.
  • Пользовательские значения.


Есть несколько вариантов решения этой задачи. Рассмотрим их.

  • Самое простое, что можно сделать, — это разные сайты. Нужен русский сайт — сделали. Нужен перевод, скопировали сайт, перевели все данные и всё. Этот вариант приемлем, когда сайт небольшой, всего несколько страниц, статичный и нет админки.
  • Разные БД. Сайт в зависимости от выбранной локализации подключается к одной или другой БД. Если необходимо добавить новый язык – то переводится вся БД и ресурсы сайта. Но БД могут быть разные, и статья, написанная на одном языке, не будет доступна для перевода на другой, плюс будет необходимость дублировать изображения, которые в принципе нет необходимости переводить.
  • Управление локализацией при работе с базой данных.

Сразу рассмотрим третий вариант и определим, как мы это организуем в приложении:

  • В адресе сайта теперь появляется параметр lang
    • Возможно, адрес будет иметь вид http://our-site.com/{lang}/{controller}/{action}
    • Возможно, адрес будет иметь вид http://our-site.com/{controller}/{action}?lang=ru

  • Параметр lang – это ISO двухбуквенное определение языка (ru – русский, uk – украинский, cs – чешский)
  • Первым делом мы используем System.Globalization для правильного вывода дат
  • Организуем внутренние ресурсы для вывода ошибки относительно заданных языков
  • Организуем таблицы в БД таким образом, чтобы для каждой записи существовал перевод необходимых полей.

Мы будем создавать 2 локализации – русскую и английскую, причем русская будет по умолчанию.

Routing

В DefaultAreaRegistration добавим обработка lang (/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" }
            );

Итак, если строка у нас начинается с lang, то мы используем обработку маршрута “lang”. Обратите внимание на contstrains (ограничения), тут задается, что язык может быть только ru или en. Если это условие не исполняется, то мы переходим к следующей обработке маршрута – “default”, где по-умолчанию lang=ru.
Используем это для инициализации в DefaultController для смены культуры потока (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);
        }
    } 


Естественно, в BaseController мы убираем инициализацию культуры потока через конфигурационный файл (/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);
        }

Запускаем, и проверяем, как изменяется вывод даты:
ASP.NET MVC Урок C. Многоязычный сайт

Первый этап пройден. Переходим к управлению ресурсам сайта.

Ресурсы сайта

Ресурсы сайта – это все статические строки, которые надо перевести:

  • Наименования меню
  • Подсказки
  • Выводы ошибок

На главной странице у нас таких строк четыре: роли, пользователи, вход и регистрация. Создадим ресурсные файлы:

  • Добавим папку Asp.net папку App_LocalResources:
    ASP.NET MVC Урок C. Многоязычный сайт
  • Создадим в ней файлы GlobalRes.resx и GlobalRes.en.resx:
    ASP.NET MVC Урок C. Многоязычный сайт
  • Добавляем в них наши строки, в GlobalRes – русский перевод, в GlobalRes.en – английский:
    Enter Вход
    Register Регистрация
    Roles Роли
    Users Пользователи
  • Открываем для GlobalRes свойства и устанавливаем следующие значения для полей
    • Build Action: Embedded Resource
    • Custom Tool: PublicResXFileCodeGenerator

    ASP.NET MVC Урок C. Многоязычный сайт

  • Теперь добавим namespace LessonProject.App_LocalResources в Web.cofig в system.web.webPages.razor (Web.config):
    <system.web.webPages.razor>
        <pages pageBaseType="System.Web.Mvc.WebViewPage">
          <namespaces>
            <add namespace="LessonProject.Helper" />
            <add namespace="LessonProject.Tools" />
            <add namespace="LessonProject.App_LocalResources" />
          </namespaces>
        </pages>
      </system.web.webPages.razor>
    

  • Используем в UserLogin.cshtml (/Areas/Default/Views/Home/UserLogin.cshtml) и Index.cshtml ((/Areas/Default/Views/Home/Index.cshtml):
    @model LessonProject.Model.User
    
    @if (Model != null)
    {
        <li>@Model.Email</li>
        <li>@Html.ActionLink("Выход", "Logout", "Login")</li>
    }
    else
    {
        <li><span class="btn btn-link" id="LoginPopup">@GlobalRes.Enter</span></li>
        <li>@Html.ActionLink(GlobalRes.Register, "Register", "User")</li>
    }
    
    …
    
    @{
        ViewBag.Title = "LessonProject";
        Layout = "~/Areas/Default/Views/Shared/_Layout.cshtml";
    }
    <h2>LessonProject </h2>
    <p>
        @DateTime.Now.ToString("D")
        <div class="menu">
        <a href="@Url.Action("Index", "Role", new { id = "1" })">@GlobalRes.Roles</a>
        @Html.ActionLink(GlobalRes.Users,  "Index", "User")
        </div>
        
    </p>
    

Запускаем, проверяем:

ASP.NET MVC Урок C. Многоязычный сайт

Перейдем к заданию сообщений валидации на примере LoginView:

  • Выделяем ErrorMessage для полей в ресурсные файлы (/App_LocalResources/GlobalRes.resx):
    EnterEmail Введите email
    EnterPassword Введите пароль

  • Задаем правила валидации в LoginView.cs (/Models/ViewModel/LoginView.cs):
    public class LoginView
        {
            [Required(ErrorMessageResourceType=typeof(GlobalRes), ErrorMessageResourceName="EnterEmail")]
            public string Email { get; set; }
    
            [Required(ErrorMessageResourceType = typeof(GlobalRes), ErrorMessageResourceName = "EnterPassword")]
            public string Password { get; set; }
    
            public bool IsPersistent { get; set; }
        }
    

Проверяем в страничной версии localhost/en/Login:
ASP.NET MVC Урок C. Многоязычный сайт

Но для popup входа эти предупреждения так и останутся на русском языке, потому, что мы для вызова popup-блока используем url по умолчанию. Соответственно, задав параметр lang, мы сможем изменить это:

  • Добавим в ресурсы (/App_LocalResources/GlobalRes(.en).resx)CurrentLang = ru и CurrentLang = en
  • Выведем это в hidden-поле в _Layout.cshtml (/Areas/Default/Views/Shared/_Layout.cshtml):
    <body>
        @Html.Hidden("CurrentLang", GlobalRes.CurrentLang)
        <div class="navbar navbar-fixed-top">
    •	Добавим это в ajax-вызовы (/Scripts/common.js):
    _this = this;
        this.loginAjax = "/Login/Ajax";
    
        this.init = function ()
        {
            _this.loginAjax = "/" + $("#CurrentLang").val() + _this.loginAjax;
            $("#LoginPopup").click(function () {
                _this.showPopup(_this.loginAjax, initLoginPopup);
            });
        }
    
      function initLoginPopup(modal) {
            $("#LoginButton").click(function () {
                $.ajax({
                    type: "POST",
                    url: _this.loginAjax,
                    data : $("#LoginForm").serialize(),
    

Проверяем:
ASP.NET MVC Урок C. Многоязычный сайт

База данных

Переходим к самому важному разделу, работе с БД. Например, у нас есть объект типа Post (блого-запись), которая, естественно, должна быть на двух языках:

ID Уникальный номер записи
UserID Автор записи
Header Заголовок Требует перевода
Url Url записи
Content Содержимое записи Требует перевода
AddedDate Дата добавления

Итак, как это всё будет организовано:

  • Создадим таблицу Language, где и будут определены языки
  • Создадим таблицу Post, где будут все поля, не требующие перевода
  • Создадим таблицу PostLang, связанную с Post и Language, где будет перевод необходимых полей для таблицы Post и связанный с таблицей Language

ASP.NET MVC Урок C. Многоязычный сайт

Ок, теперь добавим это в 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

Создаем модели с помощью уже созданных сниппетов /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;
        }
        
    }

Итак, у нас есть набор PostLangs в объекте класса Post, где и хранятся различные переводы. Причем, перевод на английский или русский язык может быть, так может и не быть. Но, по крайней мере, хотя бы один язык должен быть. Что необходимо сделать для этого:

  • Добавим языковые поля в Post (Header, Content)
  • Создадим свойство CurrentLang, при изменении которого будут инициализироваться языковые поля.
  • При создании записи в БД Post автоматически создается запись в БД PostLang.
  • При изменении записи в БД, проверяется, какой именно язык изменяется, и если такого языка (перевода) еще нет, то создается новая запись PostLang в БД:

Перейдем к реализации (/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; }
    }

Тут важно заметить, что если необходимого перевода нет, то берется первый попавшийся, и устанавливается IsCorrectLang = false. Это для того, что лучше показать пользователю хоть какую-то информацию, чем не показать ничего.
Создание/изменение объекта Post (/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();
        }

Рассмотрим, как работает CreateOrChangePostLang функция:

  • При вызове данной функции, мы ищем в Language необходимый язык. Если язык не найден, то вызова не происходит, и мы не создаем PostLang объект (т.е. перевод)
  • Если мы находим необходимый язык, то вызываем CreateOrChangePostLang:
    • Если cache нулевое (объект PostLang еще точно не создан) или
    • Если cache не нулевое, но перевод не найден, то
      • Создаем перевод (запись в БД PostLang)
    • Иначе изменяем перевод, который найден.

При удалении записи Post все PostLang удаляются по связи OnDelete = cascade (проследите за этим)
В БД должны быть уже добавлены записи для необходимых языков:

1 Ru Русский
2 En Английский

Админка
Сейчас, чтобы это всё продемонстрировать, мы создадим админку. План действий таков (мы его еще потом доработаем и озвучим):

  • Создать модели
  • Связать язык ввода и пользователя (для того, чтобы было понятно, в каком языке работает администратор или редактор)
  • Создать переключение между языками
  • Создать домашнюю страницу админки
  • Создать контроллер по работе с постами
  • Вывести посты в default/postController части

Добавим в таблицу User LanguageID:
ASP.NET MVC Урок C. Многоязычный сайт

Добавляем в IRepository.cs:

bool ChangeLanguage(User instance, string LangCode);

Реализуем в /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;
        }

Создаем модель /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; }
    }

Строки валидации не надо вставлять в GlobalRes, так как тут мы работаем только в админке и это нам ни к чему (так как администраторы люди скромные). Но если есть другие требования, то мы знаем что делать.
Создаем /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);
        }

    }

И /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");
        }
    }

Итак, AdminController выбирает и устанавливает, в каком языке мы сейчас работаем. Если данный язык не установлен, то выбирается первый попавшийся, и в HomeController.cs:LangMenu устанавливается для пользователя. Создадим 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
                });
            }
        }
    }

Опишем все View (+js-файлы):
/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>

И обработчик SelectedLang (/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();
});


Заходим под админом (у меня это chernikov@gmail.com) и переходим на страницу localhost/admin:

ASP.NET MVC Урок C. Многоязычный сайт

Если не удалось зайти и выкинуло на /Login, то проверьте связь UserRole в БД, чтобы текущий пользователь имел роль с кодом “admin”.

Открываем выпадающий список языков. Он и показывает, в каком языке мы в данный момент работаем.
Добавляем контроллер PostController.cs (/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");
        }
    }

Изменим PageableData, чтобы можно было сделать Foreach (/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>

При инициализации в ForEach, в каждом объекте уже инициализируются языковые поля. Язык – тот, с которым в данный момент работаем в админке.
View для редактирования тривиальна, так как мы всю работу делаем в Controller, а наш PostView уже использует языковые настройки. (/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>
}

Обратите внимание на подсказку о необходимости перевода. В данном случае, поля уже будут заполнены, и их нужно перевести и сохранить. Таким образом, будет добавлен перевод.
Добавляем пару постов и переводим их:
ASP.NET MVC Урок C. Многоязычный сайт

Ок, посты созданы.
Создадим PostController в Default Area и выведем посты (/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>

И проверяем:

ASP.NET MVC Урок C. Многоязычный сайт

Супер!

Переключение между языками

Создадим переключалку ru/en в клиентской части. Добавляем класс LangHelper.cs (/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());
        }
    } 

Добавляем Partial в _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")

Запускаем. Вуаля! Красота.

ASP.NET MVC Урок C. Многоязычный сайт

Неверный формат, перевод на русский
Иногда, когда мы вводим в ожидаемое числовое поле текстовое значение, то можем получить следующее сообщение:

The value 'one hundred dollars' is not valid for Price.

Но как это сообщение вывести на русском. Следующие действия помогут это сделать:

  • Добавить папку App_GlobalResources
  • Добавить ресурс Messages.resx
  • Добавить строку “PropertyValueInvalid: Значение {0} недопустимо для поля {1}
  • В App_Start добавить строку в Application_Start() (/Global.asax.cs)
  • DefaultModelBinder.ResourceClassKey = «Messages»;
  • Для указания имени поля можно использовать атрибут Display[Name=”Цена”]
  • Получаем результат:
    ASP.NET MVC Урок C. Многоязычный сайт
Итог

Работа в многоязычном сайте заключается в следущем:

  • Отделить переводные строки в ресурсы
  • Определить языковые поля в таблицах БД и связать их через таблицу Language
  • Использовать ajax-запросы с учетом языковой поддержки

Напоследок скажу, что не стоит начинать делать многоязычный сайт, если заказчик явно это не говорит, но, если в обозримом будущем будет использоваться такая возможность, то начинать строить сайт необходимо с использованием многоязычности, хотя бы на уровне БД.

Все исходники находятся по адресу https://bitbucket.org/chernikov/lessons

Автор: chernikov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js