Thursday, September 11, 2008

Translate your blog

A startup company that presented at TC50 offers to publish your blog in different languages. I guess it saves foreign language speakers from having to run it through Google translate or Windows Live Translator for themselves and your blog would be indexed in other language so more likely to be found. Perhaps I should try it out...

TC50: AlfaBetic Translates Your Blog For A Worldwide Audience, Free of Charge

Thursday, September 4, 2008

Localizing text in Javascript

How do you make text that is rendered via Javascript localizable? Here I'll share how I solved this on a recent project.
Here's some Javascript code embedded in a page:
<button onclick="alert('You got me!');">Click me</button>

Changing the button label to localizable text can be done by turning this button into a server side control:
<asp:button onclick="alert('You got me!');" text="<%Resources: Button_ClickMe %>" runat="server"/>

Where the button label is now defined as a (page local) string resource called Button_ClickMe. We still need to put the "You got me!" text into a string resource and pass that to the Javascript code some how.


Fortunately ASP.Net provides some handy functionality in the ScriptManager class that allows injection of code into the generated page. The ScriptManager.RegisterExpandoAttribtute is particularly useful - it allows any arbitrary attribute on any DOM object to be assigned a value. I decided to make use of this to store localizable strings as attributes on an object in the DOM.
Because I want to be able to use this functionally across multiple pages, I have defined a new base page, GlobPage (derived from System.Web.UI.Page) for all of my localizable pages to inherit from. My GlobPage class adds an empty Div control to the page to act as the placeholder for storing localized strings.
protected override void OnInit(EventArgs e)
{
LocTextContainer = new HtmlGenericControl("div");
LocTextContainer.ID = "LocalizedTextContainer";
Page.Controls.AddAt(1, LocTextContainer);
base.OnInit(e);
}

GlobPage also defines the following function to add localized string values to the LocalizedTextContainer Div
protected void RegisterJavascriptLocalStringResource(string key)
{
Page.ClientScript.RegisterExpandoAttribute("LocalizedTextContainer", key, (string)GetLocalResourceObject(key));
}
In the code behind of my page that holds the "Click Me" button I add the following to the Page_Load() method:
RegisterJavascriptLocalStringResource("Button_Response");

This puts the localized text that is defined on the Button_Response string resource into an attribute called "Button_Response" on the LocalizedTextContainer div in the generated page.
It can then be accessed from Javascript by simply using the expression LocalizedTextContainer.Button_Response as in the following:
<asp:button onclick="alert(LocalizedTextContainer.Button_Response);" text="<%Resources: Button_ClickMe %>" runat="server"/>
So don't be afraid to use Javascript in a globalized web application - it can be done!

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?
e.g.
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.