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:
And when you click the Spanish button, you see this:
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.