Custom ASP.NET Server Controls and Language Localization
One of the products my company sells is an ASP.NET server control called SmartChart that generates OrgCharts from hierarchical data sources. Up until recently the vast majority of our sales were to companies in the US so we never worried much about language localization support. In the back of my mind I always knew we’d need to get to that, but always put it at the bottom of the feature list since it wasn’t overly exciting. A few days ago we had a customer from France contact us and mention that they really needed to localize some of the strings we display in the control such as Edit, Delete, Insert into other languages for their clients. I decided it was time to make the update.
There are a lot of articles on localization so I’m not going to explain the overall concept in this post. Check out the following links if you’re new to the topic and want to know more:
- http://quickstarts.asp.net/QuickStartv20/aspnet/doc/localization/localization.aspx
- http://www.west-wind.com/presentations/wwDbResourceProvider/introtolocalization.aspx (excellent article by my good buddy Rick Strahl…he’s created some great localization code)
Creating Satellite Assemblies
The first solution that I tried to add localization capabilities into the server control was to use the built-in satellite assembly functionality in .NET. I had worked with these in the past so I started with something I knew. Satellite assemblies allow language resources to be deployed separately from an application’s main .dll providing more flexibility. Here are the basic steps to create a satellite assembly:
- Create a file named Resource.fr-FR.resx and add the appropriate keys and values for the localized strings (French in this case). .resx files can be created by adding a new Resource File item into a project.
- Add the keys and values into the .resx file. In this case the values would be in French. The keys will always be the same across different language .resx files.
- Run the following command to create a resources file:
- resgen Resource.fr-FR.resources
- Run the following command to create a satellite assembly from the .resources file:
- al /t:lib /embed:Resource.fr-FR.resources,SmartWebControls.Localization.Resource.fr-FR.resources /culture:fr-FR /out:SmartWebControls.SmartChart.resources.dll
- Create a folder under your Website's bin named fr-FR (the folder is named after the target culture)
- Copy SmartWebControls.SmartChart.resources.dll created in Step 4 into the fr-FR folder
Once the satellite assembly is ready you can use .NET’s ResourceManager class to get to individual keys and their associated values based on a specific language culture (English, French, German, etc.). Problem is, the test satellite assembly I created didn’t work even though I knew all of the steps were being followed properly. The localized language strings were never read for some reason.
After playing around with it more I realized that the problem was due to strong names. Because the server control assembly was signed with a strong name key file and the satellite wasn’t (the customer would create the satellite assembly and we weren’t going to give out our strong name key file to be used for signing) it wouldn’t work properly. Turns out that both the satellite assembly and the main application assembly have to signed by the same key…which makes sense. After figuring out the problem I realized that satellite assemblies weren’t going to get the job done in this case. On to some other more simple options….
Using HttpContext.GetGlobalResourceObject
I considered building my own resource solution specific to my control but after thinking through the options more I realized that the simplest solution would be to leverage the HttpContext object’s static GetGlobalResourceObject method along with ASP.NET’s support for .resx files. GetGlobalResourceObject allows you to pass in the resource class name (for SmartWebControls.SmartChart.fr-FR.resx the class name would be SmartWebControls.SmartChart) followed by the key that you’d like to retrieve from the resource.
If the customer supplied a localized resource file for a particular culture such as fr-FR or de-DE and placed it in an ASP.NET folder named App_GlobalResources, I would read in the appropriate key value and use it in the server control. If they didn’t I’d read in my embedded resource values from the default Resources.resx file embedded in the control’s assembly. An example of what the default Resources.resx file looks like in Visual Studio is shown next:
The simple wrapper class named ResourceManager that checks for a customer resource file and resorts to the default file if nothing is found is shown next:
using System; using System.Collections.Generic; using System.Text; using System.Web; using System.Globalization; namespace SmartWebControls.Localization { internal class ResourceManager { const string RESOURCE_BASE = "SmartWebControls.SmartChart";
internal static string GetValue(string key) { CultureInfo culture = CultureInfo.CurrentCulture; //First try to load resource value from App_GlobalResources in case user is localizing control object val = HttpContext.GetGlobalResourceObject(RESOURCE_BASE, key, culture); if (val != null) { return val.ToString(); } else { //If no value is found then load it from the embedded resource file (Localization/Resource.resx) return Resource.ResourceManager.GetString(key, culture); } } } }
To retrieve a localized value I can call the ResourceManager’s GetValue method. The following example shows how to retrieve the localized value for the SaveText key:
saveSpan.InnerText = ResourceManager.GetValue("SaveText");
I’m still playing around with it and may ultimately change how it works, but going this route allows customers to use standard .NET .resx files to define localized resources and allows me to access the resource strings easily without writing a custom resource solution. A definite win-win situation.