ASP.NET Core Localization – Culture

Globalization is the process of designing or retrofitting an application so that it is capable of supporting multiple cultures (languages and regions).  Localization is the process of adding the resources needed for the application to support a specific culture.  This is commonly thought of as providing text translations for the culture’s language, but it also includes formatting dates and numbers.  For example, dates in the United States are typically formatted as “month-day-year” while almost every other country formats dates as either “day-month-year” or “year-month-day”, with varying separators (a slash, dash, or period are common).  Similarly, when formatting numbers, cultures use different digit group separators (comma, period, apostrophe, space) and decimal marks (comma or period).

In this post we’ll look at culture and formatting of numbers and dates, determining request culture, and building custom request culture providers.  You can download the example projects here:

Note:  these projects use the RC2 daily builds; to use the dailies, please see this post.

Culture and Formats

We identify cultures using a name format consisting of an ISO 639-1 two-character lowercase language code and an ISO 3166-1 two-character uppercase country code.  For example, the culture name for English in the United States is “en-US”, while the culture name for English in Canada is “en-CA”.

The table below shows the variations of date and number format across different cultures.  Currency values are also shown since the currency symbol is part of the formatting.  When formatting a number as a currency, specify the culture explicitly or be sure the current thread culture is what you expect; 42 US Dollars is not the same as 42 Euros.

Culture Name Description Formatted Date Formatted Number Formatted Currency
en-US English (United States) 5/1/2016 1,234,567.89 $42.00
de-DE German (Germany) 01.05.2016 1.234.567,89 42,00 €
de-CH German (Switzerland) 01.05.2016 1’234’567.89 CHF 42.00
fr-FR French (France) 01/05/2016 1 234 567,89 42,00 €
fr-CH French (Switzerland) 01.05.2016 1 234 567.89 CHF 42.00
ko-KR Korean (Korea) 2016-05-01 1,234,567.89 ₩42

I tested this on both Windows and Ubuntu, but on Windows, you’ll have to configure the console to use a font with international characters (e.g. Consolas) and change the code page to UTF-8:

console-windows

Configuring Localization

To work with localization, add the following dependencies to your project.json file:

To configure localization, we need an instance of RequestLocalizationOptions.  This class lets us control aspects of localization like the cultures supported, the default culture, and the strategies used to determine request culture.  We can also use this class to indicate whether localization should fall back to a parent culture if the request culture isn’t in the list of supported cultures but the application does support the parent culture.  For example, if the request culture is “fr-CH” but that is not supported, we can configure localization to fall back to “fr” if that is a supported culture.

Here we’ll create a RequestLocalizationOptions instance and configure it with a list of supported cultures:

You might wonder why we have to provide a list of supported cultures.  It seems reasonable to expect that the framework would support any culture provided by the request, but the problem is that a request could specify a non-existent culture like “ms-FT”, and the process of trying to create the instance and throwing an exception is expensive.  By providing a list of supported cultures, the request localization middleware can simply look  up the requested culture against the list and fall back to the default if it isn’t found.  For some historical context, it is interesting to read this github issue.

It is worth mentioning the difference between culture and UI culture:  the current culture is used when formatting data, while the UI culture is used for localized string resources, which we’ll see in the next post.

In the Startup.Configure method, we call the UseRequestLocalization extension method, which adds the RequestLocalizationMiddleware to the application builder using the localization options provided:

Determining Request Culture

When the RequestLocalizationMiddleware is invoked, it uses the request culture providers configured in the options to determine the culture of the request.  RequestLocalizationOptions has a RequestCultureProviders property, which is a List<IRequestCultureProvider>.

The IRequestCultureProvider interface is simple, having a single method:

ProviderCultureResult has properties that indicate the request’s culture(s).  As we’ll see when we look at the Accept-Language header, a request might specify multiple cultures.

By default, RequestLocalizationOptions sets its RequestCultureProviders property to a list with the following providers, in this order:

  1. QueryStringRequestCultureProvider
  2. CookieRequestCultureProvider
  3. AcceptLanguageHeaderRequestCultureProvider

RequestLocalizationMiddleware calls the providers in this order, and the first provider that returns culture information will have its results used.

Using the Query String

The QueryStringCultureProvider expects default query string keys of “culture” to specify culture and “ui-culture” to specify the UI culture.  You can change the key values using the QueryStringKey and UIQueryStringKey properties of QueryStringCultureProvider.

aspnet-localization-querystring-linux

Note that even though you can use the same field name multiple times on the query string, the provider will only use the first occurrence of the fields.  So, for example, if we use this query string:

http://localhost:5000/?culture=en-CA&culture=fr-FR

The QueryStringCultureProvider only looks at the first “culture” value, and in this case uses “en-CA” if that is supported, or the default culture if “en-CA” is not supported.

Using a Cookie

The CookieRequestCultureProvider uses a default cookie name of .AspNetCore.Culture.  and expects a cookie value with this format:  “c={culture-name}|uic={ui-culture-name}”.  You can change the cookie name with the CookieRequestCultureProvider.CookieName property, but not the format of the cookie value.

aspnet-localization-cookie-linux

Using the Accept-Language Header

The AcceptLanguageHeaderRequestCultureProvider determines the request culture from the Accept-Language header.  This provider is different from the other two in that you can get a provider result with multiple languages, and you can’t specify the culture and UI culture separately; the provider will use the same culture for both.

The value of the Accept-Language header is a series of one or more language ranges, each of which can have a quality value indicating the client’s preference.

For example, this header value says the client prefers German, but will also accept US English.  If neither of those are available, the client will accept French:

Accept-Language: de-DE, en-US;q=0.8, fr-FR;q=0.7

If the quality value is not specified, it defaults to 1.  For more on the Accept-Language header, see RFC 2616.

aspnet-localization-acceptlanguage-linux

Custom Request Culture Providers

If the default request culture providers don’t meet your needs, ASP.NET Core provides two ways to add custom request culture logic.  For example, you might create a provider that retrieves the user’s preferred culture from a database.  In this case, you could add your provider logic immediately after the cookie provider, and in your custom provider logic, retrieve the culture and set the cookie for use in future requests.

In these examples, we’ll simply pass the culture and UI culture as segments in the request URL.  While this is not a practical example, it does allow us to easily test our custom logic.

The first way we can add custom request provider logic is to use the CustomRequestCultureProvider, which takes our logic as a Func<HttpContext, Task<ProviderCultureResult>>. In our example, we create a new CustomRequestCultureProvider and insert it as the first provider in the options.RequestCultureProviders list:

Now we can pass culture information as path segments of the URL:

aspnet-localization-custom-request

Our second option is to create a class that implements the IRequestCultureProvider interface:

We add an instance of our custom provider to the options.RequestCultureProviders list:

And again, we can pass culture information as part of the URL using the values expected by our class:

aspnet-localization-my-custom-request

In my next post, I’ll look at how ASP.NET Core uses the UI culture to provide localized strings.

1 Comment

  1. […] my last post on localization I showed how culture determines the formatting of dates and numbers, and how […]

Leave a Reply