Не так давно я столкнулся с проблемой отображения количество некоторых объектов в единственном и множественном числе (”Нет комментариев” или “меньше минуты назад”, “1 комментарий” или “минуту назад”, “2 комментария” или “2 минуты назад”). Это достаточно просто в английском (только три возможных варианта), но я работаю над приложением, которое должно быть локализовано для нескольких культур. Например, в русском у нас есть как минимум 4 формы (”Нет комментариев”, “1 комментарий”, “2 комментария”, “5 комментариев”) и не столь очевидные правила для множественных чисел (”11 комментариев”, “111 комментариев”, но “21 комментарий”). Я не знаю других языков, но подозреваю, что некоторые из них могут быть более сложные, чем русский. Здесь вы найдете мои мысли о такой локализации строк.
ASP.NET (и, насколько я знаю, Java, Ruby и т.д.) не содержат встроенной функциональности для локализации строк с числами. Да, я знаю о методе Ruby pluralize, но он не работает для русского, и, я думаю, для некоторых других языков тоже. Итак, мне нужно разработать механизм определения формы, желательно используя ресурсы ASP.NET, с наиболее простым возможным интерфейсом. Добавление новых языков должно быть тоже достаточно простым.
Итак, вот моя мысль. Мы определим несколько строк в наших ресурсах (Comments0, Comments1, Comments2 и т.д.). У нас есть простой интерфейс, скажем IResourceIndexer. Интерфейс включает единственный метод, который должен возвращать индекс ресурса по количеству объектов. Нам нужно реализовать интерфейс для различных языков (английского, русского и т.д.). Затем нам нужно создать фабрику, которая будет возвращать специфичный для культуры IResourceIndexer. И последний шаг — создать статический класс со вспомогательными методами, которые будут возвращать строки, используя количество объектов. Но лучше один раз увидеть, чем сто раз услышать. Посмотрим на код.
Вот интерфейс. Как вы видите, он довольно прост — всего лишь один метод, возвращающий индекс ресурса.
{
public interface IResourceIndexer
{
int GetResourceIndex(long count);
}
}
Давайте реализуем индексаторы для английского и русского языков:
{
public class EnglishResourceIndexer : IResourceIndexer
{
public int GetResourceIndex(long count)
{
if (count == 0) return 0;
if (count == 1) return 1;
return 2;
}
}
}
{
public class RussianResourceIndexer : IResourceIndexer
{
public int GetResourceIndex(long count)
{
if (count == 0) return 0;
if (count == 1) return 1;
int twoDigits = (int) count % 100;
if (twoDigits > 10 && twoDigits < 20) return 3;
int lastDigit = (int) (count % 10);
if (lastDigit == 1) return 4;
if (lastDigit > 1 && lastDigit < 5) return 2;
return 3;
}
}
}
Как вы могли заметить, русский немного сложнее английского
Но он все равно достаточно просто для реализации. Надеюсь, для других языков мы сможем сделать что-то вроде этого без проблем. Обратите внимание, я добавил разные индексы ресурсов для 1 и 21 в русском. Обычно, этого не требуется, но я хочу иметь возможность использовать строки “минуту назад” для 1 и “21 минуту назад” для 21.
Теперь нам нужно создать фабрику, чтобы получать индексатор по текущей культуре (которая может быть определена из свойства Thread.CurrentThread.CurrentUICulture).
using System.Globalization;
using System.Threading;
namespace App_Code
{
public static class NumericResourceFactory
{
static NumericResourceFactory()
{
_resourceIndexerCache = new Dictionary<string, IResourceIndexer>();
}
public static IResourceIndexer GetResourceIndexer()
{
CultureInfo culture = Thread.CurrentThread.CurrentUICulture;
return GetResourceIndexer(culture);
}
private static IResourceIndexer GetResourceIndexer(CultureInfo culture)
{
string id = culture.TwoLetterISOLanguageName;
if (!_resourceIndexerCache.ContainsKey(id))
{
switch(id)
{
case "ru":
_resourceIndexerCache[id] = new RussianResourceIndexer();
break;
default:
_resourceIndexerCache[id] = new EnglishResourceIndexer();
break;
}
}
return _resourceIndexerCache[id];
}
private static Dictionary<string, IResourceIndexer> _resourceIndexerCache;
}
}
Как вы, наверное, заметили, я определяю индексатор по двухбуквенному коду ISO. Конечно, вы можете производить более сложную обработку. Обратите внимание, что английский - язык по умолчанию в моем приложении, вам, возможно, придется изменить порядок условий. Я использую кэш индексаторов, чтобы избежать потери производительности в фабрике.
Почти закончили. Теперь нам нужно определить ресурсы и реализовать вспомогательный класс:
{
public static class ResourceStrings
{
public static string GetCommentsString(int comments)
{
IResourceIndexer indexer = NumericResourceFactory.GetResourceIndexer();
string format = GetResourceString("Comments" + indexer.GetResourceIndex(comments));
return String.Format(format, comments);
}
private static string GetResourceString(string id)
{
return Resources.NumericResources.ResourceManager.GetString(id,
Resources.NumericResources.Culture);
}
}
}
И ресурсы:
<root>
<data name="Comments0" xml:space="preserve">
<value>No comments</value>
<comment>0 comments</comment>
</data>
<data name="Comments1" xml:space="preserve">
<value>1 comment</value>
<comment>1 comment</comment>
</data>
<data name="Comments2" xml:space="preserve">
<value>{0} comments</value>
<comment>2 comments (and more)</comment>
</data>
<data name="Comments3" xml:space="preserve">
<value />
<comment>not used in English</comment>
</data>
<data name="Comments4" xml:space="preserve">
<value />
<comment>not used in English</comment>
</data>
</root>
<root>
<data name="Comments0" xml:space="preserve">
<value>Нет комментариев</value>
<comment>0 комментариев</comment>
</data>
<data name="Comments1" xml:space="preserve">
<value>{0} комментарий</value>
<comment>1 комментарий</comment>
</data>
<data name="Comments2" xml:space="preserve">
<value>{0} комментария</value>
<comment>2-4 комментария</comment>
</data>
<data name="Comments3" xml:space="preserve">
<value>{0} комментариев</value>
<comment>5-9 комментариев</comment>
</data>
<data name="Comments4" xml:space="preserve">
<value>{0} комментарий</value>
<comment>21 комментарий</comment>
</data>
</root>
Обратите внимание, у меня есть 5 строк в ресурсах для английского, несмотря на то, что только 3 реально используются. Это потому, что английский — язык по умолчанию, и для построения сателлитной сборки с русскими ресурсами необходимо, чтобы одинаковые ресурсы были в обоих файлах.
Немного об использовании. Для начала, вам необходимо проинициализировать культуру потока. В приложениях ASP.NET нужно переопределить метод InitializeCulture и установить свойства Thread.CurrentThread.CurrentCulture и Thread.CurrentThread.CurrentUICulture:
{
CultureInfo culture;
if (Request.UserLanguages != null && Request.UserLanguages.Length > 0)
culture = CultureInfo.CreateSpecificCulture(Request.UserLanguages[0]);
else
culture = CultureInfo.CreateSpecificCulture("");
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
base.InitializeCulture();
}
В этом случае будет использоваться культура браузера (в Firefox — Tools/Options/Advanced/Languages -> Choose, добавьте первым русский или английский; в Internet Explorer это можно сделать тут — Tools/Internet Options/Languages). Теперь вы можете написать что-то вроде этого в коде страницы .aspx:
<asp:Label runat="server">
<%= ResourceStrings.GetCommentsString(20) %>
</asp:Label>
Пример проекта может быть загружен здесь. Есть комментарии?
Русский
English
Спасибо за отличный пример!
Кстати, код не компилируется:
Compilation Error
Description: An error occurred during the compilation of a resource required to service this request. Please review the following specific error details and modify your source code appropriately.
Compiler Error Message: CS0246: The type or namespace name ‘App_Code’ could not be found (are you missing a using directive or an assembly reference?)
2Mihail: Странно, но у меня код компилируется нормально
В Visual Studio 2005 нужно открыть проект как File/Open/Web Site…
есть плагин для Ruby On Rails - Globalize. Там эта проблема решина так. В таблице с языками у каждого языка есть так называемая pluralization-строка. Например для русского:
, для английского:
, для ирландского:
и т.д. Затем переводы находятся по ключу и количеству, где количество это на самом деле не количество, а число которое возвращает pluralization-строка при выполнении, если c это количество объектов.
Использовать в коде это очень просто, напр.
%d подмениться на число, а вместо COMMENT подставиться слово “комментарий” в нужной форме.
Про плюрализацию моя любимая тема.
В доке к GNU gettext изложен большой набор разных вариантов в разных языках (и то, как они это обрабатывают):
Additional functions for plural forms