Localizing multiple sites


This has been a really fun week. After spendin a week at Microsoft (MVP Summit), I came back to attempt to get my sites together. I found that there were some changes that forced me to do a bit of a big bang integration and my QA guy was busy on other work. Oh, the joys of working at a startup.

Localization 101

I have done little localization exercises in the past, but nothing of this sort. I have two live sites that essentially share the same code base. In an ideal world, they should share 100% of the business logic (via business libraries) and the pages should be fairly similar with CSS and master pages being the primary changes. This is not an ideal world.

First, let me explain the setup, and then much of this will make sense.

We control two production sites right now, called My Jaguar Watch and My Land Rover Watch. These sites are used as vehicle recovery and tracking sites.

Step 1: Move config elements to the config file

This should have been done when we were building the site, but it was a bit impossible moving all of the dev team onto a unified model when we were making up much of the model as we were coding. As it stood, a lot of the elements that change between the two sites were hard coded. This meant you could not copy and paste code from one to another and simply send out a new build. You had to make sure the page did not have key words in it that should not appear on the other site. In addition, the hard coded bits meant we could not share the code files in our source repository (SourceSafe at this time), which meant code could get out of sync. Ouch!

I quickly found that I could move the site name and the car make to the config files and use the ConfigurationManager object to pull these values. I did the same for customer and dealer support numbers, which made the code fairly neutral. I could now share the code behind files without issue.

Step 2: Figure a method to use tokens in the page

While I could have done this a variety of ways, I wanted a control that would automatically check to see if it needed to be "de tokenized" and automagically do the work. To handle this, I made a control that inherited from label I called a DeTokenizedLabel. Yeah, the name was stupid, but it did the work necessary. The method necessary to get this working properly was RenderControl:

        
        public override void RenderControl(HtmlTextWriter writer)
        {
            //Check if there are tokens to replace
            if (base.Text.IndexOf('{') > -1)
            {
                HttpContext context = HttpContext.Current;
                Dictionary dict = (Dictionary)context.Application["dictionary"];

                base.Text = TokenController.DeTokenizeControl(base.Text, dict);
            }  

            base.RenderControl(writer);
        };

Fairly simple. I will likely refine it before it goes out to production. When I created the second class, I pushed the actual "de tokenizing" method into its own class.

    public static class TokenController
    {
        public static string DeTokenizeControl(string text, Dictionary dict)
        {
            if (dict == null)
            {
                //Get dictionary
                HttpContext context = HttpContext.Current;
                dict = (Dictionary)context.Application["dictionary"];
            }

            foreach (KeyValuePair pair in dict)
            {
                if (text.Contains(pair.Key))
                {
                    text = text.Replace(pair.Key, pair.Value);
                }
            }

            return text;
        }
    }

The check for the dictionary may seem like a bit of overkill (perhaps it should be removed from the classes?), but it served another purpose later. Now, there is one flaw in the program thus far. Did you spot it? If not, the control is tightly coupled to a dictionary object that contains replacement text. If you do not load the application dictionary, the page using these controls will blow up. It’s okay, in this instance, as I am not making this available for human consumption, so I will fix it in a later refactor, along with the duped retrieval of the dictionary.

Now, I can set a controls text to something like:

Welcome {Make} owner to {SiteName}

and have it render

Welcome Jaguar owner to My Jaguar Watch.
Welcome Land Rover owner to My Land Rover Watch.

Page Title Tokens

This one stumped me a bit, until I had a bit of time away from the application. Since all pages needed site name, I had to have a solution that worked on all pages. Options:

  1. Load into the master page. This is doable, but a bit too messy for my liking.
  2. Add code to every Page_Load – with 55 pages times two sites, this was cumbersome
  3. Make a WatchSitePage that inherits from System.Web.UI.Page

I opted for number three. Since I already had my detokenize method in the TokenController class (yeah, the names still suck), I had the info I needed. I will have to experiment on which method to use, but here is the basic code for the new base class for my site pages.

public class WatchSitePage : Page
{
    protected override void OnLoadComplete(System.EventArgs e)
    {
        this.Title = TokenController.DeTokenizeControl(this.Title, null);
    }
}

The DeTokenizeControl method already handles the creation of the dictionary. New idea, make dictionary static once loaded. Okay, back to our regularly scheduled program.

Localizing the site

I started trying to localize with the standard ASP.NET 2.0 method. If you have not done this, it is fairly simple. First, you have to make sure all of your text is in controls. If you do not want to use a bunch of labels (render to <SPAN> in text, so labels are okay for all but the purist ), you can use the localize control. I have not seen what this renders to, so don’t ask … yet.

To localize with local page resources, you open a  page and then choose Generate Local Resources from the Tools menu.  This will create a set of tags for your page. You can then use meta:ResourceName to use that particular resource tag. For example, suppose you have this particular setup:

     <asp:Label runat="server">Label1</asp:Label>
     <asp:Label runat="server">Label2</asp:Label>

You now create resources and a resource is created called Label1.Text for which you set the text to "Foo" and one for Label2.Text which you set to "bar". The XML in the resource file looks like this:

 
<data name="Label1.Text" xml:space="preserve">
  <value>Foo</value>
 </data>
 <data name="Label2.Text" xml:space="preserve">
  <value>Bar</value>
 </data>

You then can set the text to the localized resource (named pageName.aspx.resx for the default langauge) in the labels.

     <asp:Label runat="server" meta:resourceKey="Label1">Label1</asp:Label>
     <asp:Label runat="server" meta:resourceKey="Label2">Label2</asp:Label>

So far, this is the basic stuff you can get off of the QuickStarts for ASP.NET. Notice that the meta:Resource key does not require that you include the .Text, as you can set multiple values for a single resource using the same resource file. There is an equivalent with the explicit syntax, which is <%$ Resources:Label1.Text %>. Note that you will need the .Text for explicit naming.

External Resource Files

External resource files are a bit more difficult, but fortunately Michèle Leroux Bustamante has already solved the problem. I used her CustomResourceProviders project, in particular the ExternalResourceProvider classes. From here, I was able to use the following syntax to set up the title:

<%@ Page Language="C#" AutoEventWireup="true" 
    CodeFile="Default.aspx.cs" Inherits="_Default" 
    Title="<%$ Resources:WatchSiteResources|PageTitles, _default %>" %>

I am awaiting the need to fully internationalize the site name from My Jaguar Watch to the equivalent in all languages. This will make simple token replacements a bit more complex, but I can do it by having a nested localized hierarchy instead of a dictionary.

Until next time!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: