Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Using Custom Culture Approach to Fix Persian Culture for Persian Calendar

0.00/5 (No votes)
17 Sep 2011 3  
Describes how to create a custom Persian culture with Persian Calendar as one of it's optional calendars.

Introduction

This article describes how to create a new custom Persian culture that fixes the standard Persian culture issues with the Persian Calendar and month names. This is actually a documentation and description of what Reza Taroosheh has done in How to set PersianCalendar to CultureInfo.

Background

Issues with the .NET CultureInfo class in the case of Persian (fa-IR) culture have been discussed before, for example, see:

As it could be seen in the above references, the main issue is how to set the PersianCalendar as the default calendar of Persian Culture. Reza Taroosheh, in his article How to set PersianCalendar to CultureInfo proposed a workaround of creating a custom culture to fix these issues; he also provided the corrected custom culture file and an MSI to setup this file, but he does not describe the procedure of how to create such a culture. This article describes how to do so and also provides code that actually does what Taroosheh has previously done.

Using the Code

Using the code is straightforward, we can call the static method CreateAndRegisterPersianCulture to create and register a new custom Persian culture with the Persian calendar as the default calendar. The custom culture also fixes the Persian month names issue. Please note that this process should be done only once on each machine; once done, programs can instantiate a new fixed Persian culture simply by calling the standard method CultureInfo persian = new CultureInfo("fa-IR"). For additional information regarding custom cultures, please refer to the Custom Culture article on CodeProject.

// On each target machine you should create and register the custom culture by:
//...
if (CreateAndRegisterCustomPersianLocale())
    Log("Custom culture for 'fa-IR' successfuly created and registered.\r\" + 
      "nNow you can use new CultureInfo('fa-IR', false) " + 
      "to instantiate a new fixed Persian Cuture.\r\n");
else
    Log("Failed to create and register custom Persian culture.\r\n");

After this, you may simply use the custom culture by:

CultureInfo testCulture = new CultureInfo("fa-IR",false);
testCulture.DateTimeFormat.ShortDatePattern ="yyyy/MM/dd");
testCulture.DateTimeFormat.LongDatePattern = "dddd dd MM yyyy";

Please see below for why you should set short and long date patterns.

The demo download contains a console application to create and register the custom Persian culture. One may use it as:

CustomPersianCultureBuilder [register|test|unregister] [quiet]

This can register, test, or unregister the custom Persian culture. The quiet flag is useful while using it as part of a setup procedure.

Detailed Information

Following the instruction in Custom Culture, one may readily setup a new culture for "fa-IR". This can be a replacement culture so that existing codes will use this without modifications. A pseudo-code for such an operation can be:

string cultureName = "fa-IR";
// Try to unregister if previously registered.
try
{
    CultureAndRegionInfoBuilder.Unregister(cultureName);
}
catch
{
}
CultureAndRegionInfoBuilder builderFa =
      new CultureAndRegionInfoBuilder(cultureName,
      CultureAndRegionModifiers.Replacement);
builderFa.GregorianDateTimeFormat = new DateTimeFormatInfo();
// FixPersianDateTimeFormatMonthNames(null);
builderFa.GregorianDateTimeFormat.Calendar = 
   new GregorianCalendar(GregorianCalendarTypes.Localized);
// Fix month naemes.
//builderFa.GregorianDateTimeFormat =
//   FixPersianDateTimeFormatMonthNames(builderFa.GregorianDateTimeFormat);
builderFa.GregorianDateTimeFormat.AbbreviatedDayNames = 
          new string[] { "ی", "د", 
          "س", "چ", "پ", "ج", "ش" };
builderFa.GregorianDateTimeFormat.ShortestDayNames = 
          new string[] { "ی", "د", "س", 
          "چ", "پ", "ج", "ش" };
builderFa.GregorianDateTimeFormat.DayNames = 
          new string[] { "یکشنبه", "دوشنبه", 
          "ﺳﻪشنبه", "چهارشنبه", "پنجشنبه", 
          "جمعه", "شنبه" };
builderFa.GregorianDateTimeFormat.AbbreviatedMonthNames = 
          new string[] { "فروردین", "ارديبهشت", 
          "خرداد", "تير", "مرداد", 
          "شهریور", "مهر", 
          "آبان", "آذر", "دی", 
          "بهمن", "اسفند", "" };
builderFa.GregorianDateTimeFormat.MonthNames = 
          new string[] { "فروردین", "ارديبهشت", 
          "خرداد", "تير", "مرداد", 
          "شهریور", "مهر", 
          "آبان", "آذر", "دی", 
          "بهمن", "اسفند", "" };
builderFa.GregorianDateTimeFormat.AMDesignator = "ق.ظ";
builderFa.GregorianDateTimeFormat.PMDesignator = "ب.ظ";
builderFa.GregorianDateTimeFormat.FirstDayOfWeek = DayOfWeek.Saturday;
builderFa.GregorianDateTimeFormat.FullDateTimePattern = "yyyy MMMM dddd, dd HH:mm:ss";
builderFa.GregorianDateTimeFormat.LongDatePattern = "yyyy MMMM dddd, dd";
builderFa.GregorianDateTimeFormat.ShortDatePattern = "yyyy/MM/dd";

// Register the custom culture.
builderFa.Register();

The above code creates a custom Persian culture with Persian month and week day names. But still the issue of default calendar remains untouched. As you will notice, we are actually setting GregorianDateTimeFormat properties, which seems wrong. Later on in this article, I'll talk about this issue.

Default Persian Calendar

One may suggest that the Persian calendar can be set by the AvailabelCaledars property of the CultureAndRegionInfoBuilder class. But if you call this method in code like this:

builder.AvaialbleCalendars = new Calendar[] {new PersianCalendar(),new HijriCalendar()};

An exception of "Custom Calendars are not Supported." is raised. This is because internally CultureAndRegionInfoBuilder uses the System.Globalization.CultureDefinition.CalendarIdOfCalendar method to validate calendars. This method ignores PersianCalendar as a valid calendar:

internal static CalendarId CalendarIdOfCalendar(Calendar calendar)
{
    if (calendar is GregorianCalendar)
    {
        switch (((GregorianCalendar) calendar).CalendarType)
        {
            case GregorianCalendarTypes.Localized:
                return CalendarId.GREGORIAN;

            case GregorianCalendarTypes.USEnglish:
                return CalendarId.GREGORIAN_US;

            case GregorianCalendarTypes.MiddleEastFrench:
                return CalendarId.GREGORIAN_ME_FRENCH;

            case GregorianCalendarTypes.Arabic:
                return CalendarId.GREGORIAN_ARABIC;

            case GregorianCalendarTypes.TransliteratedEnglish:
                return CalendarId.GREGORIAN_XLIT_ENGLISH;

            case GregorianCalendarTypes.TransliteratedFrench:
                return CalendarId.GREGORIAN_XLIT_FRENCH;
        }
    }
    else
    {
        if (calendar is HebrewCalendar)
        {
            return CalendarId.HEBREW;
        }
        if (calendar is ThaiBuddhistCalendar)
        {
            return CalendarId.THAI;
        }
        if (calendar is TaiwanCalendar)
        {
            return CalendarId.TAIWAN;
        }
        if (calendar is JapaneseCalendar)
        {
            return CalendarId.JAPAN;
        }
        if (calendar is KoreanCalendar)
        {
            return CalendarId.KOREA;
        }
        if (calendar is HijriCalendar)
        {
            return CalendarId.HIJRI;
        }
        if (calendar is UmAlQuraCalendar)
        {
            return CalendarId.UMALQURA;
        }
    }
    throw new NotSupportedException(
      CultureAndRegionInfoBuilder.GetResourceString("CustomCaledarsNotSupported"));
}

One may still insist on using AvailableCalendars, with a Reflection approach, to directly set the property, avoiding the validation process in the property set method. Such an approach has been successfully tested in previous works to set the Calendar property of DateTimeFormatInfo. But here it fails because although the AvailableCalendars is set without problems, later the Register method will fail.

Reza Taroosheh has already solved this issue (in fact, you can obtain a setup for fixed Persian culture from here). I figured out that he's done that by manually fixing the NLP file. One may assume that somewhere in the NLP file there's a byte sequence corresponding to the optional calendars array. Looking at IDs of optional calendars of a standard Persian culture, the sequence should be 0x0C, 0x2, 0x1, 0x6. If we seek this sequence and replace the first byte (ID of GREGORIAN_XLIT_ENGLISH) with 0x16 (for Persian calendar), in effect the first optional calendar which is also the default calendar will be changed to Persian calendar. This can be done like this:

private static void FixNLPForOptionalCalendars(string NLPFileName)
{
    // This is the byte stream of optional caledars in .NLP file,
    // we will search this byte sequence and then correct the first element to 0x16
    // this will set PersianCalendar as the default calendar for the culture.
    byte[] optinalCalendarsByteSequens = 
           new byte[8] { 0xc, 0, 0x2, 0, 0x1, 0, 0x6, 0 };

    // Fix NLP file name if it does not contain path ans extension.
    if (!NLPFileName.Contains("\\"))
        NLPFileName = Environment.ExpandEnvironmentVariables("%WinDir%") + 
                      @"\globalization\" + NLPFileName;
    if (!NLPFileName.ToLower().EndsWith(".nlp"))
        NLPFileName = NLPFileName + ".nlp";
    System.IO.FileStream strm = new System.IO.FileStream(NLPFileName, 
              System.IO.FileMode.Open, System.IO.FileAccess.ReadWrite);
    while (strm.Position < strm.Length)
    {
        byte b = (byte)strm.ReadByte();
        if (b == optinalCalendarsByteSequens[0])
        {
            strm.Position = strm.Position - 1;
            if (strm.Position + optinalCalendarsByteSequens.Length < strm.Length)
            {
                byte[] readBuff = new byte[optinalCalendarsByteSequens.Length];
                strm.Read(readBuff, 0, readBuff.Length);
                if (ArrayEqual(readBuff, optinalCalendarsByteSequens))
                {
                    strm.Position = strm.Position - readBuff.Length;
                    break;
                }
                else
                    strm.Position = strm.Position - readBuff.Length + 1;
            }
        }
    }// while
    if (strm.Position < strm.Length)
    {
        // If found set the first entry to 0x16
        // which is the ID for Persian Calendar.
        strm.WriteByte(0x16);
        strm.Flush();
    }
    strm.Close();
}

This will open the NLP file, look for the optional calendars byte sequence, and fixes the first entry.

Problem with Date Patterns

When you use the new custom Persian culture, you will soon notice that the short and long date patterns are blank and you should manually set them upon creation of each new CultureInfo object based on the custom culture, by something like culture.DateTimeFormat.ShortDatePattern="yyyy/MM/dd". Also, you may have noted that we have set the GregorianDateTimeFormat property of our CultureAndRegionInfoBuilder. That's true, we can only set the attributes of Gregorian calendar using the custom culture of .NET. And we are actually overriding the wrong attributes because we are setting the month names in the localized Gregorian calendar. That is because:

Although .NET supports multiple calendars in a single culture, there's no way to specify attributes of these calendars other than the Gregorian calendar.

But somehow things go right by chance. When month names are requested from a DateTimeFormatInfo object, CalendarTable will return null for Calendar.ID= 0x16 (PersianCalendar). But the clever programmer uses the default calendar month names in this.m_cultureTableRecord.SMONTHNAMES:

private string[] GetMonthNames()
{
    if (this.monthNames == null)
    {
        string[] sMONTHNAMES = null;
        if (!this.m_isDefaultCalendar)
        {
            sMONTHNAMES = CalendarTable.Default.SMONTHNAMES(this.Calendar.ID);
        }
        if (((sMONTHNAMES == null) || (sMONTHNAMES.Length == 0)) || 
             (sMONTHNAMES[0].Length == 0))
        {
            sMONTHNAMES = this.m_cultureTableRecord.SMONTHNAMES;
        }
        Thread.MemoryBarrier();
        this.monthNames = sMONTHNAMES;
    }
    return this.monthNames;
}

This behavior helps our custom culture return the correct month names. But unfortunately somewhere else in InitializeOverridableProperties of the DateTimeFormatInfo class, the three properties yearMonthPattern, shortDatePattern, and longDatePattern are set from the calendar, which happens to be empty.

private void InitializeOverridableProperties()
{
    if (this.amDesignator == null)
    {
        this.amDesignator = this.m_cultureTableRecord.S1159;
    }
    if (this.pmDesignator == null)
    {
        this.pmDesignator = this.m_cultureTableRecord.S2359;
    }
    if (this.longTimePattern == null)
    {
        this.longTimePattern = this.m_cultureTableRecord.STIMEFORMAT;
    }
    if (this.firstDayOfWeek == -1)
    {
        this.firstDayOfWeek = this.m_cultureTableRecord.IFIRSTDAYOFWEEK;
    }
    if (this.calendarWeekRule == -1)
    {
        this.calendarWeekRule = this.m_cultureTableRecord.IFIRSTWEEKOFYEAR;
    }
    if (this.yearMonthPattern == null)
    {
        this.yearMonthPattern = this.GetYearMonthPattern(this.calendar.ID);
    }
    if (this.shortDatePattern == null)
    {
        this.shortDatePattern = this.GetShortDatePattern(this.calendar.ID);
    }
    if (this.longDatePattern == null)
    {
        this.longDatePattern = this.GetLongDatePattern(this.calendar.ID);
    }
}

Since InitializeOverridableProperties is called whenever you set the Calendar property of a DateTimeFormatInfo, you should set the date pattern properties whenever you change the Calendar property and also when a new culture is first instantiated.

Setup Program

One may wish to install the custom Persian locale as part of his/her setup program of his/her own application. One approach may be that you can find it in the source code. In this approach, the CustomPersianCulture console application has been used as a setup custom action with appropriate arguments (see the CustomPersianCutureSetup project in the source code). This approach works well except for the fact that as already noted here, the Unregister method of CultureAndRegionInfoBuilder cannot delete the NLP file. It actually renames it to something like "fa-IR.tmp0". Therefore uninstalling the setup will leave a temp file in the destination %WinDir%\Globalization folder. An alternative method would be installing the custom locale by:

  1. Create an NLP file on your computer.
  2. In the setup project, copy this file to the %WinDir%\Globalization folder on the target.
  3. Add the string value "fa-IR" to the Registry key HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Nls\CustomLocale.

Summary

A workaround for the PersianCalendar issue in Persian culture was presented. This is actually a documentation of what Reza Taroosheh has already done in How to set PersianCalendar to CultureInfo. This is also an alternative approach of what I've previously noted in: Fixing Optional Calendars for Persian Culture in .NET. This approach seems much better as it does not require Reflection which causes code access issues, an example of which can be seen here.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here