Managing Cultures in a Global Application

I have been doing a lot of work on globalized applications lately and I have learned a lot. I thought I'd try to share some of what I've learned with everyone. One of the first issues we ran into was managing formatting of data for different scenarios. Educating developers on which culture to use when is challenging and you end up not having a unified formatting. Also, if this formatting ever changes, you would need to then go find all the places which need to be changed.  This could be daunting because you don't know the original intent of the developer.  Did the original developer choose "en-US" because she wanted to format it for a developer, or because that was the culture of the database?  It's hard to tell, so we want to avoid getting ourselves into this situation.

We'll start with a simple pattern and word towards a more advanced one.  They all should work, you just end up with some tradeoffs between simplicity and maintainability. In the simple example, we create a class with properties which map to an intent.

public static class Cultures
public static CultureInfo UserFormatCulture { get { return Thread.CurrentThread.CurrentCulture; } }
public static CultureInfo UserLanguageCulture { get { return Thread.CurrentThread.CurrentUICulture; } }
public static CultureInfo DatabaseCulture { get { return CultureInfo.InvariantCulture; }}
public static CultureInfo DirectoryCulture { get { return CultureInfo.GetCulture("en-US"); }}
public static CultureInfo DeveloperCulture { get { return CultureInfo.GetCulture("en-US"); }}

The UserFormatCulture is the culture used to format numbers, dates, etcetera for display to the user, or to parse information entered by the user. The UserLanguageCulture is used to look up resources in resource files. These two properties simply map to the built-in properties on the current thread used for the same purpose. The DatabaseCulture and DirectoryCulture provide the culture used to read and store data in external systems. This may be en-US or InvariantCulture depending on the system you are using. This was one of the biggest areas our developers made mistakes before we started using this pattern. We had data stored in many cultures and when we read data out of the directory server we'd get parse errors when dates weren't stored in dd/mm/yyyy format of the user's culture. Using this pattern makes it easy for even junior developers to use the right culture, and if you ever need to change, you have a centralized place to make the change.

A more robust implementation could store this information into a config file rather than hard code it in each property. While this still leaves the properties themselves hardcoded, it's a little more flexible because the values can be changed.  Still, you might have to make a change to a core component if you decide that you need 

The next step would be to get rid of the class all together and use something more akin to a service locator. 

var devCulture = ServiceLocator.Resolve<CultureInfo>("DeveloperCulture"); 

A Culture is kind of a service, right?  It provides services for formatting, so I think we're in the clear calling it a service.  It would be nice if it implemented some interface that we could use, but it works.  The service locator model gives us a little more flexibility and allows us to add new intents without changing our core framework.  We can also build some rules into the service locator about fallback cultures, if a requested intent is not configured.

I tend to lean towards Unity as my container, factory, service locator of choice, but you could use whatever you wanted.  In Unity, I could register some well known types like the DeveloperCulture at startup, but we'd need a static factory extension to support getting the UserFormatCulture and UserLanguageCulture. That's because we want to get what the CultureInfo.CurrentUICulture is right now, not what it was during application startup.

Right now I've only implemented the simple solution in the toolkit, but I'll be moving towards the more dynamic solution when I get more time.

Skip to main content