Интернационализация (Internationalization) в ASP.NET MVC 3

Если ваш веб-сайт ориентирован на пользователей из различных частей света, эти пользователи могут хотеть просматривать контент на вашем веб-сайте на их собственном языке. Создание мультиязыкового сайта – нелегкая задача, но это позволяет привлечь большее количество аудитории. К счастью, .NET Framework уже имеет компоненты, которые поддерживают использование различных языков и культур.

Мы разработаем ASP.NET MVC 3 приложение, которое будет содержать следующую функциональность:

  1. Показ контента на различных языках.
  2. Автоопределение языка пользовательского браузера.
  3. Возможность переопределения языка пользовательского браузера.

Глобализация и локализация в ASP.NET

Интернационализация оперирует терминами Глобализация и Локализация. Глобализация – процесс проектирования приложений, поддерживающих различные культуры, локализация – процесс настройки приложения для определенной культуры.

Формат имени культуры - "<languagecode2>-<country/regioncode2>", где <languagecode2> - код языка, <country/regioncode2>- код субкультуры. Например, es-CL – Чили, испанский, и en-US - США, английский.

ASP.NET отслеживает два значения, связанных с культурой - Culture и UICulture. Значение Culture определяует результаты таких функций, как дата и форматирование валюты. UICulture определяет, какие ресурсы должны быть загружены ResourceManager. ResourceManager просто находит специфичные для культуры ресурсы, определяемые CurrentUICulture. Каждый тред в .NET имеет собственные объекты CurrentCulture и CurrentUICulture. Например, если текущая культура треда (CurrentCulture) определена как “en-US” (США, английский), DateTime.Now.ToLongDateString() выведет "Saturday, January 08, 2011", но если CurrentCulture определена как "es-CL" (испанский, Чили), метод выведет "sábado, 08 de enero de 2011".

Поддержка различных языков в ASP.NET MVC 3

Есть несколько методов реализации поддержки различных языков в ASP.NET MVC 3:

  1. С использованием строк ресурсов на всех наших представлениях
  2. С использованием различного набора представлений для каждого языка и местоположения.
  3. Смешиванием вышеупомянутых методов.
Какой метод лучше?

Зависит от вас, это вопрос удобства. Некоторые люди предпочитают использовать одно представление для всех языков, так как им легче управлять, другие считают, что замена контента представления кодом типа “@Resources.Something” может привести к мешанине и нечитабельности. Представления должны быть настолько простыми, насколько возможно. Если ваше представление выглядит хорошо с большим количеством кода, пусть будет так. Однако иногда выбора нет, например, в языках, где разметка должна быть другой, например, в языках, которые читаются справа налево. Возможно, смешивание двух методов – лучший выход. В нашем примере мы будем использовать второй метод просто затем, чтобы показать, как выполнить задачу без использования обычных строк ресурсов.

Конвенции именования представлений

Для создания различных представлений для каждой культуры мы будем добавлять название культуры к имени представления. Например, Index.cshtml (стандартное представление), Index.es-CL.cshtml (испанский, Чили), Index.ar-JO.cshtml (арабский, Иордания). Имя представления, не имеющее суффикса культуры, распознается как стандартная культура.

Глобализация на веб-сайте

Мы создадим ASP.NET MVC 3 приложение и глобализируем его шаг за шагом.

Нажмите File->New Project в Visual Studio и создайте новый проект ASP.NET MVC 3 с шаблоном Internet Application.

clip_image001

Создание модели

Нам нужна модель. Добавьте класс User в папку Models.

clip_image002

Интернационализация сообщений валидации

Модель, представленная выше, не содержит логики валидации, что не является хорошей практикой для современного приложения. Мы можем использовать аннотации данных для добавления логики валидации к нашей модели, однако, чтобы глобализировать сообщения валидации, нам нужно ещё несколько параметров. Так, "ErrorMessageResourceType" определяет тип ресурса для поиска при сообщении об ошибке. "ErrorMessageResourceName" определяет имя ресурса для сообщения об ошибке.

Измените класс Person и добавьте в него следующие атрибуты:

 namespace MvcInternationalization.Models
{
    public class Person
    {
        [Required(ErrorMessageResourceType=typeof(MyResources.Resources), 
                  ErrorMessageResourceName="FirstNameRequired")]
        [StringLength(50, ErrorMessageResourceType = typeof(MyResources.Resources), 
                          ErrorMessageResourceName = "FirstNameLong")]       
        public string FirstName { get; set; }

        [Required(ErrorMessageResourceType = typeof(MyResources.Resources), 
            ErrorMessageResourceName = "LastNameRequired")]
        [StringLength(50, ErrorMessageResourceType = typeof(MyResources.Resources), 
            ErrorMessageResourceName = "LastNameLong")]
        public string LastName { get; set; }

        [Required(ErrorMessageResourceType = typeof(MyResources.Resources), 
            ErrorMessageResourceName = "AgeRequired")]        
        [Range(0, 130, ErrorMessageResourceType = typeof(MyResources.Resources), 
            ErrorMessageResourceName = "AgeRange")]                
        public int Age { get; set; }

        [Required(ErrorMessageResourceType = typeof(MyResources.Resources), 
            ErrorMessageResourceName = "EmailRequired")]
        [RegularExpression(".+@.+\\..+", ErrorMessageResourceType = typeof(MyResources.Resources), 
            ErrorMessageResourceName = "EmailInvalid")]        
        public string Email { get; set; }

        public string Biography { get; set; }
    }
}
Локализация сообщений валидации

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

Мы сохраним файлы ресурсов в отдельной сборке, чтобы мы могли ссылаться на них в других проектах в будущем.

Нажмите правой кнопкой на Solution и выберите Add->New Project, после чего выберите тип проекта Class Library и назовите его MyResources.

Нажмите правой кнопкой на проекте MyResources и выберите Add->New Item, Resource File, назвав его Resources.resx. Это будет наша культура по умолчанию (en-US), так как у названия файла нет специальных суффиксов. Добавьте следующие имена и значения в файл как показано на изображении.

clip_image003

clip_image004

Не забудьте отметить модификатор доступа к ресурсу как public, чтобы к нему можно было обращаться из других проектов. Теперь создайте новый файл ресурсов и назовите его "Resources.es-CL.resx". Его содержимое показано на изображении.

clip_image005

Повторите последовательность действий для арабской версии. Может так получиться, что вы не сможете ввести некоторые символы из-за отсутствия поддержки арабского в вашей ОС, но вы можете просто скачать файлы со ссылки выше.

clip_image006

Нам нужно добавить ссылку на проект MyResources в наше приложение, чтобы можно было загружать строки ресурсов прямо с веб-сайта. Нажмите правой кнопкой на References в проекте MvcInternationalization и выберите проект MyResources во вкладке Projects.

Определение культуры

Браузер в каждом запросе отправляет заголовок Accept-Language, содержащий список с элементами типа язык-страна, которые сконфигурированы в пользовательском браузере. Проблема в том, что культура может не относиться к предпочитаемому пользователем языку (если он находится в компьютерном клубе, например). Мы должны позволить пользователю выбирать язык. Чтобы это сделать, можно хранить предпочитаемый пользователем язык в каком-либо хранилище, например, в cookies.

Мы создадим базовый контроллер, загружающий содержимое cookies или, если соответствующие cookies отсутствуют, будет использоваться заголовок Accept-Language, посылаемый пользовательским браузером. Создайте контроллер и назовите его BaseController:

 namespace MvcInternationalization.Controllers
{
    public class BaseController : Controller
    {
        
        protected override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            // Is it View ?
            ViewResultBase view = filterContext.Result as ViewResultBase;
            if (view == null) // if not exit
                return;

            string cultureName = Thread.CurrentThread.CurrentCulture.Name; // e.g. "en-US" // filterContext.HttpContext.Request.UserLanguages[0]; // needs validation return "en-us" as default            

            // Is it default culture? exit
            if (cultureName == CultureHelper.GetDefaultCulture())
                return;

            
            // Are views implemented separately for this culture?  if not exit
            bool viewImplemented = CultureHelper.IsViewSeparate(cultureName);
            if (viewImplemented == false)
                return;
            
            string viewName = view.ViewName;

            int i = 0;

            if (string.IsNullOrEmpty(viewName))
                viewName = filterContext.RouteData.Values["action"] + "." + cultureName; // Index.en-US
            else if ((i = viewName.IndexOf('.')) > 0)
            {
                // contains . like "Index.cshtml"                
                viewName = viewName.Substring(0, i + 1) + cultureName + viewName.Substring(i);
            }
            else
                viewName += "." + cultureName; // e.g. "Index" ==> "Index.en-Us"

            view.ViewName = viewName;

            filterContext.Controller.ViewBag._culture = "." + cultureName;

            base.OnActionExecuted(filterContext);
        }


        protected override void ExecuteCore()
        {
            string cultureName = null;
            // Attempt to read the culture cookie from Request
            HttpCookie cultureCookie = Request.Cookies["_culture"];
            if (cultureCookie != null)
                cultureName = cultureCookie.Value;
            else
                cultureName = Request.UserLanguages[0]; // obtain it from HTTP header AcceptLanguages

            // Validate culture name
            cultureName = CultureHelper.GetValidCulture(cultureName); // This is safe


            // Modify current thread's culture            
            Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(cultureName);
            Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(cultureName);
           
            base.ExecuteCore();
        }
    }
}

Контроллер проверяет на наличие cookies и определяет текущую культуру треда. Конечно, мы должны проверять значения cookies, так как они могут быть отредактированы на клиентской стороне, что и будем делать с помощью вспомогательного класса CultureHelper. Если имя культуры не проходит валидацию, будет возвращена стандартная культура. После этого, когда мы вызовем Return в методе контроллера, имя представления будет изменено контроллером. Таким образом мы подтверждаем тот факт, что пользователь получит правильный контент согласно своей культуре.

Класс CultureHelper

Класс CultureHelper позволяет хранить имена культур, реализованные на нашем сайте

clip_image007

 namespace MvcInternationalization.Utility
{
    public static class CultureHelper
    {
        // Include ONLY cultures you are implementing as views
        private static readonly  Dictionary<String, bool> _cultures  = new Dictionary<string,bool> {
            {"en-US", true},  // first culture is the DEFAULT
            {"es-CL", true},
            {"ar-JO", true}
        };


        /// <summary>
        /// Returns a valid culture name based on "name" parameter. If "name" is not valid, it returns the default culture "en-US"
        /// </summary>
        /// <param name="name">Culture's name (e.g. en-US)</param>
        public static string GetValidCulture(string name)
        {
            if (string.IsNullOrEmpty(name))
                return GetDefaultCulture(); // return Default culture

            if (_cultures.ContainsKey(name))
                return name;

            // Find a close match. For example, if you have "en-US" defined and the user requests "en-GB", 
            // the function will return closes match that is "en-US" because at least the language is the same (ie English)            
            foreach(var c in _cultures.Keys)
                if (c.StartsWith(name.Substring(0, 2)))
                    return c;

            
            // else             
            return GetDefaultCulture(); // return Default culture as no match found
        }


        /// <summary>
        /// Returns default culture name which is the first name decalared (e.g. en-US)
        /// </summary>
        /// <returns></returns>
        public static string GetDefaultCulture()
        {
            return _cultures.Keys.ElementAt(0); // return Default culture

        }


        /// <summary>
        ///  Returns "true" if view is implemented separatley, and "false" if not.
        ///  For example, if "es-CL" is true, then separate views must exist e.g. Index.es-cl.cshtml, About.es-cl.cshtml
        /// </summary>
        /// <param name="name">Culture's name</param>
        /// <returns></returns>
        public static bool IsViewSeparate(string name)
        {
            return _cultures[name];
        }

    }
}

_cultures мы должны заполнять вручную, это список имен культур, которых поддерживает наш сайт. Первый параметр – имя культуры (например, en-US), второй показывает, реализовали ли мы отдельные представления для этой культуры. Если второй параметр false, используется стандартное представление.

Достоинством реализованного класса является то, что он поддерживает поиск похожих языков. Например, если пользователь из Аргентины (es-ar), то есть культуры, не поддерживаемой нашим сайтом, он увидит контент сайта с использованием es-cl (испанский, Чили) вместо английского языка. Таким образом, нет необходимости реализовывать поддержку всех культур до тех пор, пока вам не нужно реализовать поддержку таких данных, как валюта, формат даты и прочее.

Контроллеры

Visual Studio уже создал контроллер HomeController, поэтому мы будем использовать его. Измените HomeController.cs:

 namespace MvcInternationalization.Controllers
{
    public class HomeController : BaseController
    {
        [HttpGet]
        public ActionResult Index()
        {
          
            return View();
        }

        [HttpPost]
        public ActionResult Index(Person per)
        {
                 return View();
        }

        public ActionResult SetCulture(string culture)
        {
            // Validate input
            culture = CultureHelper.GetValidCulture(culture);

            // Save culture in a cookie
            HttpCookie cookie = Request.Cookies["_culture"];
            if (cookie != null)
                cookie.Value = culture;   // update cookie value
            else
            {

                cookie = new HttpCookie("_culture");
                cookie.HttpOnly = false; // Not accessible by JS.
                cookie.Value = culture;
                cookie.Expires = DateTime.Now.AddYears(1);
            }
            Response.Cookies.Add(cookie);

            return RedirectToAction("Index");
        }

        public ActionResult About()
        {
            
           return View();
        }        

    }
}

Метод "SetCulture" предоставляет пользователю возможность изменять текущую культуру и сохранять ее в cookie _culture. Можно сохранять имя культуры в Session или где-либо ещё, но cookies хороши своей легковесностью и отсутствием копии на серверной стороне.

Создание шаблона представления

Теперь мы создадим представление, ассоциированное с методом Index контроллера HomeController. Для этого сначала удалите существующий файл Index.cshtml в папке Views/Home. Затем нажмите правой кнопкой мыши на методе HomeController.Index() и выберите команду Add View. clip_image008

Отметьте "Create a strongly-typed view", выберите класс Person. Выберите также Create в выпадающем меню Scaffold template. Нажмите Add и измените содержимое созданного представления:

clip_image009

Теперь сделайте две копии Index.cshtml, назвав их Index.es-CL.cshtml и Index.ar-JO.cshtml. Эти представления будут отображать контент локализованных версий Index.cshtml. Внесите изменения:

clip_image010

clip_image011

«Испанское» представление

image

«Арабское» представление

Разумеется, для простых частичных представлений типа _LogOnPartial.cshtml и представлений, на которые не ссылаются контроллеры, мы можем спокойно использовать строки ресурсов.

image

«Арабское» представление

Пробуем

Запустите веб-сайт. Обратите внимание на то, что отрабатывает валидация на клиентской стороне.Понажимайте на radio buttons, что переключиться между культурами, и убедитесь, что язык с нестандартной разметкой отображается корректно. Использование отдельных представлений позволяет контролировать позиционирование элементов, что делает представления легкочитаемыми.

image

Английский

image

Испанский

image

Арабский

Локализация на клиентской стороне

Скрипты на клиентской стороне

В случае клиентской стороны мы должны побеспокоиться о числах, датах, времени и сообщениях, так как они меняются от культуры к культуре. Есть много методов реализации локализации на клиентской стороне, вот два из них:

  1. Создание отдельных локализированных файлов javascript для каждой культуры и языка.
  2. Создание одного файла javascript для всех культур с использованием Microsoft Ajax Library.

Для первого метода мы будем пользоваться одними правилами для представлений и файлов ресурсов. Например, для файла myscript.js нужно будет создать "myscript.es-CL.js", "myscript.ar-JO.js", и пр. На скрипты в представлении легко сделать ссылку путем добавления суффика имени культуры к имени файла javascript:

 <script src="@Url.Content("~/Scripts/myscript" + ViewBag._culture + ".js")" type="text/javascript"></script>

Переменная _culture уже определена в контроллере.

Даже если вы захотите использовать Microsoft Ajax Library, вам могут понадобиться отдельные файлы javascript, определяющие текстовые сообщения для пользователя. Вы можете определить строковый объект, содержащий список сообщений или использовать отдельные представления для каждой культуры, или использовать код javascript в представлениях вместо вынесения их в отдельные файлы.

Загрузить код

Download code!

Резюме

Создание мультиязыкового веб-приложения – дело нелегкое, но необходимое, если веб-приложение ориентировано на пользователей со всего мира. Глобализация, конечно, не в первых приоритетах при разработке сайта, однако, она должна быть хорошо продумана в начале разработки, чтобы дальнейшая разработка происходила легче. К счастью, ASP.NET поддерживает глобализацию и имеет некоторые удобные .NET классы.

Итак, мы рассмотрели процесс создания ASP.NET MVC 3 приложения с использованием трёх различных языков, включая один с нестандартной разметкой. Предлагаем последовательность действий для глобализации сайта ASP.NET MVC 3:

  1. Добавьте контроллер, от которого будут наследоваться все контроллеры и который будет перехватывать имена представлений и настраивать их для текущей культуры.
  2. Добавьте вспомогательный класс, который будет хранить список имен поддерживаемых культур.
  3. Создайте представление или группу представлений для каждой культуры и языка.
  4. Создайте файлы ресурсов с сообщениями для различных культур и языков (например, Resources.resx, Resources.es-CL.resx, Resources.ar-JO.resx)
  5. Локализируйте файлы Javascript.

Благодарности

Это перевод оригинальной статьи ASP.NET MVC 3 Internationalization. Благодарим за помощь в переводе Александра Белоцерковского.