Last time I introduced a candidate approach for integrating localization into FubuMVC, but I ran out of steam before I could talk much about the mechanics or more of the rationalization for the approach. To close it out, I want to talk about the mechanics, the justifications, some open questions, and a little bit about how Dovetail does localization. For the moment, the code in question is on GitHub here.
Forgetting about how exactly the localization data is stored, the key abstraction for retrieving localization data that will be consumed by FubuMVC is just an interface called ILocalizationProvider with these methods:
There are only two important methods (HeaderFor() is overloaded). The first method is for looking up a string based on the StringToken passed in. Why not just “string TextFor(string key)” you might ask (and should)? Early on we decided to make the localized string a first class object in its own right and I think it’s paid off. I’ll give you a couple reasons and see if any of them resonate with you:
- By making the localized string name a class instead of a primitive string for the key, we can embed more information with the StringToken. In our case, we define the StringToken with a default text value. If the localization provider doesn’t recognize that particular StringToken (and the current culture is en-US), the localization provider can just return the default text. It’s perfect in a way because you can drop in the placeholder for the localized string without having to duck off into resx files, or the database, or anything else.
- Scanning an assembly for localized strings. We can easily seed our database tables for localization by scanning over the types in our assemblies, then finding every StringToken class, and making sure there’s an entry in the database for each unique StringToken field. At Dovetail, we have command line utilities that we can use to find and enter any gaps in our database tables for localization by scanning over the assemblies in our solution.
- Because it’s just convenient to go: <% =UserMessageKeys.SOMETHING%> and its ToString() method does the localization right there and then.
- One of the underlying thoughts behind FubuMVC is the belief that magic strings are, if not evil, at least undesirable.
The second method on ILocalizationProvider is the HeaderFor() method that looks up information about a given property and returns this object:
I don’t see users consuming the ILocalizationProvider.HeaderFor() method very often, but it’s a necessary foundational piece to conventional html construction like the call to LabelFor(x => x.SomeProperty).
Finally, for convenience there’s a static facade over localization I’m calling Localizer for the moment:
But Statics are Evil!
Well, yes, they can be and often are – but let’s remember why and when statics can bite you in the private parts. Statics can (and often do) kill you because of tight coupling or static state that gets carried around between automated tests. In the case of Localizer, it’s our belief that the convenience of being able to say MyKeys.STATUS.ToString() or Localizer.HeaderTextFor( a property ) outweigh the potential negatives here. In this case, the ability to swap out the way it obtains the underlying ILocalizationProvider leaves the Localizer decoupled from the exact implementation of the localization storage and gives you an easy way to stub out localization for testing scenarios.
In our application, we store localization strings in a database table. It’s more or less necessary for us because we allow administrators of the system to edit a subset of the localized data and we also depend on the localization for customization. I did an informal poll on twitter a couple months ago about how people stored localization data and the results were about 3 to 1 in favor of using the database compared to storing the data in the built in resx capability in ASP.Net. Again, it’s not scientific, but it probably is representative of the folks who would consider using FubuMVC. We’re not saying that you can’t use the resx scheme with this proposed solution, but it’s definitely optimized for something other than resx’s and satellite assemblies.
Other points of (maybe) interest:
- If there isn’t a localized string matching a request, we try to use the default text of a StringToken. Failing that, the localization returns a fake value like “en-US_Status” as a placeholder. It’s not something we allow into production, but in development it helps you find missing localized strings through simple inspection.
- We use some command line tools to scan through the application assemblies and find any new properties on Entity objects that are missing header information and any new StringToken fields that don’t have either default text or an entry in the database table. We have other command line tools to dump the state of these tables into Xml for the purpose of source control and also a tool to import these files back into the database as part of our standard build.
I do think the approach I’m presenting here is definitely geared towards storing localized text information in either the database or your own files rather than .Net’s built in resource file solution. I did an obviously informal poll on Twitter a month or so back and it came back nearly unanimous that folks were not using the resx files for localization. That may be simply a statement of how my twitter followers skew in their opinions, but those are also the folks most likely to use FubuMVC anyway.