Written by Oskar Sjöberg on 30:th of september 2016
I’ve been porting a multilingual e-commerce web site from PHP/ CodeIgniter to ASP.Net / MVC, it has been live for almost six months now, on the whole; the port was a breeze.
Last week I decided to refactor some code left from the PHP-era; custom code for managing culture-specific formatting of numbers and currencies. It was a horrible mess of conditions and string formatting with regular expressions, it did not support alternate placements of the currency symbol and it was difficult to extend with new currencies.
Also I thought the refactoring would be quite easy. Just set the correct Thread.CurrentThread.CurrentCulture and Thread.CurrentThread.CurrentUICulture based on the visited domain on Application_BeginRequst and then replace all calls to the ported formatting functions to the ones built into the BCL.
It all seemed straight forward until I visited the Swedish version of the page where I directly saw an error.
Short translated summary from the Swedish writing rules on Wikipedia (Be aware; Swedish link).
Numbers are written with a space grouping separator between every group of three digits from the right, like this: 1 000 with optional decimals. Swedish currency is written the same way but optionally with two decimals and a trailing currency symbol, “kr”, like this: 1 000,00 kr.
So my thinking was that in code, it would look like this:
Thread.CurrentThread.CurrentCulture = new CultureInfo("sv-SE", useUserOverride: false);
.
.
.
Console.WriteLine(1000.ToString("N"));
Console.WriteLine(1000.ToString("C"));
The output for currency however, did not match my expectations:
1 000,00
1.000,00 kr
I obviously spotted this because Swedish is my native language. What I find interesting is that it is only currencies that use the “.” grouping separator.
After some poking around I found out that CultureInfo.NumberFormatInfo contains two properties for what the grouping symbol would be; NumberGroupSeparator and CurrencyGroupSeparator.
So I decided to investigate how common it is for cultures to have a different grouping separator for numbers than the grouping separator for currencies.
CultureInfo
.GetCultures(CultureTypes.AllCultures)
.Where(cultureInfo => cultureInfo.NumberFormat.CurrencyGroupSeparator != cultureInfo.NumberFormat.NumberGroupSeparator)
.Select(cultureInfo => new { cultureInfo.EnglishName, cultureInfo.Name, cultureInfo.NumberFormat.CurrencyGroupSeparator, cultureInfo.NumberFormat.NumberGroupSeparator })
EnglishName | Name | CurrencyGroupSeparator | NumberGroupSeparator |
---|---|---|---|
Dutch (Belgium) | nl-BE | . | |
Dari | prs | , | . |
Dari (Afghanistan) | prs-AF | , | . |
Sami, Northern (Sweden) | se-SE | . | |
Sami (Southern) | sma | . | |
Sami, Southern (Sweden) | sma-SE | . | |
Sami (Lule) | smj | . | |
Sami, Lule (Sweden) | smj-SE | . | |
Swedish | sv | . | |
Swedish (Sweden) | sv-SE | . |
Out of 832 cultures on my machine there are only ten that have different group separators. I can obviously tell that this is not right for Swedish, and likely it is not right for Sami which is a language used by the Sami people in the northern part of Sweden, Finland and Norway.
However when visiting various e-commerce sites like the Swedish version of the Microsoft store, they print out the currency correct. Also applying the currency format on a cell in Excel is also correct and has probably been correct through all times.
The culture data used by CultureInfo is fetched from the operating system. It seems like Microsoft itself is not using the data provided from the operating system.
Also note that different versions of Windows come with different settings pre-loaded. For example the currency symbol for da-DK (Danish-Denmark) is printed before the value on Windows Server 2012 but on Windows 10 the currency symbol is printed after the value. This means that your development environment will not yield the same result as the production server.
The solution in my case was pretty straight forward, clone the Swedsh CultureInfo including its NumberFormat and change the CurrencyGroupSeparator to space, and then set the Thread.CurrentCulture to my customized version.
But this makes me beg the question, if everyone using CultureInfo need to make work-arounds for the errors in CultureInfo and also handle descrepancies between operating systems, what is the point of CultureInfo? Are there any more errors or descrepancies in the CultureInfo data that I need to compensate for? Next time I am not likely to be familiar with the language and can spot the error.