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

MVC 2: Полное руководство по локализации

Время на прочтение 14 мин
Количество просмотров 18K
Автор оригинала: Alex Adamyan
imageВ данной статье мы рассмотрим все аспекты локализации веб приложения основанного на ASP.NET MVC. Я использую последнюю доступную MVC 2 RC 2 версию на время написания данного топика.

До того, как мы начнем я хотел бы поблагодарить команду MVC, отличная работа ребята, я наслаждаюсь процессом написания веб-приложений, когда использую данный фреймворк. Я искал фреймворк такого типа, после небольшого опыта работы с Ruby on Rails.

Мы рассмотрим следующие проблемы:
  1. Валидация представлений
  2. Простой механизм переключения культур
  3. Локализация сообщений валидации модели
  4. Локализация атрибута DisplayName
  5. Кэш и локализация
Для работы вам понадобится Visual Studio 2008 Express и ASP.NET MVC 2 RC2, а также создать новый MVC 2 веб-проект.

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


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

Я предлагаю следующую структуру директорий:

image

Views — файлы ресурсов для aspx страниц. Models — файлы ресурсов для локализации моделей представления.

Директория Views содержит подпапки для каждого контроллера, в которых находятся файлы ресурсов (количество файлов зависит от набора поддерживаемых языков).

Models содержит поддиректории для каждой группы моделей представления. Для сгенерированных моделей Account (LogOn, Register, ChangePassword) у нас есть папка Account и файлы ресурсов для каждой языковой культуры.

Файлы ресурсов


Несколько слов о правилах именования файлов ресурсов. Файлы ресурсов имеют следующий формат имени:
[RESOURCE-NAME].[CULTURE].resx

RESOURCE-NAME — имя файла. Может быть абсолютно любым. Оно используется для группировки, когда у нас несколько файлов ресурсов с одинаковыми resource-name — они составляют единый ресурс с различными культурами, указанных в CULTURE.

CULTURE — индикатор культуры файла ресурса. Культуры бывают двух типов: нейтральные и точные. Нейтральные культуры состоят только из языкового кода (en, ru, de и т.п.). Точные культуры состоят из языкового кода и кода региона (en-US, en-UK)

Также существует специальное значение для файлов ресурсов, у которых не определена культура, они называются культуры «по умолчанию» или «базовые(fall-back)». Как вы можете догадаться по имени, они используются для файлов ресурсов, если текст не был найден в определенном файле ресурсов культуры или же, когда не существует файла для заданной культуры. Я настоятельно рекомендую вам использовать базовый файлы ресурсов, особенно если пользователь имеет возможность каким-то образом установить не поддерживаемую культуру.

Несколько примеров файлов ресурсов:

MyStrings.en-US.resx — английский США

MyStrings.en-UK.resx — английский Великобритания

MyStrings.en.resx — нейтральный английский (по умолчанию для английского)

MyStrings.ru.resx — нейтральный русский

MyStrings.resx — базовый файл ресурсов.

Отлично, теперь мы готовы что-нибудь локализировать и посмотреть как оно работает. Я покажу вам небольшой пример локализации заголовка в созданном веб-приложении. В примере я буду использовать два языка: английский(по умолчанию) и нейтральный русский, но вы можете использовать и любые другие языки.

Прежде всего, создайте структуру папок, как я описал выше, нам нужны будут файлы ресурсов для родительской страницы Site.Master. Я создам директорию Shared в Resources\Views и добавлю два файла ресурсов:

SharedStrings.resx — базовый файл ресурсов с данными на английском.

SharedStrings.ru.resx — базовый файл ресурсов с данными на русском.

Добавьте и заполните свойство «Title» в оба файла.

image

Важно! Убедитесь, что модификаторы доступа для каждого файла ресурсов установлены в public. Также проверьте, что у файлов ресурсов значение свойства «Custom Tool» равно «PublicResXFileCodeGenerator». Иначе файлы ресурсов не будут скомпилированы и доступны.

image

image

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

[PROJECT-NAME].Resources.Views.Shared

Для улучшения читабельности и соответствию правил именования я изменил свойства Custom Tool Namespace файлов ресурсов на ViewRes (для файлов ресурсов представлений).

Пришло время вносить изменения в страницу Site.Master.

<div id="title">

<h1>My MVC Application</h1>
</div>
замените на
<div id="title">

<h1><%=ViewRes.SharedStrings.Title%></h1>

</div>

Запустите приложение и убедитесь, что все работает и вы видите заголовок в нужном месте (сейчас он должен считываться с файла ресурсов). Если все работает, перейдем к демонстрированию, как изменить культуру.

Для изменения культуры, нам нужно изменить свойства CurrentCulture и CurrentUICulture объекта CurrentThread для каждого запроса!!! Для этого мы разместим код, который изменяет культуру в метод Application_AcquireRequestState Global.asax (данный метод является обработчиком события и вызывается для каждого запроса)

Добавьте следующий код в файл Global.asax.cs:
protected void Application_AcquireRequestState(object sender, EventArgs e)
{
  //Create culture info object
  CultureInfo ci = new CultureInfo("en");

  System.Threading.Thread.CurrentThread.CurrentUICulture = ci;
  System.Threading.Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name);
}


Снова запускаем приложение и убеждаемся, что оно работает. Далее изменяем строковый параметр конструктора CulturInfo (в моём случае это будет «ru») и повторно запускаем проект. У вас должно получится следующее:

image

image

Вот и все. Мы локализировали заголовок Site.Master и вы можете проделать это с любым текстом.

Простой механизм переключения культур


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

Хранилищем для выбранной пользователем культуры мы будем использовать объект сессии. И для изменении культуры, разместим ссылки для каждого языка на master-странице. Нажатие на ссылки будет вызывать некое действие в контроллере Account, которое изменяет значение сессии на соответствующую культуру.

Добавьте следующий код в класс AccountController:
public ActionResult ChangeCulture(string lang, string returnUrl)
{
    Session["Culture"] = new CultureInfo(lang);
    return Redirect(returnUrl);
}

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

Теперь создадим простой пользовательский элемент управления, который содержит ссылки на культуры. Добавьте новое частичное представление (partial view) в Views\Shared директорию, назовите файл CultureChooserUserControl.ascx и вставьте в него следующее:
<%= Html.ActionLink("English", "ChangeCulture", "Account"
   new { lang = "en", returnUrl = this.Request.RawUrl }, null)%>
<%= Html.ActionLink("Русский", "ChangeCulture", "Account"
   new { lang = "ru", returnUrl = this.Request.RawUrl }, null)%>


Итак, мы создали две ссылки, первая для английского, а вторая для русского языка. Пришло время разместить данный элемент управления на Site.Mater странице. Я добавлю его в , рядом в формой входа.

Найдите и замените в коде <div id=«logindisplay»> следующим:
<div id="logindisplay">
<% Html.RenderPartial("LogOnUserControl"); %>
<% Html.RenderPartial("CultureChooserUserControl"); %>
</div>

Что еще осталось сделать? А Осталось самое важное, мы размещаем объект информации о культуре в сессии, но нигде не используем его. Вновь вносим изменения в файл Global.asax.cs метод Application_AcquireRequestState:
protected void Application_AcquireRequestState(object sender, EventArgs e)
{
    //Очень важно проверять готовность объекта сессии
    if (HttpContext.Current.Session != null)
    {
      CultureInfo ci = (CultureInfo)this.Session["Culture"];
      //Вначале проверяем, что в сессии нет значения
      //и устанавливаем значение по умолчанию
      //это происходит при первом запросе пользователя
      if (ci == null)
      {
        //Устанавливает значение по умолчанию - базовый английский
        string langName = "en";
        //Пытаемся получить значения с HTTP заголовка
        if (HttpContext.Current.Request.UserLanguages != null && HttpContext.Current.Request.UserLanguages.Length != 0)
        {
          //Получаем список 
          langName = HttpContext.Current.Request.UserLanguages[0].Substring(0, 2);
        }
        ci = new CultureInfo(langName);
        this.Session["Culture"] = ci;
      }
      //Устанавливаем культуру для каждого запроса
      Thread.CurrentThread.CurrentUICulture = ci;
      Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name);
    }
}

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

image

Локализация сообщений валидации модели


Я нашел отличное решение данной проблемы, опубликованное Филом Хааком, но так как данная статья должна быть полным руководством, я не могу не коснуться данного вопроса, а также существуют определенные неясности, которыя я хотел бы прояснить. Но прежде всего, я рекомендую прочитать пост Фила Хаака

Я объясню, как локализировать валидационные сообщения модели Account, в особенности для RegistrationModel. Также я хочу описать, как локализировать валидационные сообщения Membership, которые прописаны прямо в коде контроллера AccountController.

Итак, давайте создадим ValidationStrings.resx и ValidationStrings.ru.resx в папке Resources\Models\Account (убедитесь в публичности модификаторов доступа). Как вы уже догадались, мы будем хранить валидационные сообщения в этих файлах.

Я создал следующие свойства в обоих файлах (пример английского):

image

Мы должны изменить наши модели следующим образом (пример RegisterModel):
[PropertiesMustMatch("Password", "ConfirmPassword",
ErrorMessageResourceName = "PasswordsMustMatch",
ErrorMessageResourceType = typeof(ValidationStrings))]
   public class RegisterModel
   {
     [Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ValidationStrings))]
     [DisplayName("Username")]
     public string UserName { get; set; }
     [Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ValidationStrings))]
     [DataType(DataType.EmailAddress)]
     [DisplayName("Email")]
     
     public string Email { get; set; }
     [Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ValidationStrings))]
     [ValidatePasswordLength(ErrorMessageResourceName = "PasswordMinLength",ErrorMessageResourceType = typeof(ValidationStrings))]
     [DataType(DataType.Password)]
     [DisplayName("Password")]
     public string Password { get; set; }
 
     [Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ValidationStrings))]
     [DataType(DataType.Password)]
     [DisplayName("Confirm password")]
     public string ConfirmPassword { get; set; }
}

Мы добавим свойства ErrorMessageResourceName и ErrorMessageResourceType к атрибутам Required, PropertiesMustMatch и ValidatePasswordLength, где ErrorMessageResourceType является типом класса ресурса, в котором хранятся сообщения и ErrorMessageResourceName является названием свойства. К сожалению не существует строго типизированного вариант чтения свойства так что убедитесь, что у данных магических строк корректные значения.

Мы почти у цели, осталась маленьках деталь. У нас присутствуют два собственных атрибута валидации PropertiesMustMatchAttribute и ValidatePasswordLenghtAttribute, в которых мы должны изменить CultureInfo.CurrentUICulture в методе FormatErrorMessage на CultureInfo.CurrentCulture, иначе в нашем случае ничего заработает.

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

image

Ой, как вы заметили, мы забыли локализировать имена свойств в моделях представления и получили небольшую кашу. Для этого нам нужно локализировать значения атрибута DisplayName, но это не так просто, как кажется на первый взгляд. Я расскажу об этом в следующей главе, а сейчас давайте доделаем оставшуюся деталь. Это локализация валидационных сообщений Membership API.

Откройте AccountController и переместитесь в конец файла, там должен быть метод ErrorCodeToString, который создает сообщение об ошибке, если пользовательская регистрация провалилась. Все сообщения жестко прописаны в коде. Все что нам нужно — это создать соответствующие свойства для каждого сообщения в, уже созданном, ValidationStrings файле ресурса и поместить их туда, вместо текста в методе ErrorCodeToString.

На этом все с моделью валидации. Пришло время для DisplayName!

Локализация атрибута DisplayName


Как мы заметили в предыдущей главе, значение DisplayName учавствует в валидационных сообщениях, которые используют параметры для форматирования. Еще одним поводом задуматься о атрибуте DisplayName, являются текстовые метки полей в HTML-форме, они созданы при участии значения DisplayName.

Реальной проблемой является то, что DisplayName не поддерживает локализацию, не существует способа связать его с файлом ресурса, откуда он будет брать значение.

Это означает, что нам нужно расширить DisplayNameAttribute и переписать свойство DisplayName, таким образом, чтобы оно возвращало локализированное имя. Я создал унаследованный класс и назвал его LocalizedDisplayName.
public class LocalizedDisplayNameAttribute : DisplayNameAttribute
{
   private PropertyInfo _nameProperty;
   private Type _resourceType;
 
   public LocalizedDisplayNameAttribute(string displayNameKey)
     : base(displayNameKey)
   {
 
   }
 
   public Type NameResourceType
   {
     get
     {
       return _resourceType;
     }
     set
     {
       _resourceType = value;
       //инициализация nameProperty, когда тип свойства устанавливается set'ром
       _nameProperty = _resourceType.GetProperty(base.DisplayName, BindingFlags.Static | BindingFlags.Public);
     }
   }
 
   public override string DisplayName
   {
    get
    {
       //проверяет,nameProperty null и возвращает исходный значения отображаемого имени
       if (_nameProperty == null)
       {
         return base.DisplayName;
       }
 
       return (string)_nameProperty.GetValue(_nameProperty.DeclaringType, null);
     }
   } 
}

Важной деталью является понимание того, что нам нужно считывать значение свойства, каждый раз, когда оно запрашивается, поэтому метод GetValue вызывается в get'ере свойства DisplayName, а не в конструкторе.

Для хранения отображаемых имен, я создал файлы ресурсов Names.resx и Names.ru.resx в папке Resources\Models\Account и создал следующие свойства.

image

Теперь нам нужно заменить атрибут DisplayName на LocalizedDisplayName и предоставить тип класса ресурса. Измененный код RegisterModel будет выглядеть следующим образом:
[PropertiesMustMatch("Password", "ConfirmPassword",
ErrorMessageResourceName = "PasswordsMustMatch",
ErrorMessageResourceType = typeof(ValidationStrings))]
   public class RegisterModel
   {
     [Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ValidationStrings))]
     [LocalizedDisplayName("RegUsername", NameResourceType = typeof(Names))]
     public string UserName { get; set; }
 
     [Required(ErrorMessageResourceName = "Required",ErrorMessageResourceType = typeof(ValidationStrings))]
     [DataType(DataType.EmailAddress)]
     [LocalizedDisplayName("RegEmail", NameResourceType = typeof(Names))]
     public string Email { get; set; }

     [Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ValidationStrings))]
     [ValidatePasswordLength(ErrorMessageResourceName = "PasswordMinLength",
ErrorMessageResourceType = typeof(ValidationStrings))]
     [DataType(DataType.Password)]
     [LocalizedDisplayName("RegPassword", NameResourceType = typeof(Names))]
     public string Password { get; set; }
 
     [Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ValidationStrings))]
     [DataType(DataType.Password)]
     [LocalizedDisplayName("RegConfirmPassword", NameResourceType = typeof(Names))]
     public string ConfirmPassword { get; set; }

   }

* This source code was highlighted with Source Code Highlighter.

Запустите приложение и убедитесь в его работоспособности, выглядеть должно так:

image

Кэш и локализация


Странная глава, не так ли? Вы думаете как объединить кэширование и локализацию? Хорошо, давайте представим следующий сценарий: откройте HomeController и добавьте атрибут OutputCache в метод действия Index:
[OutputCache(Duration=3600, VaryByParam="none")]
 public ActionResult Index()
 {
   ViewData["Message"] = "Welcome to ASP.NET MVC!";

   return View();
 }

Запускаем приложение и пробуем изменить язык на Index странице, чтобы проверить, что заголов все еще локазалирован.

Вот чёрт! Вы наверное подумали, что кэширование и локазилация не могут быть использованы вместе? Не переживайте, решение данной проблемы существует :)

Что вы знаете о OutputCache, а точнее, что вы знаете о свойстве VaryByCustom? Пришло время его использовать.

Когда мы запрашиваем Index страницу первый раз, OutputCache кэширует страницу. При втором запросе (когда мы нажимаем на ссылке выбора языка) OutputCache думает, что ничего не изменилось и возвращает результат из кэша, следовательно страница повторно не создается. Вот поэтому не работал выбор языка. Для разрешения данной проблемы, нам нужно каким-то образом сказать OutputCache, что версия страницы изменилась (как в случае, когда действие получает определенный параметр и мы передаем его в свойства VaryByParam).

VaryByCustom — идеальный кандидат для нашего решения проблемы и существует специальный метод у класса System.Web.HttpApplication в файле Global.asax.cs. Мы перепишем стандартную реализацию данного метода:
public override string GetVaryByCustomString(HttpContext context, string value)
 {
    if (value.Equals("lang"))
    {
      return Thread.CurrentThread.CurrentUICulture.Name;
    }
    return base.GetVaryByCustomString(context,value);
 }

Вначале метод проверяет, если значение параметра соответствует «lang» (никакого особого значения, простая строка, которая используется, как значение VaryByCustom) и, если это так, возвращает имя текущей культуры. Иначе возвращает значение стандартной реализации.

Теперь добавим свойство VaryByCustom со значением «lang» в каждый атрибут OutputCache, который вы хотите использовать локализацией и на этом все. Обновленный метод действия Index выглядит следующим образом:

[OutputCache(Duration=3600,VaryByParam="none", VaryByCustom="lang")]
 public ActionResult Index()
 {
    ViewData["Message"] = "Welcome to ASP.NET MVC!";
    return View();
 }

Попробуйте снова запустить приложение и насладится рабочим переключением культур.

Мы закончили последнюю главу и, надеюсь, я ничего не упустил.
Теги:
Хабы:
+28
Комментарии 11
Комментарии Комментарии 11

Публикации

Истории

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

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