Wednesday, September 3, 2008

Persisting globalized messages

This is the first in what I plan to be a series of tips and techniques related to globalization of applications. Let’s dive in…

Lots of applications not only show messages to the user, but want to persist them for some reason or other. Say you have a system that logs various messages to a database. These messages are later shown to a user looking at the log.
e.g. “Record X was changed by Y on dateZ”.

This may be written out by code such as:
Log(String.Format(“Record {0} was changed by {1} on {2}.”, recordName, userName, changeDate.ToString()));

If you are building a globalized application (i.e. one that can operate in multiple languages and cultures), you obviously don’t want to go writing out English text like that. So you do the obvious thing and refactor the localizable text into a string resource, right?
Msg_RecordXChangedByYOnZ: “Record {0} was changed by {1} on {2}.”

This can then be simply translated to other languages by localizers. You then change your code to something like this:
Log(String.Format(GetResource("Msg_RecordXChangedByYonZ"), recordName, userName, changeDate.ToString()));

(GetResource here is an arbitrary method for fetching a localized string resource given a key.)

The problem now is that you are still writing a language specific string to the log (probably in the language of the user who caused the record change) but you don’t know the language of the user who is later going to be looking at these messages. Imagine the viewer's confusing at seeing messages shown in a variety of languages rather than just in their language of choice. You need some way of persisting this message in a language neutral format and then at display time (i.e. when you want to render the message) convert it to localized text in the appropriate language. Basically you need to persist the key of the string resource, and the parameter values. To solve this I defined some simple XML markup to store the information we need, to be able to reconstitute the message.

<Resource key="keyname" types="type1,type2,type3" values="val1,val2,val3"/>

The Values attribute contains a comma separated list of values to substituted into the localized string referred to by the Key attribute. (A comma may not be a safe separator character, but it’ll do for our purposes here.) Note that to be able to render the values correctly, we need to know what type they are (e.g. string, integer, float, date).

So then our code becomes:
Log(string.Format("<Resource Key=\"Msg_RecordXChangedByYOnZ\" Values=\"{0},{1},{2}\" Types=\"string,string,date\"/>", recordName, userName, changeDate.ToString()));

Now what we get persisted into the log will look something like this:
<Resource key="Msg_RecordXChangedByYOnZ" types="string,string,date" values="ProductXyz,Freddie,8/21/2008" />

Rendering this back to a language specific message for display to a user in their own language is now a matter of parsing the key name, values and types out of the XML, formatting each value according to its type and then putting it together with a String.Format. (I’m not showing the code for parsing and formatting the values here – I’ll leave that as an exercise for the reader.) So, to render the message:
String.Format(GetResource(key), formattedValues);

If you’ve been paying close attention you may have noticed there is still one issue to be fixed. The date we persisted to the log above is in whatever format was in use when the entry was being written (e.g. the current user’s culture format). We need to ensure that we persist it in a culture-invariant form so that it will be parsed correctly at render time.

The corrected code:
Log(string.Format("<Resource Key=\"Msg_RecordXChangedByYOnZ\" Values=\"{0},{1},{2}\" Types=\"string,string,date\"/>", recordName, userName, changeDate.ToString(CultureInfo.InvariantCulture)));

This approach proved very useful on a recent project where I found a lot of plain text messages being logged to a database. Now those messages can be viewed in the readers language of choice. A key benefit of this approach is that it doesn’t require any database schema changes to accommodate a wide variety of messages. Hopefully this technique will prove useful to someone else out there also.

No comments: