Dynamic change of localization by Anton


There are many ways to localize dynamically, 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.

Here is 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 ObservableResources:

public class ObservableResources : 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 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. 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. 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 CultureChanged;
public event EventHandler 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 = new ObservableResources(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()));
}
}

It’s elegant and simple to use.