Данная статья будет узконаправленной и покрывает локализацию через БД, поэтому подробно расписывать как делать локализацию с помощью файлов ресурсов (resx) можно посмотреть, например, тут: MVC 2: Полное руководство по локализации. Для локализации с помощью представлений я тоже там ссылки.
Для начала я кратко расскажу о вариантах локализации сайта, покажу пример создания своего ResourceProviderFactory, после чего создам небольшое приложение для демонстрации.
Варианты локализации
Во многих обсуждениях и статьях упоминается лишь только два варианта локализации, например, статья ASP.NET MVC 3 Internationalization, на которую можно встретить множество ссылок выделяет следующие:
— Файлы ресурсов (resx)
— Использовать разные «Представления» (View)
Первый способ как правило применяется для статики: названий полей, валидации и прочего. Существенным минусом использования второго является, необходимость делать много ручной работы по копирования одного и того же кода, в случае, если нужно будет даже незначительно поменять верстку, также сложно представить структуру сайта с множеством языков, количество файлов будет огромным, иногда перевод разбивают по директориям, становится конечно нагляднее, но масштабируемость оставляет желать лучшего. В моём случае мне нужно было переводить динамический контент, который добавляется через админку, вариант редактирования resx файлов из админки я не рассматривал, но реализации Вы можете найти самостоятельно, как говорится затея на любителя, поэтому выделяем третий вариант:
— Локализация с помощью БД
Конечно же можно комбинировать все эти три варианта.
Пример реализации
Сразу скажу, что я создаю пустой проект MVC 3, так как буду использовать Entity Framework Code First, переписывать Membership Provider в данной статье я не буду, пример как это делать можете посмотреть, например, тут: Custom Membership Providers. Просто запомните, что «админка» будет общедоступна, конечно можно было реализовать авторизацию, через конфиг файл как это демонстрирует Стивен Сандерсон в своих книгах, но статья о другом.
Сделаем пародию на склад, у нас будет таблица продуктов с 4-мя полями:
-Идентификатор
-Имя продукта (его мы будем переводить с помощью БД)
-Цена (данное поле нам нужно для демонстрации проблем с валидацией при локализации)
-Дата привоза (аналогично предыдущему)
Следующим этапом создадим класс Product и установим атрибуты с помощью Data Annotations (если Вам не нравится такой вариант, то можете воспользоваться Fluent API, к которому в любом случае придётся обращаться в крупном проекте) и создадим DbContext:
public class Product
{
public int ProductId { get; set; }
[Required]
[StringLength(128)]
public string Name { get; set; }
[Required]
public decimal Price { get; set; }
[Required]
public DateTime ImportDate { get; set; }
}
public class ProductDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
}
Теперь я сгенерирую контроллер и все действия (Actions) автоматически, получилось немного страшновато, поэтому придётся добавить стилей, правда я бы рекомендовал использовать несколько другой подход для генерации. Объединение создания и редактирования в одном представлении, например, как тут: Непутевые заметки о ASP.NET MVC. Часть 1 (и единственная), что уберет одно представление, старайтесь, чтобы у Вас было как можно меньше «копипаста».
В итоге у нас получилась такая таблица:
Переходим к ResourceProviderFactory, немного погуглив я нашёл довольно старую статью в MSDN Extending the ASP.NET 2.0 Resource-Provider Model, а также описание ResourceProviderFactory Class с примером реализации, но уже для 4-го фреймворка. На том же codeproject есть готовый пример, который тоже можно взять за основу: ASP.NET 2.0 Custom SQL Server ResourceProvider.
Создадим теперь класс для хранения переводов:
public class GlobalizationResourse
{
public int GlobalizationResourseId { get; set; }
[Required]
[StringLength(128)]
public string ResourseObject { get; set; }
[Required]
[StringLength(128)]
public string ResourseName { get; set; }
[Required]
[StringLength(5)]
public string Culture { get; set; }
[Required]
[StringLength(4000)]
public string ResourseValue { get; set; }
}
И не забудьте его добавить в контекст БД. У меня получилась средняя реализация между codeproject и примером из MSDN, код можно скачать в конце статьи, так как там около 150 строк. И добавим провайдера в конфиг:
<system.web>
<globalization enableClientBasedCulture="true" resourceProviderFactoryType="DbLocalizationExample.Models.CustomResourceProviderFactory" uiCulture="auto" culture="auto" />
...
</system.web>
Всё бы ничего, но чтобы проверить работу локализации нам нужна возможность выбора языка, для хранения локализации я буду использовать куки (сессию я бы не советовал использовать, так как вряд ли пользователь обрадуется зайдя через 20 минут (стандартное время жизни насколько я помню) на сайт, что опять нужно выбирать язык). За основу возьмём идею с сайта afana.me и получим такой вот класс:
public static class CultureHelper
{
private static readonly List<string> Cultures = new List<string> {
"ru-RU", // first culture is the DEFAULT
"en-US",
};
/// <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.Contains(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)
if (c.StartsWith(name.Substring(0, 2)))
return c;
return GetDefaultCulture(); // return Default culture as no match found
}
public static string GetDefaultCulture()
{
return Cultures.ElementAt(0); // return Default culture
}
public static string GetCultureFromCookies(HttpRequest request)
{
string cultureName = null;
// Attempt to read the culture cookie from Request
HttpCookie cultureCookie = request.Cookies["_culture"];
if (cultureCookie != null)
{
cultureName = cultureCookie.Value;
}
else if (request.UserLanguages != null)
{
cultureName = request.UserLanguages[0]; // obtain it from HTTP header AcceptLanguages
}
// Validate culture name
return GetValidCulture(cultureName); // This is safe
}
private static string AcceptLanguage()
{
return HttpUtility.HtmlAttributeEncode(System.Threading.Thread.CurrentThread.CurrentUICulture.ToString());
}
public static IHtmlString MetaAcceptLanguage<T>(this HtmlHelper<T> html)
{
return new HtmlString(String.Format(@"<meta name=""accept-language"" content=""{0}"" />", AcceptLanguage()));
}
public static IHtmlString GlobalizationLink<T>(this HtmlHelper<T> html)
{
return new HtmlString(String.Format(@"<script src=""../../Scripts/globalization/cultures/globalize.culture.{0}.js"" type=""text/javascript""></script>",
AcceptLanguage()));
}
}
Теперь нам осталось добавить действия для установки и чтения куков:
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");
}
А также логику в Global.asax для утановки культуры и проверки GetVaryByCustomString для того, чтобы использовать кэширование.
protected void Application_AcquireRequestState(object sender, EventArgs e)
{
string cultureName = CultureHelper.GetCultureFromCookies(Request);
// Modify current thread's culture
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(cultureName);
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(cultureName);
}
public override string GetVaryByCustomString(HttpContext context, string arg)
{
// It seems this executes multiple times and early, so we need to extract language again from cookie.
if (arg == "culture") // culture name (e.g. "en-US") is what should vary caching
{
string cultureName = CultureHelper.GetCultureFromCookies(Request);
return cultureName.ToLower();// use culture name as cache key, "es", "en-us", "es-cl", etc.
}
return base.GetVaryByCustomString(context, arg);
}
Пару слов о логике перевода: у меня в базе данных будет храниться язык по умолчанию, т.е. просто объект Product, но когда я захочу добавить ему перевод я напишу в представлении:
@(Culture == "ru-RU" ? item.Name : HttpContext.GetLocalResourceObject("/Home/Index", "Product_" + item.ProductId))
Что автоматически добавит значение по умолчанию в БД. Первый параметр похож на путь лишь для наглядности, там может быть любая последовательность символов (ограниченная правда 128 в БД для нашего объявления), второй это уникальный идентификатор.
В Layout добавляем возможность выбора языка:
<div class="language">
<span>@Html.ActionLink("rus", "SetCulture", "Home", new { culture = "ru-RU" }, null)</span>
<span>@Html.ActionLink("eng", "SetCulture", "Home", new { culture = "en-US" }, null)</span>
</div>
Можно запускать, но не тут то было, я поменял контекст (добавил класс для ресурсов) и теперь EF отказывается выводить данные из таблицы. Идём в View -> Other Windows -> Package Manager Console и вводим (каждая строка отдельно):
Update-Package EntityFramework
Enable-Migrations
теперь можно создать миграцию и обновить базу:
Add-Migration AddGlobalizationResourses
Update-Database
Но тут нас ждёт огорчение, студия говорит, что мы создали БД с более старым EF, где нет истории миграции, поэтому, чтобы руками не удалять нашу базу, добавим в Index такую строчку (если вы противник миграции, то правильнее её добавлять в Application_Start, но помните, что это удаляет все данные):
Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductDbContext>());
После компиляции и обращения к нему, удалим её, так как в последующем мы сможем наслаждаться всеми плюшками миграции: EF 4.3 Automatic Migrations Walkthrough.
Результат нашей работы будет выглядеть так:
Английский вариант создаётся в БД автоматически, русский же вариант храниться в Product. Логику редактирования БД я оставлю Вам, там нет ничего сложного.
Клиентская валидация
При переключении языка у нас возникает проблема с decimal и Datetime. Для русского языка мы имеем «4,00», а для английского это «4.00». Даты тоже имеют проблемы: «21.12.2012» и «12/21/2012». Для решения этих проблем мы воспользуемся globalize и подключим jquery ui datapicker, чтобы задавать формат автоматически и упростить ввод дат.
Добавим в Layout следующее («ядро» глобализации, глобализия для конкретного языка, мета тег для клиентской части и общие скрипты для валидации чисел и изменения jquery ui datapicker):
<script src="@Url.Content("~/Scripts/globalization/globalize.js")" type="text/javascript"></script>
@Html.GlobalizationLink()
@Html.MetaAcceptLanguage()
<script src="@Url.Content("~/Scripts/common.js")" type="text/javascript"></script>
Это лишь малая часть клиенской валидации, пример локализации можно посмотреть тут: ASP.NET MVC 3 Internationalization — Part 2 (NerdDinner)
Итог
Я рассказал как можно создать свой собственный провайдер ресурсов, создал небольшое приложение демонстрирующее его работу и поделился ссылками где можно прочитать больше информации по данной теме. Как пишет Jon Skeet в своей книге «C# in Depth», что приведенный здесь код — это лишь примеры, я не гарантирую, что код, который Вы возьмете отсюда будет у Вас работать. У меня используется полное кэширование перевода, скорее всего Вам нужно будет загружать перевод постепенно, если будет большой объём информации, устанавливать время жизни и т.д. Помните, что при редактировании перевода нужно обязательно чистить кэш, чтобы данные отобразились сразу (это когда Вы будете реализовывать логику редактирования перевода).
Проект можно скачать тут (Visual studio 2010): ссылка (2,89 Мб)
Источники
Ссылки
Extending the ASP.NET 2.0 Resource-Provider Model
ResourceProviderFactory Class
ASP.NET 2.0 Custom SQL Server ResourceProvider
ASP.NET MVC 3 Internationalization
ASP.NET MVC 3 Internationalization — Part 2 (NerdDinner)
Книги
Freeman A. Sanderson S. — Pro ASP.NET MVC 3 Framework Third Edition — 2011
Julia Lerman and Rowan Miller — Programming Entity Framework:Code First — 2012
Примечание: Если Вы будете делать локализацию по данном руководству ASP.NET MVC 3 Internationalization, то Вам следует помнить, что в MVC 4 ExecuteCore не работает ExecuteCore() in base class not fired in MVC 4 beta .
Автор: eforce