Определение множественной формы числа в зависимости от культуры в .NET

Автор: Дмитрий Штефлюк. Источник: http://kpumuk.info/

Не так давно я столкнулся с проблемой отображения количество некоторых объектов в единственном и множественном числе (”Нет комментариев” или “меньше минуты назад”, “1 комментарий” или “минуту назад”, “2 комментария” или “2 минуты назад”). Это достаточно просто в английском (только три возможных варианта), но я работаю над приложением, которое должно быть локализовано для нескольких культур. Например, в русском у нас есть как минимум 4 формы (”Нет комментариев”, “1 комментарий”, “2 комментария”, “5 комментариев”) и не столь очевидные правила для множественных чисел (”11 комментариев”, “111 комментариев”, но “21 комментарий”). Я не знаю других языков, но подозреваю, что некоторые из них могут быть более сложные, чем русский. Здесь вы найдете мои мысли о такой локализации строк.

ASP.NET (и, насколько я знаю, Java, Ruby и т.д.) не содержат встроенной функциональности для локализации строк с числами. Да, я знаю о методе Ruby pluralize, но он не работает для русского, и, я думаю, для некоторых других языков тоже. Итак, мне нужно разработать механизм определения формы, желательно используя ресурсы ASP.NET, с наиболее простым возможным интерфейсом. Добавление новых языков должно быть тоже достаточно простым.

Итак, вот моя мысль. Мы определим несколько строк в наших ресурсах (Comments0, Comments1, Comments2 и т.д.). У нас есть простой интерфейс, скажем IResourceIndexer. Интерфейс включает единственный метод, который должен возвращать индекс ресурса по количеству объектов. Нам нужно реализовать интерфейс для различных языков (английского, русского и т.д.). Затем нам нужно создать фабрику, которая будет возвращать специфичный для культуры IResourceIndexer. И последний шаг — создать статический класс со вспомогательными методами, которые будут возвращать строки, используя количество объектов. Но лучше один раз увидеть, чем сто раз услышать. Посмотрим на код.

Вот интерфейс. Как вы видите, он довольно прост — всего лишь один метод, возвращающий индекс ресурса..

namespace App_Code
{
    public interface IResourceIndexer
    {
        int GetResourceIndex(long count);
    }
}

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

namespace App_Code
{
    public class EnglishResourceIndexer : IResourceIndexer
    {
        public int GetResourceIndex(long count)
        {
            if (count == 0) return 0;
            if (count == 1) return 1;
            return 2;
        }
    }
}
namespace App_Code
{
    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.Collections.Generic;
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. Конечно, вы можете производить более сложную обработку. Обратите внимание, что английский - язык по умолчанию в моем приложении, вам, возможно, придется изменить порядок условий. Я использую кэш индексаторов, чтобы избежать потери производительности в фабрике.

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

namespace App_Code
{
    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);
        }
    }
}

И ресурсы:

<?xml version="1.0" encoding="utf-8"?>
<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>
<?xml version="1.0" encoding="utf-8"?>
<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:

protected override void InitializeCulture()
{
    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:

<%@ Import namespace="App_Code" %>
<asp:Label runat="server">
    <%= ResourceStrings.GetCommentsString(20) %>
</asp:Label>

Пример проекта может быть загружен здесь. Есть комментарии?



Опубликовал admin
21 Сен, Пятница 2007г.



Программирование для чайников.