воскресенье, 11 января 2015 г.

Делаем локализацию сайта на ASP.NET MVC

Представьте, что вы решили делать сайт с использованием ASP.NET MVC. И, помимо всего прочего, вам на этом сайте вам нужна возможность отображения его на нескольких языках. Задача, хоть и кажущаяся с первого взгляда не самой тривиальной, на самом деле, достаточно проста. И я сейчас расскажу что для этого нужно сделать.


Во-первых, необходимо как-то передавать идентификатор языка. Можно, конечно, придумать какое-нибудь извращение, типа хранения текущего языка в сессии, но лучше всего просто передавать его в URL. Как минимум, в это случае, локализованными ссылками на ваш сайт можно будет без проблем делиться, хоть в соцсеятх, хоть пересылать их по e-mail. 
Для этого в вашем проекте открываем файл RouteConfig.cs, находящийся в папке App_Start и добавляем туда путь для локализованного  контента.
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "Localization",
                url: "{lang}/{controller}/{action}/{id}",
                defaults: new { lang = "ru", controller = "Home", action = "Index", id = UrlParameter.Optional },
                //ограничение необходимо, чтобы отличить параметр языка от параметра контроллера. 
                //В данном случае, в качестве параметра lang подходят все двухсимвольные комбинации из букв латинского алфавита, например "en" или "fr"
                constraints: new { lang = @"[a-z]{2}" }
            );

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }           
            );
        }

Теперь, если в URL содержится идентификатор языка, то он будет приходить нам в коллекции RequestContext.RouteData, и получить его проще всего в методе в методе Initialize контроллера. Давайте для простоты сделаем локализованную страничку авторизации, которая будет отображаться сразу же при входе на сайт, то есть по пути /.

Для этого открываем файл с кодом Home-контроллера в Controllers/HomeController.cs и дописываем необходимый нам для получения параметра языка:

        
        //эта переменная нам нужна для хранения текущего языка и удобного доступа к этому значению
        //из кода
        public string CurrentLangCode { get; protected set; }

        protected override void Initialize(System.Web.Routing.RequestContext requestContext) 
        {
            //проверяем если ли в коллекции параметр lang и если есть, получаем его.
            if (requestContext.RouteData.Values["lang"] != null && requestContext.RouteData.Values["lang"] as string != "null")
                CurrentLangCode = requestContext.RouteData.Values["lang"] as string;       
            //а если его нет, то используем язык по умолчанию
            else
                CurrentLangCode = "ru";

            //сохраняем значение языка во ViewBag, для того, чтобы легко получать к нему доступ из вьюшки
            ViewBag.CurrentLanguage = CurrentLangCode;
            base.Initialize(requestContext);
        }

Следующий этап - придумать, где мы будет хранить локализованные строки. Во многих статьях в интернете я встречал рекомендации создавать для строк файлы ресурсов, что, на мой взгляд, не очень удобно. Например, не просто будет быстренько сделать простой интерфейс редактирования этих строк, так как самым простым способ изменения какой-то строки будет перекомпиляция проекта и выкладывание новых файлов.
Поэтому, я предлагаю создать простенькую базу табличку, которую, если вы не хотите использовать полноценную БД, можно разместить даже в .MDF файле.  На этой картинке наша табличка уже содержит необходимые нам строки.

Последнее, что нас остается в создании механизма локализации - это получение этих данных и вывод их на страницу. Допустим, что получать данные из БД мы будем с помощью Entity Framework (далее в коде объектом этой таблицы будет data.Translations), на чем я не буду отдельно останавливаться. А вот о том как выводить наши строки на страницу сайта я расскажу подробнее.
Для этой цели я предлагаю использовать методы Html хэлперов. Подробно о том что это такое и для чего нужно, я напишу в одном из следующих постов, а пока смотрим на код:

        //в этом методе мы просто возвращаем любую строку по ее названию для заданного языка
        //например, если мы передадим в параметре name "HELLO_LABEL", а в lang "fr", то
        //предполагается что функция вернет что-то типа "Bon jour", в случае если такая строка есть в БД
        //этом удобно использовать для контента
        public static string GetContentString(this HtmlHelper helper, string name, string lang)
        {
            using (TranslationsEntities data = new TranslationsEntities())
            {
                var translation = data.Translations.FirstOrDefault(x => x.name == name && x.lang == lang);
                if (translation != null) return translation.text;
            }
            //А если такой строки нет, то надо об этом сообщить.
            return "[No " + name + " for " + lang + "]";
        }

        //Этот метод более сложный. Он возвращает то же самое, но для описания члена класса модели,
        //заданного аттрибутом Display. Например, [Display(Name = "HELLO_LABEL")]
        //этот метод удобно использовать для вывода форм, таблиц или других данных из БД
        //для которых у вас есть модели
        public static string GetContentStringFromModel<TModel, TProperty>(this HtmlHelper<TModel>html, Expression<Func<TModel, TProperty>> ex, string lang)
        {
            var metadata = ModelMetadata.FromLambdaExpression(ex, html.ViewData);
            string displayName = metadata.DisplayName;

            using (TranslationsEntities data = new TranslationsEntities())
            {
                var translation = data.Translations.FirstOrDefault(x => x.name == displayName && x.lang == lang);
                if (translation != null) return translation.text;
            }
            return "[No " + displayName + " for " + lang + "]";
        }

В общем-то, первой функцией уже можно пользоваться во вьюшках, а для того чтобы показать вам как использовать вторую, предлагаю создать простенькую модель, которая будет использоваться нашей формой авторизации. Для этого в папке Models проекта создаем новый файл вот такого содержания:
namespace SampleSite.Models
{
    public class LoginModel
    {
        [Required]
        [Display(Name = "USER_NAME_LABEL")]
        public string UserName { get; set; }

        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "PASS_LABEL")]
        public string Password { get; set; }
    }
}


Ну и последний штрих, собственно, вьюшка авторизации. В данному случае преимущественно используется второй вариант нашего метода вывода локализованных строк. Первый же метод используется только в самом начале для вывода заголовка.
//указываем какая модель используется
@model  SampleSite.Models.LoginModel
//с помощью первого метода вывода контента выводим заголовок 
@Html.GetContentString("LOGIN_FORM_HEADER",(string)ViewBag.CurrentLanguage);

//рисуем формочку
@using (Html.BeginForm(new { ReturnUrl = ViewBag.ReturnUrl }))

                    {

                        @Html.AntiForgeryToken()

                        @Html.ValidationSummary(true)

 

                        <fieldset>
 @Html.Label(Html.GetContentStringFromModel<SampleSite.Models.LoginModel, string>(m => m.UserName, (string)ViewBag.CurrentLanguage))<br />

                                @Html.TextBoxFor(m => m.UserName, new Dictionary<string, object> { { "autocomplete", "off" }, { "class", "loginInput keyboardInput" } })

                                @Html.ValidationMessageFor(m => m.UserName)

                            </p>

                            <p>

                                @Html.Label(Html.GetContentStringFromModel<SampleSite.Models.LoginModel, string>(m => m.Password, (string)ViewBag.CurrentLanguage))<br />

                                @Html.PasswordFor(m => m.Password, new Dictionary<string, object> { { "autocomplete", "off" }, { "class", "passwordInput keyboardInput" } })

                                @Html.ValidationMessageFor(m => m.Password)

                            </p>

                            <input type="submit" value="@Html.GetContentString("LOGIN_BUTTON_NAME", (string)ViewBag.CurrentLanguage)" />
                        </fieldset>
                    }


Теперь все должно работать, давайте проверим. Заходим на сайт без явного указания языка - получаем язык по умолчанию, в нашем случае - русский

А теперь, добавим после последнего слэша "en", чтобы получить англоязычную версию сайта:

Вот и все, ничего сложного :)


5 комментариев:

  1. Здравствуйте! Скажите пожалуйста, у меня WebCore не определяется?!

    ОтветитьУдалить
    Ответы
    1. Упс Забыл эту строчку убрать... Спасибо что подсказали. Это кусок из моего живого проекта, и это уже дальнейшая загрузка ядра сайта. К локализации они не относится, можно просто убрать.

      Удалить
    2. Не за что) А вы не знаете как реализовать локализацию через url.requestcontext?

      Удалить
    3. Не разу не пробовал. А смысл? Чтобы проверять везде в коде Url.RrequestContext.RouteData.Values["lang"] вместо локальной переменной lang ?

      Удалить
  2. Если вы ищете практичный локализационный инструмент для перевода вашего сайта или приложения на другой язык, присмотритесь к https://poeditor.com/

    ОтветитьУдалить