Цель урока. Научиться создавать многоязычные сайты. Структура БД. Ресурсы сайта. Определение языка. Переключение между языками. Работа в админке.
Проблемы многоязычного сайта
Итак, заказчик просит сделать сайт многоязычным, т.е. чтобы и по-русски, и по-французки, и по-английски. Это может быть как просто многоязычный блог, так и гостиничный сайт, сайт по работе с недвижимостью и многое другое.
Для начала определим, что же мы будем переводить:
- Написание дат, сумм в зависимости от выбранной локализации. С этим справляется класс 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 папку 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
Автор: chernikov