Локализация сайта asp.net mvc

Локализация через ресурсы

Наиболее распространненый способ локализации сайта использование ресурсов. Сразу хочется отметить основные минусы этого подхода:

Но для небольших и простых проектов этот способ может подойти. В итоге создания файлов ресурсов получается что-то вроде:

локализация через ресурсы asp.net

 Этот подход к локалицаии сайтов мы рассматривать не будем в силу основных минусов данного способа. Рассмотрим альтернативный способ локализации сайтов с использованием базы данных. Преимущества рассматриваемого способа:

Данный подход состоит из 2-х этапов: локализация View и локализация Model. Иначе говоря локализация статичных текстов (в представлении) и локализация моделей из базы данных.

Виды локализации в базе данных

1. Дополнительная таблица для каждого объекта

Примерная схема базы данных выглядит следующим образом:

MYITEMS
-------
 - MyItemId BIGINT PK
 - MyItemPrice DECIMAL

MYITEMLOCALIZED
---------------
 - CPK_MyItemId BIGINT FK
 - CPK_LanguageCode NCHAR
 - LocalizedName NVARCHAR
 - LocalizedResourcePath NVARCHAR

CUSTOMERS
---------
 - CustomerId BIGINT PK
 - CustomerName NVARCHAR

CUSTOMERLOCALIZED
---------------
 - CPK_CustomerId BIGINT FK
 - CPK_LanguageCode NCHAR
 - LocalizedName NVARCHAR
 - LocalizedResourcePath NVARCHAR

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

2. Одна таблица для переводов

MYITEMS
-------
 - MyItemId BIGINT PK
 - MyItemPrice DECIMAL
 - ItemNameLocalizationGuid uniqueidentifier(GUID)
 - ItemPictureLocalizationGuid uniqueidentifier(GUID)


CUSTOMERS
---------
 - CustomerId BIGINT PK
 - CustomerName NVARCHAR
 - CustomeerNameLocalizationGuid uniqueidentifier(GUID)

LOCALIZED
---------------
 - CPK_ElementGuid uniqueidentifier FK
 - CPK_LanguageCode NCHAR
 - LocalizedValue NVARCHAR
 

В таком случае создается одна дополнительная таблица, которая будет содержать все тексты переводов всех объектов. Существуют варианты создания такой таблицы. В случае когда идентификаторы объектов являются GUID - достаточно одной колонки для идентификации объектов. В случае, если объект идентифицируется целым числом необхъходима (во избежания коллизий) дополнительная колонка и Типом Объекта. Я бы и в случае с GUID добавил такую колонку (так увеличится скорость поиска).

Также возможные вариции между этими двумя вариантами. В своем выборе я остановился на первом варианте с использованием ISO-639 кодов для определения языка (дополнительная колонка).

Локализация View

В качестве основы для локализации этой части сайта была рассмотрена библиотека Griffin.MvcContrib. Плюсы этой библиотеки: просто разобраться, быстро работает. Из минусов можно отметить: не очень логичный способ перевода. На основе этой библиотеки был разработан следующий модуль.

Таблицы SQL Server

CREATE TABLE [dbo].[LocalizedViews](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[LanguageId] [char](2) NOT NULL,
	[ViewPath] [varchar](50) NOT NULL,
	[KeyName] [varchar](50) NOT NULL,
	[Value] [text] NOT NULL,
	[UpdatedAtUTC] [datetime] NOT NULL CONSTRAINT [DF_LocalizedViews_UpdatedAt]  DEFAULT (getdate()),
 CONSTRAINT [PK_LocalizedViews] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
))

ALTER TABLE [dbo].[LocalizedViews]  WITH CHECK ADD  CONSTRAINT [FK_LocalizedViews_Languages] FOREIGN KEY([LanguageId])
REFERENCES [dbo].[Languages] ([Id])
GO

ALTER TABLE [dbo].[LocalizedViews] CHECK CONSTRAINT [FK_LocalizedViews_Languages]
GO

В этой таблице будет содержаться весь перевод статичных текстов сайта (кнопки, надписи, заголовки). Идентификатор ресурса состоит из 2-х частей (ViewPath и KeyName). Таком образом KeyName может повторяться, а по ViewPath можно разделить и сгруппировать различные разделы сайта. При этом эту информацию можно использовать как во View (cshtml), так и в Controller. В библиотеке используется IViewLocalizationRepository, что на мой взгляд немного усложняет код, правда делает его более универсальным и поддерживает разные базы данных. Но при использовании asp.net mvc + sql server можно не изобретать нечто подобное.

Основная идея локализации html состоит в том, чтобы унаследовать WebViewPage и сделать процесс локализации более удобным.

/// <summary>
/// Base page adding support for the new helpers in all views.
/// </summary>
public abstract class OxozleWebViewPage<TModel> : WebViewPage<TModel>
{
    private ViewLocalizer _viewLocalizer;

    /// <summary>
    /// Gets class used for the view localization
    /// </summary>
    protected virtual ViewLocalizer ViewLocalizer
    {
        get
        {
            if (_viewLocalizer == null)
            {
                _viewLocalizer = DependencyResolver.Current.GetService<ViewLocalizer>();
                if (_viewLocalizer == null)
                {
                    var repos = DependencyResolver.Current.GetService<IViewLocalizationRepository>();
                    if (repos == null)
                        throw new Exception("You must register a ViewLocalizer or an IViewLocalizationRepository in your container.");

                    _viewLocalizer = new ViewLocalizer(repos);
                }
            }

            return _viewLocalizer;
        }
    }




    /// <summary>
    /// GetText inspired localization
    /// </summary>
    /// <param name="key"></param>
    /// <param name="formatterArguments">optional arguments if the string contains {} formatters</param>
    /// <returns></returns>
    public MvcHtmlString T(string siteArea, string key, params object[] formatterArguments)
    {
        var translated = ViewLocalizer.Translate(siteArea, key);
        return
            MvcHtmlString.Create(formatterArguments.Length == 0
                                     ? translated
                                     : string.Format(translated, formatterArguments));
    }
}

/// <summary>
/// Required to be able to switch page in the Views\Web.Config, but isn't extended with any new stuff.
/// </summary>
public abstract class OxozleWebViewPage : WebViewPage
{
}

Все методы сделаны на основе описанной выше библиотеки. Множество классов просто выпилино для упрощения. Если понядобятся подробности или исходный код - можно спросить в комментариях.

Аналогичный код добавляем в базовом классе контроллера. Также в базовом классе контроллера добавляем логику для определения языка пользователя.

public class ControllerBase : Controller
{	
	protected override void Initialize(RequestContext requestContext)
	{
		string language = null;

		if (OxoCookies.RequestCookies.Contains("LanguageId"))
		{
			language = OxoCookies.RequestCookies["LanguageId"].ToString();
		}
		//Сюда попадаем, если язык не определен
		else if (requestContext.HttpContext.Request.UserLanguages != null &amp;&amp;
				 requestContext.HttpContext.Request.UserLanguages.Length &gt; 0)
		{
			language = requestContext.HttpContext.Request.UserLanguages[0];
		}

		if (language.IsEmpty())
		{		
			language = "ru";
		}

		var ci = LanguageService.GetLanguage(language).CreateCultureInfo();
		Thread.CurrentThread.CurrentCulture = ci;
		Thread.CurrentThread.CurrentUICulture = ci;
		Language = Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName;
		OxoCookies.SetValue("LanguageId", Language, DateTime.Now.AddYears(1));

		base.Initialize(requestContext);
	}

	#region Translate

	private ViewLocalizer _viewLocalizer;

	/// &lt;summary&gt;
	/// Gets class used for the view localization
	/// &lt;/summary&gt;
	protected virtual ViewLocalizer ViewLocalizer
	{
		get
		{
			if (_viewLocalizer == null)
			{
				_viewLocalizer = DependencyResolver.Current.GetService&lt;ViewLocalizer&gt;();
				if (_viewLocalizer == null)
				{
					var repos = DependencyResolver.Current.GetService&lt;IViewLocalizationRepository&gt;();
					if (repos == null)
						throw new Exception(
							"You must register a ViewLocalizer or an IViewLocalizationRepository in your container.");

					_viewLocalizer = new ViewLocalizer(repos);
				}
			}

			return _viewLocalizer;
		}
	}


	protected string Translate(string siteArea, string key, params object[] formatterArguments)
	{
		var translated = ViewLocalizer.Translate(siteArea, key);
		return
			formatterArguments.Length == 0
				? translated
				: string.Format(translated, formatterArguments);
	}

	#endregion
}

Локализация моделей

Рассмотрим локализацию бизнес объектов на примере игр.

Таблицы SQL Server

CREATE TABLE [dbo].[Games](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[Complexity] [int] NOT NULL,
	[IsOpened] [bit] NOT NULL CONSTRAINT [DF_Games_IsOpened]  DEFAULT ((1)),
	[BookBackgroundFileName] [varchar](50) NULL
)
GO

CREATE TABLE [dbo].[Games_TX](
	[GameId] [int] NOT NULL,
	[LanguageId] [char](2) NOT NULL,
	[PlayersCount] [varchar](50) NULL,
	[Name] [varchar](50) NULL,
	[Description] [varchar](500) NULL
)
GO

ALTER TABLE [dbo].[Games_TX]  WITH CHECK ADD  CONSTRAINT [FK_Games_TX_Games] FOREIGN KEY([GameId])
REFERENCES [dbo].[Games] ([Id])
GO

ALTER TABLE [dbo].[Games_TX] CHECK CONSTRAINT [FK_Games_TX_Games]
GO

ALTER TABLE [dbo].[Games_TX]  WITH CHECK ADD  CONSTRAINT [FK_Games_TX_Languages] FOREIGN KEY([LanguageId])
REFERENCES [dbo].[Languages] ([Id])
GO

ALTER TABLE [dbo].[Games_TX] CHECK CONSTRAINT [FK_Games_TX_Languages]
GO

Предложенный скрипт следует модифицировать под вашу задачу. Основная идея в том, что есть основной объект Game, который содержит всю нелокализованную информацию, одинаковую для всех языков. Дополнительная таблица содержит в каждой строчке перевод игры на другие языки.

sql server локализация в базе данных

Получение DAL объектов игр.

/// <summary>
/// Все игры
/// </summary>
private static List<Game> _games = new List<Game>();
public static List<Game> Games
{
    get
    {
        if (!Config.CacheEnabled)
            _games.Clear();


    if (_games.Count == 0)
    {
        //IoC может быть любой
        IRepository&lt;Game&gt; gamesRepository = QRResolver.Current.GetInstance&lt;IRepository&lt;Game&gt;&gt;();
        //Паттерн Repository и соответствующие классы из Oxozle.Utilities.Data
        _games = gamesRepository.Get(null, null, "Games_TX").ToList();
    }

    return _games;
}

Этим кодом мы получим (и закешируем) все объекты игр и все переводы. Заранее знаем, что игр не очень много. В случае, если объектов в базе данных достаточно много следует выключить кеширование или использовать класс Cache.

Конструктор модели GameModel выглядит примерно следующим образом:

public GameModel(Game game,  string languageCode)
{
	Games_TX translate = game.Games_TX.FirstOrDefault(x =&gt; x.LanguageId == languageCode);

	if (translate == null)
		translate = game.Games_TX.FirstOrDefault();

	Complexity = game.Complexity;
	Id = game.Id;
	BackgroundFile = game.BookBackgroundFileName;

	Name = translate.Name;
	Description = translate.Description;
	PlayersCount = translate.PlayersCount;
}

Таким образом можно локализовать 100% объектов в базе данных. При этом этот способ локализации позволяет добавить функцию локализации для объектов в базе. Для этого нужно написать небольшой скрипт автозаполнения _TX таблицы из основной. В общем случае для каждой бизнес модели потребуется своя структурированная таблица _TX, в которой будет хранится локализованная текстовая информация.

Ссылки

Комментарии

comments powered by Disqus