Dynamic Localization in Silverlight

26 Comments June 21, 2010

Localization is (and has always been) a hot topic in Silverlight. There are many ways to do it, but most solutions that I've seen use some variation of the technique described in the Silverlight documentation, which puts localization resources in RESXes and uses data binding to bind XAML elements to localized resources. It works, but it has always left a bad taste in my mouth. For one thing, all the satellite assemblies built from the RESX files are packaged in the application's XAP file, meaning the XAP can grow quite large. That's wasteful, because for a given user, you probably only need one of those satellite assemblies. (A user who prefers to see content in French probably has no need to see it in Manadarin Chinese, too.) For another, you often need the ability to switch between languages at run-time so you can present a list of language choices to the user and immediately switch to the language they selected. Finally, Visual Studio suffers from a long-standing bug that leaves the constructor of the ResourceManager wrapper class it generates marked internal when you change the class's access modifier to public. This means that whenever you modify the primary RESX file, forcing a code regen, you have to manually change internal to public on the constructor in the generated code. It beats me why this hasn't been fixed after all these years, but it hasn't.

I came up with a solution that addresses all of these issues and that so far has proven to be reasonably maintainable and robust. It starts with a class that I call ObservableResources:

public class ObservableResources<T> : INotifyPropertyChanged

{

    private static T _resources;

 

    public T LocalizationResources

    {

        get { return _resources; }

    }

 

    public event PropertyChangedEventHandler PropertyChanged;

 

    public ObservableResources(T resources)

    {

        _resources = resources;

    }

 

    public void UpdateBindings()

    {

        if (PropertyChanged != null)

            PropertyChanged(this, new PropertyChangedEventArgs("LocalizationResources"));

    }

}

The inspiration for this class came from a blog post by Tim Heuer. The idea is that instead of binding XAML elements to the ResourceManager wrapper generated by Visual Studio, you bind them to an object that wraps the wrapper (whose class name comes from the names of your RESX files and is passed to ObservableResources as a template parameter) and implements INotifyPropertyChanged. Now you can change the culture at run-time and call ObservableResources.UpdatingBindings to update the binding targets. A side benefit of wrapping the wrapper is that it eliminates the need to change the Visual Studio-generated wrapper class’s constructor from internal to public. (Now if only Silverlight would allow you to declaratively instantiate generic types. Grrr. Because you have to instantiate ObservableResources programmatically, you lose design-time support.)

The second part of the solution is a helper class named LocalizationManager. I won’t post the source code here, but LocalizationManager exposes a simple API for changing cultures. If necessary, it downloads external XAPs containing localization resources and loads them into the appdomain so ResourceManager can find them. My implementation has intimate knowledge of which localization resources are stored in which XAPs, but you could easily build a more generic version that works in any application. LocalizationManager’s API looks like this:

public event EventHandler<CultureChangedEventArgs> CultureChanged;

public event EventHandler<CultureChangedErrorEventArgs> CultureChangeFailed;

public void ChangeCulture(CultureInfo culture);

To demonstrate the ObservableResources/LocalizationManager approach to localization, I built a sample app with this in MainPage.xaml:

<Grid x:Name="LayoutRoot" Background="Green">

  <StackPanel Orientation="Vertical" VerticalAlignment="Center">

    <TextBlock Text="{Binding LocalizationResources.Greeting}"

      Foreground="LightYellow" FontSize="72" FontWeight="Bold"

      HorizontalAlignment="Center" VerticalAlignment="Center">

      <TextBlock.Effect>

        <DropShadowEffect BlurRadius="12" ShadowDepth="12" Opacity="0.5" />

      </TextBlock.Effect>

    </TextBlock>

    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">

      <Button Width="120" Height="60"

        Content="{Binding LocalizationResources.EnglishLabel}"

        Tag="en" Click="OnChangeCulture" Margin="5" />

      <Button Width="120" Height="60"

        Content="{Binding LocalizationResources.FrenchLabel}"

        Tag="fr" Click="OnChangeCulture" Margin="5" />

      <Button Width="120" Height="60"

        Content="{Binding LocalizationResources.GermanLabel}"

        Tag="de" Click="OnChangeCulture" Margin="5" />

      <Button Width="120" Height="60"

        Content="{Binding LocalizationResources.SpanishLabel}"

        Tag="es" Click="OnChangeCulture" Margin="5" />

    </StackPanel>

  </StackPanel>

</Grid>

And this in MainPage.xaml.cs:

public partial class MainPage : UserControl

{

    private LocalizationManager _manager = new LocalizationManager();

    private ObservableResources<Resources> _resources =

        new ObservableResources<Resources>(new Resources());

       

    public MainPage()

    {

        InitializeComponent();

 

        // Set LayoutRoot's DataContext to specify binding sources for child elements

        LayoutRoot.DataContext = _resources;

    }

 

    private void OnChangeCulture(object sender, RoutedEventArgs args)

    {

        // Change the culture, and then rebind

        _manager.CultureChanged += (s, e) => this._resources.UpdateBindings();

        _manager.CultureChangeFailed += (s, e) => MessageBox.Show(e.Error.Message);

        _manager.ChangeCulture(new CultureInfo((sender as FrameworkElement).Tag.ToString()));

    }

}

When the app starts up, you see this:

Dynamic Localization (English)

And when you click the Spanish button, you see this:

Dynamic Localization (2)

The application XAP contains a single RESX file (Resources.resx) containing strings for the default language (English). Strings for other languages live in a separate XAP named ExternalResources.xap. Clicking one of the language buttons executes a call to LocalizationManager.ChangeCulture, which does the following:

  • Downloads ExternalResources.xap if it hasn’t been downloaded before, and sets a flag to prevent it from being downloaded again
  • Extracts all satellite assemblies from the XAP and loads them into the appdomain
  • Changes the culture by assigning the specified CultureInfo to the CurrentCulture and CurrentUICulture properties of the current thread
  • Fires a CultureChanged event to let you know that the culture has changed

My CultureChanged event handler calls UpdateBindings on the ObservableResources object, forcing the bindings that provide data to my TextBlock and Button elements to be reevaluated. Thus, the UI updates automatically.

That’s the crux of the solution, and, of course, there are details that I haven’t covered. I didn’t make my helper classes thread-safe, because I’m assuming that they’ll only be called from the UI thread. If you’d like to see the entire solution, you can download it from Wintellect’s Web site. It’s not the final word on Silverlight localization (not by a long shot), but it’s a big step in the right direction.


26 Comments

  • Gravatar Image
    Twitter Trackbacks for Jeff Prosise's Blog : Dynamic Localization in Silverlight [wintellect.com] on Topsy.com June 21, 2010 1:36 PM

    PingBack from http://topsy.com/www.wintellect.com/CS/blogs/jprosise/archive/2010/06/21/dynamic-localization-in-silverlight.aspx?utm_source=pingback&utm_campaign=L2

  • Gravatar Image
    Christopher Maduro June 24, 2010 10:27 AM

    Really great! But why do I get increasing calls to the OnPropertyChanged event handler everytime I click a button?

  • Gravatar Image
    Christopher Maduro June 24, 2010 11:55 AM

    How can I debug this, namely, detect if th satellite assembly has loaded correctly into the application?

  • Gravatar Image
    Christopher Maduro June 24, 2010 11:58 AM

    Also, I removed the ObservableResource class, and instead I use the ResourceWrapper from the SilverlightBusinessApplication template. I then trigger the propertychange on all the resources. This gives me back the design time support. This works in your application. But when I try to implement it in the SilverlightBusinessApplication template it stops working. Clueless....

  • Gravatar Image
    jprosise June 24, 2010 10:04 PM

    If the assembly fails to load, LocalizationManager fires a CultureChangeFailed event and e.Error contains an Exception telling you what went wrong.

    ResourceWrapper works at design time because (correct me if I'm wrong) it's not a generic class. If you're willing to hardcode the wrapper class, you can get design-time support back. But then if you change the name of your resource files, you break. :-(

  • Gravatar Image
    Piyush July 12, 2010 5:00 AM

    Hi,
    Thanks for sharing this, I tried re-creating the same solution in SL3 but for some reason the language does not seem to change. Could you post a VS2008/SL3 solution?

  • Gravatar Image
    jprosise July 13, 2010 1:41 AM

    It works in SL3, too. You probably failed to manually modify the CSPROJ file to specify the supported cultures. See the CSPROJ file in my ExternalResources project. (Open it with Notepad.) Make sure you add this:

    fr,de,es

  • Gravatar Image
    Roger July 24, 2010 5:34 PM

    Your solution seems a good idea. I downloaded your code and it runs well on my computer. Howevery, I tried to created a separate project using the same approach, I coud not get different language to display. My project seems load ExternalResouces.xap and changes the culture without errors. But the screen still display en culture strings after each time the buttons for other languages are clicked. I am clueless... I created a SL4 project using VS2010 in my case.

  • Gravatar Image
    Rochelle August 7, 2010 8:43 PM

    Had the same problem as Roger :(

  • Gravatar Image
    amyo August 11, 2010 2:45 AM

    This is great approach of localization.
    I faced some problem to implement it.
    Solution of the problem below (so that someone can implement):

    *When adding resource file in your own project follow the steps here:http://msdn.microsoft.com/en-us/library/dd941931%28v=VS.95%29.aspx

    *Make sure that your ExternalResources project have the same assembly name and namespace. (you can change this from the ExternalResources project property application section)

  • Gravatar Image
    Matthew September 3, 2010 10:56 AM

    Great solution, the issue I am facing in using this solution is that you need all resx files to come from an assembly with the same name, otherwise they will not be loaded correctly. Have you any ideas of how to load loose resx files into an assembly?

  • Gravatar Image
    Mahmoud Ahmed Arafa February 27, 2011 7:53 AM

    That's very Great, Thanks to you jprosise and also amyo

    and all other people

  • Gravatar Image
    Ivan turinov March 24, 2011 5:59 AM

    I am using frames (MainPage > many childpages on the frame).
    I change localization on MainPage, but objects on childpages is not updated.

  • Gravatar Image
    mridul saurabh April 13, 2011 4:35 AM

    great solution.
    I appreciated and followed !!!
    thanks and cheers :)

  • Gravatar Image
    Chiranjeevi V April 21, 2011 11:15 AM

    Hi,

    This was a good solution.

    But, this was working only for the static resources.

    In my case, i need to load the resources dynamically at runtime, as i don't know the Resource string i need to get from the satellite assembly at the compile time.


    Can anyone help me on this please.. Thanks in advance..

  • Gravatar Image
    Mike Taulty's Blog April 26, 2011 5:35 AM

    Note: these are early notes based on some initial experiments with the Silverlight 5 beta, apply a pinch

  • Gravatar Image
    Torulf June 6, 2011 5:50 PM

    Hi Jeff!

    This solution works great but I have one question,

    You mention above that it is possible to get design time support back if you hard code the resource wrapper class.
    I've tried this to no success. Would you mind share an example on how to get design time support back by hard coding the resource wrapper?

    Thanks!

  • Gravatar Image
    NewDeveloper September 7, 2011 10:34 AM

    I like your solution and it works great.
    I have a solution file with multiple projects in it.
    I would like to use a different resource file for each of the Different Projects
    Because the resource files get huge.
    I tried to add another Resource File under the localization folder and it won't read the Resources2.resx?
    Any suggestion of how to make this work?

  • Gravatar Image
    NewDeveloper November 7, 2011 4:22 PM

    I just posted a question regarding this post in this URL:
    http://stackoverflow.com/questions/8042759/resource-wrapper-for-dynamic-localization-in-silverlight-4

    Hope Jeff Prosie will anser this?

  • Gravatar Image
    Ashish December 5, 2011 10:46 AM

    What could be the reason why this is not working in MVVM?

  • Gravatar Image
    Casper March 1, 2012 9:43 AM

    I can't get this solution work for images. Anyone tried this?

  • Gravatar Image
    Joseph May 15, 2012 3:01 PM

    We integrated this into our solution, but we had the same issue as Roger and Rochelle. The problem in our situation was an Assembly Version discrepancy in our AssemblyInfo.cs file. We changed the new AssemblyInfo.cs file to match our current AssemblyVersion and AssemblyFileVersion and that got it working!

  • Gravatar Image
    Pulsman July 5, 2012 3:47 AM

    Works great for framework elements, but not in some templates like Datagridcolumn or datafield in dataform... any idea??

  • Gravatar Image
    mikeloguay August 28, 2012 1:19 PM

    Hi Jeff (and all of you),

    Congratulations for the post. It works fine on most of the cases, but I have an issue, hopefully anyone has solved the same problem.

    I have set the neutral culture to Spanish ("es") in the main project (DynamicLocalizationDemo), and added the culture to English without any region ("en") as a new .resx in the ExternalResources project.

    The problem is that, when I change the culture to "en", it doesn't use the resources located on the satellite assembly, it uses the ones in the main one (i.e. neutral ones), in that case the Spanish. If I use "en-GB" instead of "en", it works fine.

    In "SupportedCultures" tag I have the following:fr,de,es,en-GB,en

    Do you have any clue? Thanks in advance!

  • Gravatar Image
    Mike September 14, 2012 3:42 PM

    Was looking for a WPF solution, found this, and it works great. Thanks Jeff!

  • Gravatar Image
    Arun October 29, 2012 9:09 AM

    Hello Jeff,
    Your solution worked wonderfully well but it had a problem when run on a Windows 7 machine. When all language settings point to French including keyboard, install language, everything it fails to change to french.

    Any idea why this happens and how it should be handled.

Have a Comment?

Archives

Tags