Как стать автором
Обновить

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

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

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

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



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


Сразу рассмотрим третий вариант и определим, как мы это организуем в приложении:
  • В адресе сайта теперь появляется параметр lang
    • Возможно, адрес будет иметь вид our-site.com{lang}/{controller}/{action}
    • Возможно, адрес будет иметь вид 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 папку App_LocalResources:

  • Создадим в ней файлы GlobalRes.resx и GlobalRes.en.resx:

  • Добавляем в них наши строки, в GlobalRes – русский перевод, в GlobalRes.en – английский:

    Enter Вход
    Register Регистрация
    Roles Роли
    Users Пользователи


  • Открываем для GlobalRes свойства и устанавливаем следующие значения для полей
    • Build Action: Embedded Resource
    • Custom Tool: PublicResXFileCodeGenerator


  • Теперь добавим 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>
    



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



Перейдем к заданию сообщений валидации на примере 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:


Но для 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(),
    



Проверяем:


База данных

Переходим к самому важному разделу, работе с БД. Например, у нас есть объект типа Post (блого-запись), которая, естественно, должна быть на двух языках:
ID Уникальный номер записи
UserID Автор записи
Header Заголовок Требует перевода
Url Url записи
Content Содержимое записи Требует перевода
AddedDate Дата добавления


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




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


Добавляем в 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:



Если не удалось зайти и выкинуло на /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>
}


Обратите внимание на подсказку о необходимости перевода. В данном случае, поля уже будут заполнены, и их нужно перевести и сохранить. Таким образом, будет добавлен перевод.
Добавляем пару постов и переводим их:


Ок, посты созданы.
Создадим 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>

И проверяем:



Супер!

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

Создадим переключалку 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")


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



Неверный формат, перевод на русский
Иногда, когда мы вводим в ожидаемое числовое поле текстовое значение, то можем получить следующее сообщение:
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=”Цена”]
  • Получаем результат:


Итог

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

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

Все исходники находятся по адресу https://bitbucket.org/chernikov/lessons
Теги:
Хабы:
+45
Комментарии 2
Комментарии Комментарии 2

Публикации

Истории

Работа

.NET разработчик
66 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн