- PVSM.RU - https://www.pvsm.ru -

.Net Локализованные сообщения «относительного времени»

Всем привет,

На текущем проекте, от заказчика пришло требования, о том, что информация о прошедших событиях, должна показываться клиенту в виде «относительно времени» (Сообщение вида: «Событие произошло N минутуминутминуты назад»). Нам пришли шаблоны сообщений для разных вариантов, а также требования к внешнему виду, одним из которых было то, что сообщения должны быть локализованы.

«Что здесь сложного?», — подумали мы и отправились в гугл, на поиски готового решения. К сожалению, гугл изображал из себя партизана, предлагал несколько вариантов готовых решений для построения такого типа сообщений для английского языка, но ни одно из них не поддерживало локализации.

«Делать нечего, придется поддержать российский автопром изобретать свой велосипед», — решили мы. И вот, что из этого получилось…

Локализация в нашем приложении реализована через файлы ресурсов. Было принято решение, что вся необходима информация для локализации так же будет храниться в них. Главной проблемой стало определение правил по которым необходимо брать из ресурсов то или иное значение слова. В нашем случае это были слова «День», «Час», «Минута», с соответствующей числительной формой.

Поговорив с гуглом еще раз, мы нашли замечательный сайт [1], на котором описаны и стандартизированны правила построения единственных и множественных форм слов для большинства языков. Это стало отправной точкой для написания кода.

На сайте, приведенном выше, различные виды образования форм слов разбиты на категории, самые популярные из них:
— one
— few
— many
— other
Для каждой категории каждого языка приведены примеры, а так же правила, принадлежности к той или иной группе.

Для более легкого понимания, я приведу пример для Английского языка:

Language Name Code Category Examples Rules
English en one 1 one → n is 1;

other →
everything else

other 0, 2-999;

1.2, 2.07…

В двух словах расскажу, как мы решили это сделать:
Для каждого из языков, которые мы поддерживаем, создается класс, с методом просчета категории, которой принадлежит слово, по количеству объектов. В файл ресурсов этого языка добавляются возможные варианты категорий слов в виде {НазваниеОбъекта}.{Категория}. Создается класс-фарбрика, возвращающий реализацию вышеупомянутого класса в зависимости от языка, для которого необходимо просчитать форму слова. Создается класс-обертка, для локализации формы слова, один из методов которого, принимает в качестве параметра часть ключа ресурса объекта ({НазваниеОбъекта}) и количество этих объектов, и возвращает строку из соответствующего файла ресурсов, необходимой категории.

Ну что же, от слов к делу! Самое время попрограммировать…
Первым делом был создан enum, содержащий категории, которые необходимы нам для работы:

public enum PluralCategories	{
		One,
		Few,
		Many,
		Other
	}

Далее интерфейс для будущих классов просчитывающих категорию:

public interface ITimeFormatter
	{
		/// <summary>
		/// Returns category for plural form
		/// </summary>
		/// <param name="count"></param>
		/// <returns></returns>
		PluralCategories GetPluralCategoryByCount(int count);
	}

Класс-фабрика:

public interface ITimeFormatter
	/// <summary>
	/// Default fabric for TimeFormatters
	/// </summary>
	public class TimeFormatterFabric
	{
		/// <summary>
		/// Returns TimeFormatter for corresponding culture
		/// </summary>
		/// <param name="cultureInfo"></param>
		/// <returns></returns>
		public ITimeFormatter GetTimeFormatter(CultureInfo cultureInfo)
		{
			ITimeFormatter result;
			if (cultureInfo.Name.StartsWith("en", StringComparison.InvariantCultureIgnoreCase))
			{
				result = new EnglishTimeFormatter();
			} else if (cultureInfo.Name.StartsWith("ru", StringComparison.InvariantCultureIgnoreCase))
			{
				result = new RussianTimeFormatter();
			} else
			{
				result = new EnglishTimeFormatter();
			}
			return result;
		}
	}

Класс-обертка:

public class TimeFormatter
	{
		private readonly ILocalizationService _localizationService;
		private readonly ITimeFormatter _timeFormatter;

		/// <summary>
		/// Defaults constructor.
		/// </summary>
		/// <param name="localizationService"></param>
		public TimeFormatter(ILocalizationService localizationService)
		{
			_localizationService = localizationService;
			_timeFormatter = new TimeFormatterFabric().GetTimeFormatter(localizationService.CurrentUiCulture);
		}

		/// <summary>
		/// Returns localized string by key and count of items.
		/// </summary>
		/// <param name="rootResourceKey"></param>
		/// <param name="count"></param>
		/// <returns></returns>
		public string Localize(string rootResourceKey, int count)
		{
			var resourceKey = string.Format("{0}.{1}", rootResourceKey, _timeFormatter.GetPluralCategoryByCount(count).ToString());
			return _localizationService.Localize(resourceKey);
		}
	}

Класс просчета категории для Английского языка:

public class EnglishTimeFormatter : ITimeFormatter
	{
		/// <summary>
		/// Return plural category by count for English language rules
		/// </summary>
		/// <param name="count"></param>
		/// <returns></returns>
		public PluralCategories GetPluralCategoryByCount(int count)
		{
			PluralCategories result = PluralCategories.Other;
			if (count == 1)
			{
				result = PluralCategories.One;
			}
			return result;
		}
	}

Для Английского языка в файл ресурсов были добавлены следующие значения:

DateTime.Days.One day
DateTime.Days.Other days

DateTime.Hours.One hour
DateTime.Hours.Other hours

DateTime.Minutes.One minute
DateTime.Minutes.Other minutes

Ну и небольшой пример использования:

public static string ToTimeLeftString(this DateTime dateTime)	{
var result = new StringBuilder();
			var _localizationService = DependencyResolver.Current.GetService<ILocalizationService>();
			var timeFormatter = new TimeFormatter(_localizationService);
			var currentTime = DateTime.UtcNow;
			var timeLeft = currentTime - dateTime;
		       
result.AppendFormat("{0} {1} {2} {3} {4}", timeLeft.Days,
					timeFormatter.Localize("DateTime.Days", timeLeft.Days),
					_localizationService.Localize("Common.Ago"),
					_localizationService.Localize("Common.At"),					_localizationService.ConverUtcDateTimeToUsersTimeZone(dateTime).ToShortTimeString());

return result.ToString();
}

На этом все, спасибо за внимание, надеюсь, статья будет кому-то полезной.

Автор: K_O_T


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/net/3699

Ссылки в тексте:

[1] сайт: http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html