culture | Dmytro Shteflyuk's Home https://kpumuk.info In my blog I'll try to describe about interesting technologies, my discovery in IT and some useful things about programming. Mon, 07 Sep 2015 23:58:05 +0000 en-US hourly 1 https://wordpress.org/?v=6.4.3 Culture-specific strings pluralization in .NET https://kpumuk.info/asp-net/culture-specific-strings-pluralization-in-net/ https://kpumuk.info/asp-net/culture-specific-strings-pluralization-in-net/#comments Wed, 29 Aug 2007 06:33:01 +0000 http://kpumuk.info/asp-net/culture-specific-strings-pluralization-in-net/ Not so long ago I faced a problem of displaying count of some entities in singular or plural form (“No comments” or “less than a minute ago”, “1 comment” or “a minute ago”, “2 comments” or “2 minutes ago”). It’s very easy for English (there are three variants only), but I’m working on application that […]

The post Culture-specific strings pluralization in .NET first appeared on Dmytro Shteflyuk's Home.]]>
Not so long ago I faced a problem of displaying count of some entities in singular or plural form (“No comments” or “less than a minute ago”, “1 comment” or “a minute ago”, “2 comments” or “2 minutes ago”). It’s very easy for English (there are three variants only), but I’m working on application that should be localized for several cultures. For example, in Russian we have at least 4 forms (“Нет комментариев”, “1 комментарий”, “2 комментария”, “5 комментариев”) and not so obvious rules for plural forms (“11 комментариев”, “111 комментариев”, but “21 комментарий”). I don’t speak any other language, but I think that several of them might be even more complicated than Russian. Here you can find my thoughts about such strings localization.

ASP.NET (and as I know, Java, Ruby, etc) does not have built-in functionality for numeric strings localization. Yes, I know about Rubish pluralize, but it does not working for Russian, and I suspect for some other languages too. So I need to implement pluralization, desirable using ASP.NET resources, with simplest interface as possible. Adding new languages should be easy enough.

So, here is my idea. We will define several strings in our resources (Comments0, Comments1, Comments2, etc). We have some interface, for example IResourceIndexer. Interface has single method, which should return resource index by entities number. We have to implement this interface for different languages (English, Russian, etc). Then we need to create factory, which would return culture-specific IResourceIndexer. And latest step is to create static class with helper methods, which will return strings using number of entities. But seeing is believing. Let’s look at the code.

Here is interface. As you can see, it’s very simple — just single method, which returns resource index.

1
2
3
4
5
6
7
namespace App_Code
{
    public interface IResourceIndexer
    {
        int GetResourceIndex(long count);
    }
}

[lang_en]
Let’s implement indexers for English and Russian languages:
[/lang_en]
[lang_ru]
Давайте реализуем индексаторы для английского и русского языков:
[/lang_ru]

1
2
3
4
5
6
7
8
9
10
11
12
namespace App_Code
{
    public class EnglishResourceIndexer : IResourceIndexer
    {
        public int GetResourceIndex(long count)
        {
            if (count == 0) return 0;
            if (count == 1) return 1;
            return 2;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
        }
    }
}

As you can see, Russian is more difficult than English :-) But it still easy enough to implement. Hope, for other languages we could do something like this without any problems. Please note, that I have added different resource indexes for 1 and 21 in Russian. Usually this is not needed, but I want to have ability to use string “минуту назад” for 1 and “21 минуту назад” for 21.

Now we need to define factory to get indexer using current culture (which could be accessed through Thread.CurrentThread.CurrentUICulture property).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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;
    }
}

As you can see, I’m detecting indexer using two letters ISO code. Of course, you can do more complex processing here. Please note, that English is default language in my application, you might need to change cases order. I’m using indexers cache in this code to avoid factory performance hit.

Almost finished. Now we need to define resources and implement helper class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
        }
    }
}

And resources:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?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>

Please note, that I have 5 resource strings in English strings file, in spite of only 3 really need. That’s because English is my default language, and to build my satellite assembly with Russian resources, I must have same string resources in both resource files.

Little more about usage. First you should initialize thread culture. In ASP.NET applications you should override method InitializeCulture and set Thread.CurrentThread.CurrentCulture and Thread.CurrentThread.CurrentUICulture properties:

1
2
3
4
5
6
7
8
9
10
11
12
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();
}

In this case browser culture would be used (in Firefox — Tools/Options/Advanced/Languages -> Choose, add Russian or English as first language; in Internet Explorer it could be done here — Tools/Internet Options/Languages.) Now you can write something like this in .aspx file:

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

Full example project could be downloaded here. Have any comments?

The post Culture-specific strings pluralization in .NET first appeared on Dmytro Shteflyuk's Home.]]>
https://kpumuk.info/asp-net/culture-specific-strings-pluralization-in-net/feed/ 5