Creating Multilingual Web Parts
Ahalan. Parev. Goddag. Saluton. Bonjour. Guten Tag. Aloha. Hola. Shalom. Hello.
We live in a multi-lingual world. Everyone speaks a different language (unless you’re a slacker like me who barely comprehends the English one) and when we build an uber-cool Web Part (say a new Discussion Forum) that we want the world to use, it needs to support whatever language is out there (yes, including Esperanto).
A lot of SharePoint documentation (including the SDK) talks about implementing the LoadResource method and use the ResourcesAttribute to mark your properties or enum values in order to localize your Web Part properties. This is great if you want to create a property for every string you’re going to display, but what if you don’t want to do that? In some Web Parts, that’s a heck of a lot of Properties. Maurice Prather posted a simple HOWTO about using this method here, but it doesn’t really cover multiple languages for say strings that you would display on your Web Part (and don’t want to create properties for everything).
While it's certainly possible to develop a multi-lingual application with the tools provided with ASP.Net, there are a number of limitation which make the task less than a happy-happy-joy-joy experience for anyone. Some of the key problems are:
- Resource files are embedded into assemblies
- Resource files can't return strongly-typed objects
- Web controls aren't easily hooked up with resource files
While this may seem trivial, the above three issues can be quite serious - with the first being the worst. For example, since resource files are embedded into assemblies, its very difficult to ship a product which provides the client with the flexibility to change simple things like text on the screen. Do you really want to recompile your entire assembly when the translation department wants to change some text, stop the IIS process and copy the .dll into the bin folder (grant you might be able to get away with copying the file and not stopping IIS or the AppPool, but still… hey, walk with me on this).
A Simple Approach
This is a fairly basic approach but accomplishes a few things. First, resources are now externalized from your Web Part assembly(ies) and you can update them anytime you need without recompiling (say for spelling mistakes or you feel a strong desire wash over you to change “Exit” to “Leave”). Second, with some small changes we do here we can support any language automatigically (and fall back to say a default one if we can’t find the one we want).
One note is that you can accomplish multi-lingual Web Parts using a resx file, resgen, blah, blah, blah, blah so this article is an alternative to that. Personally I prefer this, but YMMV.
Create a Web Part
Yes, you know how to do this. Create a new Web Part Library Project using the Visual Studio templates. Let’s give it a name like ItsRainingMen.
Okay, fine. Let’s not so feel free to call it whatever you want. You’ll probably call it something boring like WebPartLibrary1 right? Whatever. Breathe. Move on.
Fields You Need
First we’re going to create a few fields in our Web Part that will help us manage the language and resources. We can always retrieve the current language setting (1033 for English) from our Web Part through the Language Property, but we’re going to put the language value into a Property of our own and let the user override and change it (hint for developers, this is a good thing as you don’t have to do something crazy like change your server settings in order to test other languages, just change a property at run-time and refresh your page). We’ll also create an XmlDocument to hold the contents of the resource file (which is going to be Xml as it’s easiest to implement) from where we’ll retrieve the language strings.
22 private const string defaultLanguage = "1033";
23 private string language = defaultLanguage;
24 private XmlDocument resourceFile;
So create two new fields, one XmlDocument and the other string for the language (with a default of “1033”). If you want to get fancy, this could be a drop down list of all the languages SharePoint supports and you can let the user pick it in a custom tool part. Again, more work than what I want to do here so we’ll make the users enter this manually. Why use 1033? We’ll get to that later.
OnInit
We’ll override the OnInit method in the Web Part in order to load our resource file. This may sound crazy, that we’re loading a resource file every time the Web Part is loaded but it’s a small file and takes less than a tenth of a second to load so get over it. If you really have your knickers in an uproar over it, you can always take a different approach and load the file once, toss it into a Cache and create a cache dependency on the file so if the file ever gets updated, the contents will get reloaded. I’ll leave that as an exercise for the reader.
38 protected override void OnInit(EventArgs e)
39 {
40 try
41 {
42 SPWeb web = SPControl.GetContextWeb(this.Context);
43 DirectoryInfo directoryInfo = new DirectoryInfo(this.Page.Server.MapPath("/wpresources/ItsRainingMen"));
44 FileInfo[] languageFileInfoArray = directoryInfo.GetFiles("*.lng");
45 for (int n = 0; n < languageFileInfoArray.Length; n++)
46 {
47 FileInfo fileInfo = languageFileInfoArray[n];
48 if (fileInfo.Name == (web.Language.ToString() + ".lng"))
49 {
50 this.language = web.Language.ToString();
51 }
52 }
53 if (this.language == "")
54 {
55 this.language = "1033";
56 }
57 this.resourceFile = new XmlDocument();
58 XmlTextReader reader = new XmlTextReader(this.Page.Server.MapPath("/wpresources/ItsRainingMen/" + this.language + ".lng"));
59 this.resourceFile.Load(reader);
60 reader.Close();
61 }
62 catch (Exception exception)
63 {
64 // Do something meaningful with the exception here
65 }
66 }
Our OnInit does a few things here. It creates a DirectoryInfo object for the resource directory for our Web Part and enumerates all the *.LNG files there (you can use any name you want here, but just in case you might be using something common like *.XML I didn’t want to include those files). The SPWeb.Language value for English is 1033 so naming your .LNG file as 1033.LNG means we can use this format for any language and just add new ones as we create them. For a complete list of the language codes search in the SharePoint SDK for LCID. 1033. See. Not just a hat rack.
The LNG files are just simple XML files that contain the strings you want to translate. You can make your language files as complex as you like, but really it just needs a way to a) store a token to retrieve later [using an Xml attribute] and b) store the value to be tranlated. Keeping your file simple will make it easier to edit later (if you’re really adventurous you could build a graphical editor to do translations).
First, heres the sample language file (1033.LNG) that represents the English strings.
1 <?xml version="1.0" encoding="utf-8" ?>
2 <!-- xml resource file for English translations -->
3 <strings>
4 <string id="HelloMessage">Hello</string>
5 </strings>
And here’s the same file, copied and renamed to 1036.LNG which contains the French translations.
1 <?xml version="1.0" encoding="utf-8" ?>
2 <!-- xml resource file for French translations -->
3 <strings>
4 <string id="HelloMessage">Bonjour</string>
5 </strings>
Then OnInit reads in the appropriate XML document using the XmlTextReader class into our private XmlDocument member variable called resourceFile. Now any time we need to access any string, it’ll be available in an XmlDocument so let’s load a string using XPath.
LoadResource
This is the method we’re going to override in order to a) determine what language we should load and b) use XPath to load the string from the language file so whenever we reference a string we’ll have the correct one.
73 public override string LoadResource(string id)
74 {
75 string translatedResource;
76
77 try
78 {
79 translatedResource = this.resourceFile.DocumentElement.SelectSingleNode("/strings/string[@id='" + id + "']").InnerText;
80 }
81 catch
82 {
83 translatedResource = string.Format("Error reading lanaguage ID={0}", id);
84 }
85
86 return translatedResource;
87 }
Again, simple stuff here and you’ll want to handle the exceptions accordingly.
RenderWebPart
Finally, as the simplest example we’ll just print “Hello” to the Web Part. Since we’re using translated strings, we want to display “Hello” in the appropriate language, based on the language from the SPWeb.Language setting or the value we set in the Web Part.
93 protected override void RenderWebPart(HtmlTextWriter output)
94 {
95 output.Write(SPEncode.HtmlEncode(LoadResource("HelloMessage")));
96 }
Cool huh? Anytime you want to reference a string that should be translated, use your string token (in our example it’s “HelloMessage”) and the LoadResource method. It might be a good idea to do something like split up your messages using a token that makes sense like:
- MainForm.FirstNameLabel
- MainForm.FirstNameErrorMessage
- SecondaryForm.GenericErrorMessage
Or whatever naming convention works for you. Make it readable and make it something that makes sense in your code as well as your language file.
Now that you have the framework built into your system, you can just hand out the Xml file with all the tokens and the values needed for translation and have people do your work for you (that’s the part I like). The LNG file can just be copied into the wpresources directory for deployment and a switch of a property in the Web Part will result in your entire Web Part/Application/etc. translated to whatever language you want (I personally really want the Swedish Chef language file for my Forums Web Part done first).
Now you can a) support multiple languages in your Web Parts b) add a new language just by creating a file and c) update any changes or perform translations on strings whenever you want without taking down your portal, restarting your AppPool or some other silly thing. Not a lot of code to implement it either.
P.S. I have to thank Steen Molberg and his BlogParts Web Part for the idea about the langauge files.