in

Dé specialist in .NET trainingen en consultancy

Thomas Huijer

Easy custom localization in WPF

One of our customers wanted the application that we're working on to be localized in different languages. Fortunately all that was required was localization of texts and not of positions, heights or sizes. But an important requirement was also that changes needed to be made easily by people without a degree in software-engineering, i.e. users who were able to use Office, but not much else. So we decided against the use of LocBaml and see if we could come up with a simpler approach. Here's what we came up with:

  1. we're using an Excel sheet as the basis for the translations. Each translatable text would be given a key and five translations for Dutch, English, Spanish, German and Italian.
  2. the Excel-sheet is put into our version control system TFS
  3. in our FinalBuilder script we're doing a Get Latest Version to make sure we're using the latest translations
  4. although the entire product was written in C#, we're using a VB.NET tool to convert the Excel-sheet to a serialized list of (custom) Translation objects
  5. in our XAML we're using a MarkupExtension to fetch the required translated text.

Why did we use VB.NET to create the conversion from Excel to a serialized list?

I personally have more affection for C# than for VB.NET, which is primarily driven by my strong Delphi background. I have nothing against VB: it certainly has a place in the developer community. The beauty of .NET is the freedom of choice: the choice for a specific language. The language you feel most comfortable in. If you ever catch me bashing on VB, it's on VB6 and earlier. VB.NET is a language that is as strong as C# is. It's just a matter of what you like best. I often compare it to cars. What's the better car: a BMW or a Mercedes? It depends on who you ask (if you ask me, I'd say a BMW, but that's an entirely different story)

Some people might argue that it's bad to use both VB.NET and C# in the same solution, but I don't see an issue with that for this case. The import program is really small and any C# programmer can read (and write although a bit slower) VB.NET. Most often, it's a matter of taste: most C# developers just don't like it. Anyway, the real reason we chose VB.NET here is the support for default parameters. Something which is really useful when talking to a COM-based application. But soon, well, about a year from now probably, C# will also have named and default parameters in C# 4.0.

So, the import program is pretty simple:
 

      For row As Integer = 2 To 2000

        Dim range As Excel.Range = xlws.Range(String.Format("A{0}:F{0}", row))

 

        Dim key As String = range.Value2(1, 1)

        Dim english As String = range.Value2(1, 2)

        Dim dutch As String = range.Value2(1, 3)

        Dim german As String = range.Value2(1, 4)

        Dim spanish As String = range.Value2(1, 5)

        Dim italian As String = range.Value2(1, 6)

 

        If Not String.IsNullOrEmpty(key) And key <> "-" Then

          If keys.Contains(key) Then

            doubles.AppendLine(key)

          Else

            Dim trans As New Translation(key, dutch, english, german, italian, spanish)

            list.Add(trans)

            keys.Add(key)

          End If

        End If

      Next

 

      Dim serializer As New Serialization.XmlSerializer(GetType(List(Of Translation)))

 

      File.Delete("translations.xml")

      Dim fs As New FileStream("translations.xml", FileMode.CreateNew, FileAccess.Write)

 

      serializer.Serialize(fs, list)

      fs.Close()

      xl.Quit()

 

Really straightforward, he? Really quick and dirty. I'm really against Q&D, but in this case it's just a waste to spend more time on it then needed. It gets the job done and it's highly unlikely that we'll ever look at this code again. Sometimes we have to sacrifice maintainability to get more productive. The real trick is to know when to do this and when to go for the better solution.

MarkupExtensions

But the real trick was to use theses translations in XAML. To accomplish that we wrote a MarkupExtension as said before. MarkupExtension are what's in curly braces in XAML, like this:

<Grid Width="500"
        Margin="5"
        DataContext="{Binding ElementName=entryListBox, Path=SelectedItem}" >

<TextBlock Text="This is Active caption color" Background="{StaticResource {x:Static SystemColors.HighlightBrushKey}}"/>

 

MarkupExtension are, of course i'd say, classes. And you can derive from this abstract class. The MarkupExtension class itself is pretty simple:

using System;

 

namespace System.Windows.Markup

{

  public abstract class MarkupExtension

  {

    protected MarkupExtension();

    public abstract object ProvideValue( IServiceProvider serviceProvider );

  }

}

 

The convention is that MarkupExtensions end their name with Extension, but this is not needed to specify in XAML. So the Binding MarkupExtension is really named BindingExtension and the StaticResource MarkupExtension is named StaticResourceExtension. All you have to do in the MarkupExtension is to implement the ProvideValue method. This is our implementation of our LanguageExtension

  [MarkupExtensionReturnType( typeof( string ) )]

  public class LanguageExtension : MarkupExtension

  {

    private object resourceKey;

    public object ResourceKey

    {

      get

      {

        return resourceKey;

      }

      set

      {

        resourceKey = value;

      }

    }

 

    public LanguageExtension( object resourceKey )

    {

      ResourceKey = resourceKey;

    }

 

    public override object ProvideValue( IServiceProvider serviceProvider )

    {

      return LanguageManager.Instance.LocalizedString( resourceKey.ToString() );

    }

  }

 
The MarkupExtensionReturnType attribute powers the compiler with the knowledge that this MarkupExtension always returns strings, so that it can check if you're only using it at places where a string is expected. The LanguageManager is a singleton that returns the localized text of the given key based on the currently chosen language.

    public LanguageExtension( object resourceKey )

    {

      ResourceKey = resourceKey;

    }


This constructor makes sure we're required to pass an argument when using the MarkupExtension like this: 

<Window

  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

  xmlns:lm="clr-namespace:Oosterkamp.Samples"

  x:Class="Oosterkamp.Samples.MainWindow"

  x:Name="MainApp"

        <ToggleButton x:Name="LocalizedButton" Content="{lm:Language LocalizedButtonKey}" />

</Window>


We just add the CLR namespace 'lm' at the top of the XAML file with this line of code; xmlns:lm="clr-namespace:Oosterkamp.Samples" . Then anywhere where we want a localized text we use ="{lm:Language LocalizedButtonKey} to get the localized text for the given key.

In this particular scenario, this was the best solution we could come up with. People are maintaning the translations in Excel and our automated build process in FinalBuilder makes sure that these translations are used in the next build when the Excel file is checked back into TFS.

Comments

 

Fabrice's weblog said:

Several techniques exist for localizing WPF applications. I have yet to study them before making a choice

mei 12, 2009 12:52

About Thomas

Thomas is a senior consultant and trainer. He's interested in anything related to improving software quality like clean code, testability, process optimalization, architectures and developer tools. Thomas can be reached at thomas@oosterkamp.nl.