ZetCode

C# CultureInfo

last modified October 18, 2026

In this article we show how to globalize applications in C# with CultureInfo.

Globalization is the process of designing the application in such a way that it can be used by users from across the globe (for multiple cultures). Localization is the process of customizing the application to a specific culture: translating the texts, and showing numbers, dates, and currencies the way the users expect.

Note: These terms may have different meanings in other programming languages or platforms.

Types of cultures

A neutral culture is a culture that is associated with a language but not with a country or region. A specific culture is a culture that is associated with both a language and a country or region. For example, fr is the name for the neutral French culture, while fr-FR is the name for the French culture in France. Formatting numbers, currencies, and dates needs a specific culture, since they differ even among countries with the same language (en-US and en-GB write dates in a different order).

The invariant culture is culture-insensitive. It is associated with the English language (for historical reasons) but not with any country or region. It has the empty string as its name. CultureInfo.InvariantCulture retrieves it. The invariant culture is used for storing and exchanging data in a way that is not tied to any language, and does not change with the machine's settings or the version of the operating system.

C# CultureInfo

CultureInfo provides information about a specific culture. The information includes the names of the culture, the writing system, the calendar used, the sort order of strings, and formatting for dates and numbers.

using System.Globalization;

The CultureInfo is part of the System.Globalization namespace.

Two cultures are associated with each thread:

By default, both come from the settings of the operating system (on Linux and macOS, from the LANG and LC_* environment variables). A server application must not rely on them, as the settings of the server are probably not the culture of the user. In ASP.NET Core, the localization middleware sets the culture of each request.

Note: the .NET runtime uses the ICU library for globalization data on Linux and macOS (and on current Windows versions). Some results, such as the default number of decimal places of the N format or the names of months, can differ between platforms and library versions. The examples were run on Linux with ICU. A project can switch the globalization off with <InvariantGlobalization>true</InvariantGlobalization>, for instance in small containers; then all cultures behave like the invariant culture, and creating a culture other than the invariant one fails or returns data of the invariant culture.

C# current culture

The CultureInfo.CurrentCulture property gets or sets the culture of the current thread. CultureInfo.DefaultThreadCurrentCulture gets or sets the default culture for the threads of the application that have not set their own.

Program.cs
using System.Globalization;

double val = 1235.56;

Console.WriteLine($"Current culture: {CultureInfo.CurrentCulture.Name}");
Console.WriteLine($"Current UI culture: {CultureInfo.CurrentUICulture.Name}");
Console.WriteLine(val);

// the default for all threads that have not set their own culture
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("fr-FR");
Console.WriteLine($"After the default was set: {CultureInfo.CurrentCulture.Name}, {val}");

var t = new Thread(() => Console.WriteLine($"thread: {CultureInfo.CurrentCulture.Name}"));
t.Start();
t.Join();

// the culture of the current thread
CultureInfo.CurrentCulture = new CultureInfo("de-DE");
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("ja-JP");

Console.WriteLine($"Current culture: {CultureInfo.CurrentCulture.Name}, {val}");
Console.WriteLine(val.ToString(CultureInfo.InvariantCulture));

// the explicitly set culture flows to the threads and tasks we start
var t2 = new Thread(() => Console.WriteLine($"thread 2: {CultureInfo.CurrentCulture.Name}"));
t2.Start();
t2.Join();

await Task.Run(() => Console.WriteLine($"task: {CultureInfo.CurrentCulture.Name}"));

// unless the execution context does not flow
var t3 = new Thread(() => Console.WriteLine($"thread 3: {CultureInfo.CurrentCulture.Name}"));
t3.UnsafeStart();
t3.Join();

The example prints a value in several cultures and shows how the culture is passed to other threads.

CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("fr-FR");

We set the default culture. The main thread has not set its own culture yet, so it uses the default, and so does the new thread.

CultureInfo.CurrentCulture = new CultureInfo("de-DE");
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("ja-JP");

Now the main thread has an explicit culture, de-DE. We change the default as well. The explicit culture of a thread wins over the default, and it flows with the execution context to the threads, tasks, and asynchronous continuations that the thread starts. The default is used only by threads that have no culture of their own, as the one started with UnsafeStart, which does not flow the context.

$ dotnet run
Current culture: en-US
Current UI culture: en-US
1235.56
After the default was set: fr-FR, 1235,56
thread: fr-FR
Current culture: de-DE, 1235,56
1235.56
thread 2: de-DE
task: de-DE
thread 3: ja-JP

The best practice is to set the default culture at the start of the program, before any threads are started, or to pass a culture explicitly to the methods that format and parse. The explicit argument is always the safest: the result does not depend on a global setting that other code can change.

C# list cultures

The CultureInfo.GetCultures method gets the list of supported cultures filtered by the specified culture types. AllCultures includes all, the SpecificCultures filter returns the specific ones and NeutralCultures the neutral ones.

Program.cs
using System.Globalization;

Console.OutputEncoding = System.Text.Encoding.UTF8;

CultureInfo[] all = CultureInfo.GetCultures(CultureTypes.AllCultures);
Console.WriteLine($"{all.Length} cultures");
Console.WriteLine($"{CultureInfo.GetCultures(CultureTypes.SpecificCultures).Length} specific cultures");
Console.WriteLine($"{CultureInfo.GetCultures(CultureTypes.NeutralCultures).Length} neutral cultures");

Console.WriteLine();
Console.WriteLine("{0,-10}{1,-5}{2,-35}{3,-25}", "Name", "ISO", "Display name", "Native name");

foreach (CultureInfo ci in CultureInfo.GetCultures(CultureTypes.SpecificCultures).Where(c => c.TwoLetterISOLanguageName is "sk" or "cs" or "de" or "ja" or "ar").Take(12))
{
    Console.WriteLine("{0,-10}{1,-5}{2,-35}{3,-25}", ci.Name, ci.TwoLetterISOLanguageName, ci.DisplayName, ci.NativeName);
}

// neutral and specific cultures
var fr = new CultureInfo("fr");
var frFr = new CultureInfo("fr-FR");
Console.WriteLine($"{fr.Name}: neutral={fr.IsNeutralCulture}, parent of fr-FR: {frFr.Parent.Name}");
Console.WriteLine(CultureInfo.InvariantCulture.Name == "");
Console.WriteLine(CultureInfo.CreateSpecificCulture("fr").Name);

The example counts the cultures and lists those of a few languages.

Console.OutputEncoding = System.Text.Encoding.UTF8;

To show the native names of the cultures, we set the output encoding to UTF-8. In addition, the terminal must have a font that can display all these scripts.

var fr = new CultureInfo("fr");
var frFr = new CultureInfo("fr-FR");
Console.WriteLine($"{fr.Name}: neutral={fr.IsNeutralCulture}, parent of fr-FR: {frFr.Parent.Name}");
Console.WriteLine(CultureInfo.CreateSpecificCulture("fr").Name);

Every specific culture has a neutral parent. The CreateSpecificCulture method turns a neutral culture into a specific one, here fr-FR.

$ dotnet run
787 cultures
547 specific cultures
240 neutral cultures

Name      ISO  Display name                       Native name
ar-001    ar   Arabic (world)                     العربية (العالم)
ar-AE     ar   Arabic (United Arab Emirates)      العربية (الإمارات العربية المتحدة)
ar-BH     ar   Arabic (Bahrain)                   العربية (البحرين)
...
fr: neutral=True, parent of fr-FR: fr
True
fr-FR

The number of cultures depends on the version of .NET and the ICU data.

C# invariant culture

The invariant culture is independent of the machine and of the user. We use it when we persist or transmit data: files, network protocols, databases, configuration. Saving dates and numbers in a culture-independent way allows us to read them later on any machine.

Program.cs
using System.Globalization;

// the invariant culture: stable, independent of the machine; use it for data
CultureInfo.CurrentCulture = new CultureInfo("sk-SK");

DateTime[] dates = [new(2019, 10, 9), new(2020, 1, 2)];
double price = 1234.5;

string line = string.Format(CultureInfo.InvariantCulture, "{0:d}|{1:d}|{2}", dates[0], dates[1], price);
File.WriteAllText("data.txt", line);

Console.WriteLine(line);                       // what is stored
Console.WriteLine($"{dates[0]:d} {price}");    // what the user in Slovakia sees

// reading it back
string[] parts = File.ReadAllText("data.txt").Split('|');

DateTime d = DateTime.Parse(parts[0], CultureInfo.InvariantCulture);
double p = double.Parse(parts[2], CultureInfo.InvariantCulture);
Console.WriteLine($"{d:D}");
Console.WriteLine(p + 1);

// parsing with the wrong culture fails or gives a wrong result
Console.WriteLine(double.TryParse(parts[2], out double wrong) ? wrong : "cannot parse with sk-SK");
Console.WriteLine(DateTime.TryParse("10/09/2019", out var wrongDate) ? wrongDate.ToString("d MMMM yyyy") : "invalid");
Console.WriteLine(DateTime.Parse("10/09/2019", new CultureInfo("en-US")).ToString("d MMMM yyyy"));

The example writes data with the invariant culture while the current culture is Slovak, and reads it back.

string line = string.Format(CultureInfo.InvariantCulture, "{0:d}|{1:d}|{2}", dates[0], dates[1], price);

We pass CultureInfo.InvariantCulture to string.Format. The stored line has the date as month/day/year and the decimal point.

DateTime d = DateTime.Parse(parts[0], CultureInfo.InvariantCulture);
double p = double.Parse(parts[2], CultureInfo.InvariantCulture);

We pass the invariant culture to parse as well, so the data is understood independently of the culture of the reader.

Console.WriteLine(double.TryParse(parts[2], out double wrong) ? wrong : "cannot parse with sk-SK");
Console.WriteLine(DateTime.TryParse("10/09/2019", out var wrongDate) ? wrongDate.ToString("d MMMM yyyy") : "invalid");

Parsing with the wrong culture fails or, worse, gives a wrong result. "1234.5" is not a number in Slovakia, where the decimal separator is a comma. And "10/09/2019" is read as the 10th of September, not as the 9th of October, which the same text means to a user in the United States. Such bugs often stay hidden for a long time, because they appear only on machines with a different configuration.

$ dotnet run
10/09/2019|01/02/2020|1234.5
9. 10. 2019 1234,5
streda 9. októbra 2019
1235,5
cannot parse with sk-SK
10 septembra 2019
9 októbra 2019

Note that the output of p + 1, 1235,5, is a number printed with the Slovak culture, while the number itself stays the same.

C# CultureInfo sorting

Sorting strings is culture specific. Every language orders its letters in its own way. A culture-aware comparer puts the accented letters and the digraphs in the right places. An ordinal comparison, in contrast, compares the numeric values of the characters.

Program.cs
using System.Globalization;

Console.OutputEncoding = System.Text.Encoding.UTF8;

string[] words = ["čaj", "auto", "drevo", "cibuľa", "čučoriedka", "banán", "čerešňa", "červený", "čierny", "cesnak", "Cukor", "chlieb", "hrach"];

var sk = new CultureInfo("sk-SK");
var inv = CultureInfo.InvariantCulture;

var sorted = words.ToList();
sorted.Sort(StringComparer.Create(sk, ignoreCase: false));
Console.WriteLine("sk-SK:   " + string.Join(", ", sorted));

sorted.Sort(StringComparer.Create(inv, ignoreCase: false));
Console.WriteLine("invariant: " + string.Join(", ", sorted));

sorted.Sort(StringComparer.Ordinal);
Console.WriteLine("ordinal: " + string.Join(", ", sorted));

// string comparison and CompareInfo
Console.WriteLine(string.Compare("cesnak", "čaj", sk, CompareOptions.None));
Console.WriteLine(string.Compare("cesnak", "čaj", sk, CompareOptions.IgnoreNonSpace));
Console.WriteLine(string.Equals("čaj", "caj", StringComparison.CurrentCultureIgnoreCase));

CultureInfo.CurrentCulture = sk;
Console.WriteLine(string.Equals("čaj", "caj", StringComparison.CurrentCultureIgnoreCase));
Console.WriteLine(sk.CompareInfo.Compare("čaj", "CAJ", CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace));
Console.WriteLine(sk.CompareInfo.IndexOf("Kôň a čaj", "CAJ", CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace));

The example sorts a list of Slovak words with three comparers and compares strings.

sorted.Sort(StringComparer.Create(sk, ignoreCase: false));

The comparer created for the Slovak culture follows the Slovak alphabet, in which the letter "č" comes right after "c", and "ch" is a separate letter that follows "h". Hence "chlieb" is the last word. Also, the capital letter "Cukor" is sorted among the words by its letters, not before all lower-case words.

sorted.Sort(StringComparer.Ordinal);

The ordinal comparer orders by the code of the characters: all upper-case letters first and all accented letters at the end, because "č" has a higher code than "z". This is fast and deterministic, and the right choice for identifiers and keys, but not for text shown to users.

Console.WriteLine(string.Compare("cesnak", "čaj", sk, CompareOptions.IgnoreNonSpace));
sk.CompareInfo.IndexOf("Kôň a čaj", "CAJ", CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace)

The CompareOptions loosen the comparison: ignore the case, accents, or the width of characters. The CompareInfo object of a culture has comparison and search methods which take these options.

$ dotnet run
sk-SK:   auto, banán, cesnak, cibuľa, Cukor, čaj, čerešňa, červený, čierny, čučoriedka, drevo, hrach, chlieb
invariant: auto, banán, čaj, čerešňa, červený, cesnak, chlieb, cibuľa, čierny, čučoriedka, Cukor, drevo, hrach
ordinal: Cukor, auto, banán, cesnak, chlieb, cibuľa, drevo, hrach, čaj, čerešňa, červený, čierny, čučoriedka
-1
-1
False
False
1
-1

The invariant culture sorts "č" as a variant of "c", so "čaj" comes before "cesnak"; only the Slovak culture knows the rule of the alphabet. In the last lines, the accent-insensitive search gives -1: the text "Kôň a čaj" does not contain "caj" with the same letter in Slovak, because in the Slovak alphabet "č" is a different letter and not an accented "c". This is the difference between linguistic and simple comparison.

NumberFormatInfo

The NumberFormatInfo object provides culture-specific information for formatting and parsing numeric values. It is available from the NumberFormat property of a culture.

Program.cs
using System.Globalization;

Console.OutputEncoding = System.Text.Encoding.UTF8;

foreach (var name in new[] { "en-US", "sk-SK", "de-CH", "fr-FR", "hi-IN", "fa-IR", "ja-JP" })
{
    var nfi = new CultureInfo(name).NumberFormat;

    Console.WriteLine($"{name}: decimal='{nfi.NumberDecimalSeparator}' group='{nfi.NumberGroupSeparator}' " +
        $"currency='{nfi.CurrencySymbol}' percent='{nfi.PercentSymbol}' negative='{nfi.NegativeSign}'");
}

Console.WriteLine(new CultureInfo("fr-FR").NumberFormat.NumberGroupSizes.Length);
Console.WriteLine(string.Join(",", new CultureInfo("hi-IN").NumberFormat.NumberGroupSizes));

The example shows the decimal and group separators, the currency symbol, the percent symbol, and the negative sign for several cultures.

var nfi = new CultureInfo(name).NumberFormat;

The NumberFormatInfo properties include NumberDecimalSeparator, NumberGroupSeparator, NumberGroupSizes, CurrencySymbol, CurrencyDecimalDigits, PercentSymbol, and NegativeSign.

$ dotnet run
en-US: decimal='.' group=',' currency='$' percent='%' negative='-'
sk-SK: decimal=',' group=' ' currency='€' percent='%' negative='-'
de-CH: decimal='.' group='’' currency='CHF' percent='%' negative='-'
fr-FR: decimal=',' group=' ' currency='€' percent='%' negative='-'
hi-IN: decimal='.' group=',' currency='₹' percent='%' negative='-'
fa-IR: decimal='٫' group='٬' currency='ریال' percent='٪' negative='‎−'
ja-JP: decimal='.' group=',' currency='¥' percent='%' negative='-'
1
3,2

The last two lines show the group sizes. Most cultures group the digits by three, but the Indian culture uses a group of three followed by groups of two (1,21,56,320). The group separator of the French and Slovak cultures is a no-break space, so a number is not split across lines. The Persian culture uses its own decimal and group characters, a different percent sign and a real minus sign.

C# CultureInfo numbers, currency and percent

Cultures differ in the decimal separator (a point in the United Kingdom, Japan, and Israel, a comma in Slovakia, France, and Germany), in the thousands separator (a space, a period, an apostrophe, a comma), in the grouping, in the position and spacing of the currency symbol, and of the percent sign.

Program.cs
using System.Globalization;

Console.OutputEncoding = System.Text.Encoding.UTF8;

decimal val = 12_156_320.54m;
int whole = 12_156_320;
double pct = 0.68;

foreach (var name in new[] { "en-US", "sk-SK", "de-CH", "hi-IN", "es-ES", "ja-JP", "fa-IR" })
{
    var ci = new CultureInfo(name);

    Console.WriteLine($"{name,-6} | {whole.ToString("N0", ci),-14} | {val.ToString("N2", ci),-16} | {val.ToString("C2", ci),-18} | {pct.ToString("P0", ci)}");
}

The example prints the same three values in seven cultures: an integer with the N0 format, a decimal with N2 and C2, and a double as a percentage with P0. We specify the number of decimals explicitly, because the default differs between platforms.

$ dotnet run
en-US  | 12,156,320     | 12,156,320.54    | $12,156,320.54     | 68%
sk-SK  | 12 156 320     | 12 156 320,54    | 12 156 320,54 €    | 68 %
de-CH  | 12’156’320     | 12’156’320.54    | CHF 12’156’320.54  | 68%
hi-IN  | 1,21,56,320    | 1,21,56,320.54   | ₹1,21,56,320.54    | 68%
es-ES  | 12.156.320     | 12.156.320,54    | 12.156.320,54 €    | 68 %
ja-JP  | 12,156,320     | 12,156,320.54    | ¥12,156,320.54     | 68%
fa-IR  | 12٬156٬320     | 12٬156٬320٫54    | ریال12٬156٬320٫54  | 68٪

The currency symbol comes from the culture, but the amount is just a number. A culture does not convert 12 million euros to dollars; showing "$" in front of a value in euros is a bug. When the currency is known, build the text from the currency code and the amount, with the culture used only for the number format, or take the symbol from the RegionInfo.

C# CultureInfo dates, week and names

The DateTimeFormat property (a DateTimeFormatInfo) has the patterns for dates and times, the names of days and months, and the rules for the week: the first day of the week and the way to find the first week of the year.

Program.cs
using System.Globalization;

Console.OutputEncoding = System.Text.Encoding.UTF8;

var now = new DateTime(2026, 10, 18, 22, 31, 7);

foreach (var name in new[] { "en-US", "en-GB", "sk-SK", "de-DE", "ja-JP", "ar-SA" })
{
    var ci = new CultureInfo(name);
    var dtf = ci.DateTimeFormat;

    Console.WriteLine($"{name}: first day={dtf.FirstDayOfWeek}, week rule={dtf.CalendarWeekRule}, short date='{dtf.ShortDatePattern}', long time='{dtf.LongTimePattern}'");
}

var sk = new CultureInfo("sk-SK");
Console.WriteLine(now.ToString(sk.DateTimeFormat.FullDateTimePattern, sk));
Console.WriteLine(now.ToString(sk.DateTimeFormat.LongDatePattern, sk));
Console.WriteLine(now.ToString(sk.DateTimeFormat.ShortTimePattern, sk));

Console.WriteLine(string.Join(", ", sk.DateTimeFormat.DayNames));
Console.WriteLine(string.Join(", ", sk.DateTimeFormat.AbbreviatedDayNames));

var hu = new CultureInfo("hu-HU");
Console.WriteLine(string.Join(", ", hu.DateTimeFormat.MonthNames.Take(12)));
Console.WriteLine(string.Join(" ", hu.DateTimeFormat.AbbreviatedMonthNames.Take(12)));
Console.WriteLine(sk.DateTimeFormat.GetMonthName(10));
Console.WriteLine(sk.DateTimeFormat.GetAbbreviatedDayName(DayOfWeek.Friday));

The example prints the week rules and the date patterns of several cultures, and then the names of days and months in Slovak and Hungarian.

Console.WriteLine($"{name}: first day={dtf.FirstDayOfWeek}, week rule={dtf.CalendarWeekRule}, ...");

In the United States, the week starts on Sunday, and the first week of the year is the one that contains January 1. In most European countries, the week starts on Monday, and the first week is the first one that has at least four days in the new year (the ISO 8601 rule). Saturday is the first day of the week in Iran (fa-IR), and in several Arabic countries, such as Egypt and the United Arab Emirates.

Console.WriteLine(now.ToString(sk.DateTimeFormat.FullDateTimePattern, sk));

The patterns, such as FullDateTimePattern, LongDatePattern, and ShortTimePattern, are format strings that can be passed to ToString. The standard format specifiers like "F", "D", and "t" select them automatically.

Console.WriteLine(string.Join(", ", sk.DateTimeFormat.DayNames));
Console.WriteLine(string.Join(" ", hu.DateTimeFormat.AbbreviatedMonthNames.Take(12)));

The DayNames and MonthNames properties return the full names, and AbbreviatedDayNames and AbbreviatedMonthNames the short names. The day names start with Sunday, whatever the first day of the week is, so they are indexed with (int)DayOfWeek. The month array has thirteen entries (the last one is empty), which is why we take twelve.

$ dotnet run
en-US: first day=Sunday, week rule=FirstDay, short date='M/d/yyyy', long time='h:mm:ss tt'
en-GB: first day=Monday, week rule=FirstFourDayWeek, short date='dd/MM/yyyy', long time='HH:mm:ss'
sk-SK: first day=Monday, week rule=FirstFourDayWeek, short date='d. M. yyyy', long time='H:mm:ss'
de-DE: first day=Monday, week rule=FirstFourDayWeek, short date='dd.MM.yyyy', long time='HH:mm:ss'
ja-JP: first day=Sunday, week rule=FirstDay, short date='yyyy/MM/dd', long time='H:mm:ss'
ar-SA: first day=Sunday, week rule=FirstDay, short date='d‏/M‏/yyyy g', long time='h:mm:ss tt'
nedeľa 18. októbra 2026 22:31:07
nedeľa 18. októbra 2026
22:31
nedeľa, pondelok, utorok, streda, štvrtok, piatok, sobota
ne, po, ut, st, št, pi, so
január, február, március, április, május, június, július, augusztus, szeptember, október, november, december
jan. febr. márc. ápr. máj. jún. júl. aug. szept. okt. nov. dec.
október
pi

CultureInfo Calendar

A calendar is a system of organizing datetime units. Cultures use different calendars, and some cultures can use multiple calendars. The Calendar property is the default calendar of the culture, and OptionalCalendars lists the others.

Program.cs
using System.Globalization;

Console.OutputEncoding = System.Text.Encoding.UTF8;

var now = new DateTime(2026, 10, 18, 13, 35, 18);
var perCi = new CultureInfo("fa-IR");
Console.WriteLine($"Default calendar: {perCi.Calendar}");

foreach (var oc in perCi.OptionalCalendars)
{
    perCi.DateTimeFormat.Calendar = oc;
    Console.WriteLine(now.ToString("F", perCi));
}

// the calendar of a culture is a property of the culture, the DateTime stays Gregorian
Console.WriteLine(new CultureInfo("th-TH").Calendar.GetYear(now));
Console.WriteLine(new CultureInfo("ja-JP").DateTimeFormat.Calendar);
Console.WriteLine(now.ToString("D", new CultureInfo("th-TH")));

The Persian culture uses the Persian calendar as the default one, and the Gregorian and Hijri calendars as optional calendars. We display one datetime in all supported Persian calendars, and then use the Thai and Japanese cultures.

foreach (var oc in perCi.OptionalCalendars)
{
    perCi.DateTimeFormat.Calendar = oc;
    Console.WriteLine(now.ToString("F", perCi));
}

We go through the array of supported calendars, set the DateTimeFormat.Calendar to the selected one, and print the datetime. A DateTime value is always Gregorian internally; the calendar of the culture changes only how it is shown and the calendar-specific methods.

$ dotnet run
Default calendar: System.Globalization.PersianCalendar
1405 مهر 26, یکشنبه 13:35:18
2026 اکتبر 18, یکشنبه 13:35:18
1448 جمادی‌الاول 7, یکشنبه 13:35:18
2569
System.Globalization.GregorianCalendar
วันอาทิตย์ที่ 18 ตุลาคม พ.ศ. 2569

The Thai culture shows the Buddhist era year 2569, which is 543 years ahead of the Gregorian year. See the date and time tutorial for more about calendars.

C# RegionInfo and TextInfo

The RegionInfo class describes a country or region: its names, the ISO codes, the currency, and the use of the metric system. The TextInfo class of a culture knows the casing rules and the direction of the writing.

Program.cs
using System.Globalization;

Console.OutputEncoding = System.Text.Encoding.UTF8;

// RegionInfo describes a country
foreach (var name in new[] { "sk-SK", "en-US", "ja-JP", "de-CH" })
{
    var ri = new RegionInfo(name);
    Console.WriteLine($"{ri.Name}: {ri.EnglishName}, {ri.NativeName}, currency {ri.ISOCurrencySymbol} ({ri.CurrencySymbol}), metric: {ri.IsMetric}");
}

// the region of the current culture
Console.WriteLine(new RegionInfo(CultureInfo.CurrentCulture.Name).TwoLetterISORegionName);

// text info: casing and the writing direction
var ar = new CultureInfo("ar-SA");
var en = new CultureInfo("en-US");
Console.WriteLine($"{ar.TextInfo.IsRightToLeft} {en.TextInfo.IsRightToLeft}");
Console.WriteLine(en.TextInfo.ToTitleCase("an old falcon"));
Console.WriteLine(new CultureInfo("tr-TR").TextInfo.ToUpper("istanbul"));
Console.WriteLine(en.TextInfo.ToUpper("istanbul"));
Console.WriteLine(en.TextInfo.ListSeparator + new CultureInfo("sk-SK").TextInfo.ListSeparator);

The example prints region information and shows text properties.

Console.WriteLine(new CultureInfo("tr-TR").TextInfo.ToUpper("istanbul"));

The casing depends on the culture. The Turkish culture uppercases "i" to the dotted "İ", while the English one gives "I". Use ToUpperInvariant for data, such as keys and protocol keywords. The IsRightToLeft property tells whether the writing goes from right to left, which the user interface must respect.

$ dotnet run
SK: Slovakia, Slovensko, currency EUR (€), metric: True
US: United States, United States, currency USD ($), metric: False
JP: Japan, 日本, currency JPY (¥), metric: True
CH: Switzerland, Schweiz, currency CHF (CHF), metric: True
US
True False
An Old Falcon
İSTANBUL
ISTANBUL
,;

The last line shows the list separators of the United States (comma) and Slovakia (semicolon), which matters for CSV files created by spreadsheet programs: in a Slovak, German, or French installation of a spreadsheet, a "CSV" file uses semicolons, since the comma is the decimal separator there.

C# CultureInfo errors and customization

An invalid culture name throws a CultureNotFoundException. The objects returned by CultureInfo.GetCultureInfo are cached and read-only. To change a setting, we clone a culture.

Program.cs
using System.Globalization;

// an unknown culture name
try
{
    var ci = new CultureInfo("xx-YY");
    Console.WriteLine($"{ci.Name} {ci.DisplayName}");
}
catch (CultureNotFoundException e)
{
    Console.WriteLine(e.Message);
}

try
{
    new CultureInfo("not a culture");
}
catch (CultureNotFoundException e)
{
    Console.WriteLine(e.InvalidCultureName);
}

// CultureInfo.GetCultureInfo caches and returns read-only objects
var cached = CultureInfo.GetCultureInfo("sk-SK");
Console.WriteLine(cached.IsReadOnly);

try
{
    cached.NumberFormat.NumberDecimalSeparator = ";";
}
catch (InvalidOperationException e)
{
    Console.WriteLine("read-only: " + e.Message);
}

// an own copy can be customized
var custom = (CultureInfo)new CultureInfo("en-US").Clone();
custom.NumberFormat.CurrencySymbol = "EUR ";
custom.NumberFormat.CurrencyDecimalDigits = 0;
Console.WriteLine(1234.5.ToString("C", custom));

// culture name comparisons are case-insensitive, and codes use a hyphen
Console.WriteLine(new CultureInfo("SK-sk").Name);
Console.WriteLine(CultureInfo.GetCultureInfo("sk_SK", predefinedOnly: false).Name);

The program tries invalid names, a read-only culture, and a customized copy.

var ci = new CultureInfo("xx-YY");

Not every unknown name throws an exception: a well-formed name, such as "xx-YY", is accepted by the ICU-based implementation, and creates a culture without any data, which formats like the invariant culture. Names that are not well formed, like "not a culture", throw the exception, and the InvalidCultureName property tells which name was wrong. Don't depend on the exception to validate culture names; compare with the list of GetCultures.

var custom = (CultureInfo)new CultureInfo("en-US").Clone();
custom.NumberFormat.CurrencySymbol = "EUR ";
custom.NumberFormat.CurrencyDecimalDigits = 0;

A cloned culture is writable. We change the currency symbol and the number of decimals of the currency format. Customized cultures are useful for exotic formats, but a simple NumberFormatInfo object is often enough.

$ dotnet run
xx-YY xx (YY)
not a culture
True
read-only: Instance is read-only.
EUR 1,234
sk-SK
sk_sk

Culture names are case-insensitive, and the names are normalized: "SK-sk" becomes "sk-SK". The last line shows that with the ICU implementation an underscore is accepted but not normalized, so the hyphen should be used.

Source

CultureInfo class

In this article we have worked with C# CultureInfo, which is used to globalize C# applications.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.