Google Website Translator Gadget

Monday, 11 April 2011

Pragma On Key Silverlight Localization

Globe

One of the big features that we are adding to Version 5.4 of On Key, Pragma’s Enterprise Asset Management System, is the ability to use the system in different languages.  It seems like the first language we will support in addition to English will be Brazilian Portuguese  to support customers for our Pragma Brazil service company.  So I’ve been spending some time during the past week or two looking at the various aspects of On Key that we need to localize to determine the best solutions for getting everything localized.  In this post I will cover some useful information I discovered in my research.

Static Text

Fortunately we’ve been following the recommended best practice for localizing Silverlight applications by storing all the text that needs to be localized in external resource files.  We currently have around 290 resources files with around 8200 strings that need to be translated from English into other languages.  The resources stored within these files are all static type resources like menu text, captions, tooltips, error messages etc.  I was looking for a solution that would make the process of managing and translating these strings as easy as possible.

I’ve been keeping an eye on Amanuens since I first read about their localization solution back in 2010, so I headed off to their web-site to see how it has matured since the early beta phases.  Amanuens gives you the ability to upload your resource files into the cloud to get translators to translate them into your language of choice.  You can assign your own people as translators or even get a quote from an external organization to translate your resources into your language of choice.  This is all done via an easy-to-use web-based interface.  The output is the actual translated resource files in a variety of formats of which .NET resource files is one.  Amanuens also has some other really nifty features like a shared Translation Memory, the ability to attach screenshots to provide context for translations and much more.

After creating a public facing Subversion repository containing our resource files, I signed up for a trial to experience the work flow involved in getting some of our resource files translated.  I was quite happy with the usability of the system and it seems to be a great solution for translating our resources into multiple languages over a period of time.  The change tracking capabilities of automatically picking up new strings or changes to existing strings to notify translators of work to be done is a really powerful feature and one that will really simplify the management of the resource files over time. 

Dynamic Text

In addition to the static text, On Key also has a nifty feature called Phrase Translations.  A phrase is defined as a piece of text delimited by the “{“ and “}” brackets. Any customer can create translations for their own phrases via the On Key UI to allow the engineers, artisans and other people using On Key to view certain terminology (like technical terms) in their language of preference. Take for example the phrase {Check} the {Bearing} as the description of a maintenance task to be executed. When an artisan views the description, the text will automatically be translated into their language preference specified at logon, i.e. Inspekteer the Draer if they specified Afrikaans. The technical implementation for these Phrase Translations is actually quite interesting as we utilize the SQLCLR to get the best performance possible, but that is a post for another day.

Dynamically loading Resources

One of the problems with Silverlight localization is that all the resources for the different languages are packaged into the single XAP file that is downloaded to the clients.  Ideally we only want to download the resources for each client’s neutral and language specific cultures.  One solution to the problem is to create culture specific XAP files for every language supported.  When the initial request for the application hits your server, you inspect the client’s culture settings passed via the ASP.NET request and redirect the client to load the relevant culture specific XAP file.  I wasn’t keen on creating separate build configurations for all the future languages that we will support and did some further research into the issue.  I then came across this excellent video presentation by Guy Smith-Ferrier on Internationalizing Silverlight applications.  In the presentation he covers a technique whereby he creates separate XAP files using a MSBuild task that contains only the localized resources and not the complete application.  All of this happens without having to create separate build configurations in Visual Studio.  He then further illustrates how to use MEF to dynamically download the correct culture specific XAP file at run-time when the Silverlight application starts. 

This was more to my liking and after spending some time looking at the source code, I implemented something similar in our environment.  A problem that Guy does not address in his solution is the loading of the culture neutral resources, i.e. it only loads the culture specific resources.  As every CultureInfo instance has a reference to its Parent culture, I set about to create a solution whereby I traverse the culture tree upwards, trying to download a XAP file for every culture neutral Parent resource until I arrived at the Invariant Culture that is compiled into the original application XAP file.  Here is some sample C# code that illustrates how to accomplish this:

1 public App() 2 { 3 Startup += Application_Startup; 4 Exit += Application_Exit; 5 UnhandledException += Application_UnhandledException; 6 7 InitializeComponent(); 8 } 9 10 private void Application_Startup(object sender, StartupEventArgs e) 11 { 12 var options = new NinjectSettings {InjectAttribute = typeof (InjectAttribute)}; 13 IocContainer.Kernel = new StandardKernel(options, new IocInfrastructureModule(), new IocNavigationModule(), new IocServiceModule()); 14 15 DownloadLanguageResources(Thread.CurrentThread.CurrentUICulture); 16 } 17 18 private void DownloadLanguageResources(CultureInfo culture) 19 { 20 string xapResourceFilename = String.Format(CultureInfo.InvariantCulture, "{0}.{1}.xap", "Pragma.OnKey5.Client", culture.Name); 21 22 XapLoader xapLoader = new XapLoader(xapResourceFilename); 23 xapLoader.Completed += (s, args) => 24 { 25 if (culture.Parent.IsNeutralCulture) 26 { 27 // Search for user's region neutral resources 28 DownloadLanguageResources(culture.Parent); 29 } 30 else 31 { 32 RootVisual = new Page(); 33 34 // Set the resource to use for all Telerik controls 35 LocalizationManager.DefaultResourceManager = Strings.ResourceManager; 36 } 37 }; 38 xapLoader.DownloadAsync(); 39 }

You will notice that I’m not using MEF to dynamically download the XAP files, but instead I’m using a custom XapLoader class.  I had two reasons for not using MEF:

  1. I quite new to MEF and I couldn’t figure out how to get MEF to dynamically download multiple DeploymentCatalog instances as I traverse the culture tree.  If a reader knows how to do this, please respond in the comments.
  2. I wasn’t interested in the whole MEF discovery mechanism to wire up modules etc.  I only wanted to download the XAP file.  This might change at a later stage when we start refactoring On Key into a more modular approach, but for now this was unnecessary overhead that I wanted to avoid.

Here is the source code for the XapLoader class.  I cannot seem to find the original link to give the original author some credit, but the code is really self explanatory so I leave it to speak for itself:

 

1 /// <summary> 2 /// XAP file loader to dynamically download and load additional XAP resources at run-time 3 /// </summary> 4 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Xap")] 5 public class XapLoader 6 { 7 private readonly string _xapName; 8 9 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "xap")] 10 public XapLoader(string xapName) 11 { 12 Check.ArgumentNotNullOrEmptyString(xapName, "xapName"); 13 _xapName = xapName; 14 } 15 16 public event EventHandler<XapLoadedEventArgs> Completed; 17 18 #region Methods 19 20 public void DownloadAsync() 21 { 22 Uri uri = new Uri(_xapName, UriKind.Relative); 23 WebClient wc = new WebClient(); 24 wc.OpenReadCompleted += OnXapLoadingResponse; 25 wc.OpenReadAsync(uri); 26 } 27 28 private void InitXap(Stream stream) 29 { 30 StreamResourceInfo xapStreamInfo = new StreamResourceInfo(stream, null); 31 string appManifest = new StreamReader(System.Windows.Application.GetResourceStream( 32 xapStreamInfo, 33 new Uri("AppManifest.xaml", UriKind.Relative)).Stream).ReadToEnd(); 34 35 XElement deploy = XDocument.Parse(appManifest).Root; 36 37 List<XElement> parts = (from assemblyParts in deploy.Elements().Elements() 38 select assemblyParts).ToList(); 39 40 foreach (XElement xe in parts) 41 { 42 string source = xe.Attribute("Source").Value; 43 AssemblyPart asmPart = new AssemblyPart(); 44 StreamResourceInfo streamInfo = System.Windows.Application.GetResourceStream( 45 xapStreamInfo, 46 new Uri(source, UriKind.Relative)); 47 asmPart.Load(streamInfo.Stream); 48 } 49 } 50 51 #endregion 52 53 #region Event Handlers 54 55 private void OnXapLoadingResponse(object sender, OpenReadCompletedEventArgs e) 56 { 57 if ((e.Error == null) && (e.Cancelled == false)) 58 InitXap(e.Result); 59 60 if (Completed != null) 61 { 62 XapLoadedEventArgs args = new XapLoadedEventArgs(); 63 args.Error = e.Error; 64 args.Cancelled = e.Cancelled; 65 Completed(this, args); 66 } 67 } 68 69 #endregion 70 } 71 72 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Xap")] 73 public class XapLoadedEventArgs : EventArgs 74 { 75 #region Properties 76 77 public bool Cancelled { get; set; } 78 public Exception Error { get; set; } 79 80 #endregion 81 }

Here is an example of the code in action when I start up my browser with Brazilian Portuguese as my preferred browser language:

image

Notice how the original XAP file is downloaded and also the two subsequent request for the language specific and language neutral XAP files.  The language specific XAP file (pt-BR.xap) was not found, but the language neutral XAP file for Portuguese was found and the UI is therefore displayed in Portuguese using these neutral resources.

Conclusion

There are obviously a lot of other dimensions to consider when localizing your Silverlight application like right-to-left support, different time zones and much more.  Guy Smith-Ferrier covers a lot of this in his presentation so I highly recommend watching the video and/or downloading the slides.  I might follow up with another post on some of these topics at a later point in time, but for now this is about all I have to say.  Lekker lees Wink

2 comments: