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.
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
{
CultureAndRegionInfoBuilder.Unregister(cultureName);
}
catch
{
}
CultureAndRegionInfoBuilder builderFa =
new CultureAndRegionInfoBuilder(cultureName,
CultureAndRegionModifiers.Replacement);
builderFa.GregorianDateTimeFormat = new DateTimeFormatInfo();
builderFa.GregorianDateTimeFormat.Calendar =
new GregorianCalendar(GregorianCalendarTypes.Localized);
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";
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)
{
byte[] optinalCalendarsByteSequens =
new byte[8] { 0xc, 0, 0x2, 0, 0x1, 0, 0x6, 0 };
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:
- Create an NLP file on your computer.
- In the setup project, copy this file to the %WinDir%\Globalization folder on the target.
- 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.