While you could simply start with one default language and let him switch the language using some control, it is probably far more "polite" to use the fact that he already tells you his preferences: Each browser has the ability to let the user chose his is language preferences:

This information is sent with each request via the accept-language http header, which contains a collection of weighted language tags, say the following header with the above settings:

Accept-Language: de-DE,de;q=0.8,en-US;q=0.5,en;q=0.3

This is very basic stuff and I’m only going into this detail because I have actually seen code that uses the header value as if it contains exactly one language and ignoring the weight.

The simple approach…

“All” we have to do is grab the language from the header and tell .NET for each request:

Note the exception handling. This is necessary due to the different notions of "language" (see the last post), and the possibility that .NET might not support Klingon. Most people will never actually encounter this issue during regular operations, but there is the occasional exception (no pun intended). And of course the possibility of malicious requests…

Accessing the header value and setting the cultures is something that can be found in every blog post and tutorial. Actually ASP.NET would do this automatically (see also Scott’s post).

Unfortunately this is simply too shortsighted.

Matching the user’s language choice…

I don’t want the user’s first language choice applied unconditionally. I want the user’s language choices that I can best support with my supported cultures.

My application is going to support English and German. What if my French colleague showed up? We could set the culture to “fr-FR” (as, in fact, the above code will do). Since there are no French localizations, proper fallback strategy should ensure he gets English texts. Is this really the best choice, if he asks for “fr-FR,de-DE,en-US”? And what about date formats? Is the meaning of 01/06/2013 immediately obvious? Does he even suspect, that – within an otherwise English UI – this is actually the first of June (interpretation as dd/mm/yyyy) – not epiphany (mm/dd/yyyy)? Thus it is better to present him a consistent (in this case American English, i.e. en-US) version, rather than a confusing mix.

So, what I want is matching the users’ list of preferred regions (all of the header values, not just the first one!) to the collection of regions my application supports, and set the culture to the best match.

This is something I have rarely seen addressed, much less correctly. E.g. Nadeem is one of the few who actually recognized that necessity; still, his implementation does not quite meet my expectations.

Let’s make that crystal clear: Say my application supports en-US and de-DE, with en-US the default.

Perfect matches:

If the user’s request is "de-DE, en-US;q=0.8", then de-DE is the perfect match. (Obviously.) Likewise, if the user’s request is "fr-FR, en-US;q=0.8", then en-US is the perfect match. Not the users’ first choice, but anyway. Also if the user’s request is "fr-FR, de-DE;q=0.8, en-US;q=0.5", then de-DE is the perfect match. This is a case which most, if not all implementations I have seen, are missing.

Something that is also missed quite often: If his request is "de, en-US;q=0.8", then de-DE is still the correct choice, because de encompasses all German regions.

If his request is "de-AT, en-US;q=0.8", then en-US is the best choice, because it’s a perfect match, while de-AT matches de-DE only partly. This is debatable, but since he could control that via the browser settings, it’s better than other interpretations.

Partial matches:

If his request is "de-AT, fr-FR;q=0.8", then we have no perfect match. But de-DE matches de-AT at least partly, so it would be a better choice, than using the fallback to the default region en-US.

No match:

Only if no requested region matches any of the supported cultures even slightly, e.g. "es-ES, fr-FR;q=0.8", the default region en-US is used – nothing else that could be done.

Now, this requires a little more code than before…

Parsing the header…

The Accept-Language header generally contains entries with weights (usually they are sorted according to weight anyway, but I don’t like to rely on that). Parsing it is relatively straightforward, but requires some groundwork: