What’s wrong with ASP.NET? Cultures

The problem

The ability offered by .NET to set a thread-level culture then automatically format and select localizable resources using that culture’s settings is wonderful stuff. Unfortunately, it’s an approach that plays out quite a bit better in a client-side application than in a server-based application. The reason for this lies in the nature of the work one performs in a server-based application: some formatting and/or rendering is intended for consumption by client applications, but some (e.g.: log entries) is intended for consumption on the server.

Things tend to muddle along just fine as long as both the client and the server use the same or similar cultures. This is helped along by the fact that most servers are unlikely to have additional .NET language packs installed, so any exceptions logged on the server are likely to contain English error messages. However, there are plenty of cases in which it might be necessary to install additional language packs on the server (e.g.: server and/or application administrators don’t speak English), so it might be a wee bit naïve for a software vendor to assume that the only .NET language supported on a server is one that will be understood by folks who read text generated for comsumption on the server.

In case this all sounds a bit odd, let’s take a look at some concrete examples. Our base scenario is a localizable ASP.NET application that, as is typical, sets the current thread’s CurrentCulture and CurrentUICulture properties to match the preferred culture of the client user. Amongst other things, this application happens to create a log entry every time an invoice is created via its UI. e.g.:

private void LogOrder(int invoiceId, double price)
string logText = string.Format(“Invoice {0:N0} for {1:C} created at {2:G}.”,
invoiceId, price, DateTime.Now);

// Text written to log here…

If the application has set the current thread’s CurrentCulture property to the client’s preferred culture, here is what will be written to the server-side log for various values of the client culture:

Client culture Logged text
en-US Invoice 21,456 for $5,000.00 created at 5/6/2006 9:30:00 AM.
en-CA Invoice 21,456 for $5,000.00 created at 06/05/2006 9:30:00 AM.
en-GB Invoice 21,456 for £5,000.00 created at 06/05/2006 09:30:00.
fr-FR Invoice 21 456 for 5 000,00 € created at 06/05/2006 09:30:00.
fr-CA Invoice 21 456 for 5 000,00 $ created at 2006-05-06 09:30:00.
es-ES Invoice 21.456 for 5.000,00 € created at 06/05/2006 9:30:00.
es-MX Invoice 21,456 for $5,000.00 created at 06/05/2006 09:30:00 a.m..
ja-JP Invoice 21,456 for ¥5,000 created at 2006/05/06 9:30:00.

The above table shows some of the sorts of trouble that one can get into applying client-side culture options to formatting of server-side texts, and things will only get worse when/if digit substitution is fully implemented. The problem is compounded by the fact that the client-specified culture used to apply formatting is no longer known at the time one reads the server-side text. Of course, formatting as in the above example is a mistake, and there are ways to avoid the problem, but more on that after we look at something a little more difficult to work around.

Like many applications built upon it, the .NET framework emits a variety of texts based on localized resources. The examples with which many developers will likely be most familiar are exception messages. In fact, in most applications, a very large majority of exception messages will have been generated within the .NET Framework base class libraries. Let’s say that your application happens to be running on a server with the Hungarian language pack, and an exception is logged while a Hungarian client is using the application. Would you have any idea what the exception message “Nem megfelelő a bemeneti karakterlánc formátuma” means? (It happens to be the Hungarian version of “Input string was not in a correct format”, but BabelFish won’t exactly be helping you figure that one out…)

To add to the fun, some Framework-generated texts (including exception messages) are automatically formatted from resource strings containing placeholders. Since you cannot control the formatting applied in these instances, you may run into interpretation issues even if you don’t have any additional language packs installed. For example, if you see an exception message containing the number “2,345”, how can you tell if this was a 2345 written under an American culture option or a 2.345 written under a French Canadian setting? Sometimes context may help, but other times you’ll just plain have to guess.

The workaround

So what can we do to avoid the problem? The easy answer is to not use the thread culture as a store for the client’s preferred culture. This, of course, means that we would then need to perform explicit formatting of any non-string values before passing them to UI elements for rendering. While somewhat burdensome, particularly for folks who like living the RAD life, this isn’t exactly the end of the world. There’s also a bit of good news: FxCop can help you identify at least those cases of explicit formatting that are defaulting to using the thread culture.

The first bit of rather bad news with this approach is that you can’t use ASP.NET’s auto-magical client culture detection since it pops the detected culture into the current thread properties. Things might be a bit different if the ASP.NET team had chosen to make the HttpApplication.SetCulture method virtual, but it’s not, so we’re stuck implementing our own mechanism from scratch if we want to use an alternate store for the client culture.

The second bit of bad news is that there are several ASP.NET controls that are rather aggressive about using the thread culture internally. For example, when using the CompareValidator with ValidationDataType.Double, it’s possible to use culture-specific parsing1. However, there’s no way to specify which culture one wants to use for parsing. Unless you choose to use culture-invariant parsing, the thread culture will be used. That means that a French Canadian user’s 5,123 would theoretically be interpreted as 5123 rather than 5.123 if the thread culture is set to en-US because that’s the appropriate setting for server-side use. However, things are even worse in practice than in theory, and our 5,123 will fail validation since the CompareValidator does not permit use of thousands group separators and will reject the comma if the thread culture is en-US.

For a fully functional workaround, you’ll need to implement at least four sets of changes:

  1. Use an alternate store for the client culture,
  2. Create a mechanism for populating this new store with the client-preferred culture (rather than setting Culture and UICulture attributes to “auto”),
  3. Where possible, pre-format non-string values that will be rendered by controls, and
  4. Implement custom versions of control that take the rendering culture as a property rather than using the thread culture exclusively.

The solution

The workarounds are a pack of trouble, and far more work than most folks would be willing to put in unless they’ve already been bitten by client vs. server culture bugs. The real solution here is for the ASP.NET platform to properly accomodate separate tracking of client and server cultures. Obviously, items 1 and 2 from the workarounds list above would be a start. In addition, it would probably be a great idea if controls defaulted to using the client culture but allowed easy overriding to use either the server culture or a custom assigned culture.

1For some bizarre reason, CompareValidator is strictly culture-invariant for some data types (e.g.: ValidationDataType.Integer), which presents its own set of potential problems. However, this isn’t directly relevant to the client vs. server culture issue, so I’ll drop it for now. However, if you want to complain, connect.microsoft.com is the place.

Leave a Reply

Your email address will not be published. Required fields are marked *