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

Aug 29
2007 08:33 (ASP.NET, Программирование) · English (4,631 views)

Не так давно я столкнулся с проблемой отображения количество некоторых объектов в единственном и множественном числе (”Нет комментариев” или “меньше минуты назад”, “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>

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

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

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

1
Mihail
сказал 29.08.2007 в 10.24

Спасибо за отличный пример!

2
Mihail
сказал 29.08.2007 в 10.37

Кстати, код не компилируется:

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?)

3
сказал 29.08.2007 в 12.24

2Mihail: Странно, но у меня код компилируется нормально :-) В Visual Studio 2005 нужно открыть проект как File/Open/Web Site…

4
pechkinator
сказал 07.09.2007 в 17.15

есть плагин для Ruby On Rails - Globalize. Там эта проблема решина так. В таблице с языками у каждого языка есть так называемая pluralization-строка. Например для русского:

c%10==1 && c%100!=11 ? 1 : c%10>=2 && c%10<=4 && (c%100<10 || c%100>=20) ? 2 : 3

, для английского:

c == 1 ? 1 : 2

, для ирландского:

c==1 ? 1 : c==2 ? 2 : 3

и т.д. Затем переводы находятся по ключу и количеству, где количество это на самом деле не количество, а число которое возвращает pluralization-строка при выполнении, если c это количество объектов.

Использовать в коде это очень просто, напр.

"%d COMMENT".t(@comments.count)

%d подмениться на число, а вместо COMMENT подставиться слово “комментарий” в нужной форме.

5
сказал 26.01.2008 в 16.06

Про плюрализацию моя любимая тема.

В доке к GNU gettext изложен большой набор разных вариантов в разных языках (и то, как они это обрабатывают):

Additional functions for plural forms

Оставить отзыв

Вы можете использовать простые теги форматирования HTML (вроде <a>, <ul> and others). Чтобы вставить пример код, используйте <code lang="php">$a = "hello";</code> (поддерживаемые языки: ruby, php, yaml, html, csharp, javascript). Также Вы можете использовать <code>$a = "hello";</code>, синтаксис не будет подсвечен. Если вы не хотите использовать тег <code>, замените символ < на &lt;.

Отправить

 
Copyright © 2005 - 2008, Dmytro Shteflyuk