Archive

Posts Tagged ‘Resource’

Localization Revisited

May 31, 2012 Leave a comment

Among the first things I covered in this blog was Localization. After I had finished those posts, I felt like I had covered everything one would need to write fully globalized applications. However, meanwhile I found out how to do some things better, and I’d like to share that here.

Making Localization Work at Design Time

One of the drawbacks of the original WPFGlue localization components was that they didn’t work nicely at design time. The reason was that I hadn’t found out how to automatically locate an application’s resources when the application was hosted inside the VS XAML designer.

The key here is a property on the Application class named ResourceAssembly. When the VS designer hosts an assembly, it will put a reference to it into this property, so the MarkupExtensions can locate the assembly that contains the current project’s resources.

Also, I revised the detection for the resource basename. The basename contains the project’s root namespace. One can find this as the namespace of the project’s Application class, which will normally be the project’s only class derived of System.Windows.Application.

I encapsulated this all into a new base class for the localization markup extensions:

Namespace Localization
    Public MustInherit Class ResourceExtension
        Inherits System.Windows.Markup.MarkupExtension

        Public Sub New()
            MyBase.New()
        End Sub

        Public Sub New(ByVal key As String)
            MyBase.New()
            Me.Key = key
        End Sub

        Private _Key As String
        Public Property Key() As String
            Get
                If String.IsNullOrEmpty(_Key) Then
                    Return "No Key"
                Else
                    Return _Key
                End If
            End Get
            Set(ByVal value As String)
                _Key = value
            End Set
        End Property

        Private Shared _BaseName As String
        Public Shared Property BaseName() As String
            Get
                If _BaseName Is Nothing And Assembly IsNot Nothing Then
                    Dim applicationType As Type = (Aggregate t As Type In Assembly.GetTypes() Where t.IsSubclassOf(GetType(System.Windows.Application)) Into First())
                    If applicationType IsNot Nothing Then
                        _BaseName = applicationType.Namespace & ".Resources"
                    End If
                End If
                Return _BaseName
            End Get
            Set(ByVal value As String)
                _BaseName = value
                _ResourceManager = Nothing
            End Set
        End Property

        Private Shared _Assembly As System.Reflection.Assembly
        Public Shared Property Assembly() As System.Reflection.Assembly
            Get
                If _Assembly Is Nothing Then
                    _Assembly = System.Reflection.Assembly.GetEntryAssembly
                End If
                If _Assembly Is Nothing Then
                    _Assembly = Application.ResourceAssembly
                End If
                Return _Assembly
            End Get
            Set(ByVal value As System.Reflection.Assembly)
                _Assembly = value
                _ResourceManager = Nothing
            End Set
        End Property

        Private Shared _ResourceManager As System.Resources.ResourceManager
        Public Shared Property ResourceManager() As System.Resources.ResourceManager
            Get
                If _ResourceManager Is Nothing Then
                    If Assembly IsNot Nothing And BaseName IsNot Nothing Then
                        _ResourceManager = New System.Resources.ResourceManager(BaseName, Assembly)
                    End If
                End If
                Return _ResourceManager
            End Get
            Set(ByVal value As System.Resources.ResourceManager)
                _ResourceManager = value
            End Set
        End Property
    End Class
End Namespace

 

Notice the fallback detection for the resource assembly: the first location is the application domain’s entry assembly. If the assembly isn’t running it’s own domain (because it’s hosted in the designer), the next location is the ResourceAssembly. As before, one can override the resource assembly by setting the Localization.ResourceAssembly attached property.

By the way, in the post "The Last Word on Localized Resources" I suggested using a localized URL in order to use different localized ResourceDictionaries in ones XAML files. I don’t recommend this approach any longer. While it works at run time, the designer experience is bad: one will get squiggly lines telling that the resources can’t be found, and one cannot see the values of the resources in the designer panel. With the new markup extensions based on the above class, one will have instant feedback.

Funny enough, VS will use the set of resources that match its own language version. So, running US-English Microsoft Visual Basic 2010 Express on German Windows, I see the English localization in the designer and the German localization when I start the debugger.

How to Use Bitmap Resources

Another thing I learned is how to convert from System.Drawing.Bitmap, which is what one gets from the ResourceManager when one accesses an image resource, to System.Windows.Media.Imaging.BitmapSource, which is what the WPF Image control expects:

Public Overrides Function ProvideValue(ByVal serviceProvider As System.IServiceProvider) As Object
    Dim result As BitmapSource = NoImage
    If ResourceManager IsNot Nothing Then
        Dim data As System.Drawing.Bitmap = TryCast(ResourceManager.GetObject(Key), System.Drawing.Bitmap)
        If data IsNot Nothing Then
            Dim sourceRect = New Int32Rect(0, 0, data.Width, data.Height)
            result = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(data.GetHbitmap, Nothing, sourceRect, BitmapSizeOptions.FromEmptyOptions)
        End If
    End If
    Return result
End Function

 

The conversion happens through a shared bitmap handle, so I assume it’s not too heavy on memory consumption. As a nice side effect, one can now use PNG images without them being unaccountably scaled to 133% their pixel size. Also, the images will now instantly be visible in the designer.

How to Create Alternative Resource Files

In my first post about localized resources, I described a kind of complicated procedure for creating resource files for alternative languages. A somewhat simplified procedure, which goes without closing the IDE, is as follows:

  • Complete your application, creating resources in the My Project / Resources designer as you go.
  • When done, activate the “Show All Files” icon in Solution Explorer.
  • Locate the Resources.resx file in the My Project folder and create a copy of it. You can do this using the context menu in Solution Explorer. The copy can be located anywhere in your project directory. Probably you should keep it outside the My Project folder.
  • Rename the copy to reflect the intended language and culture, e.g. to Resources.de.resx for general German, or Resources.ar-SA.resx for Arabic (Saudi Arabia). At that stage, you can deactivate the “Show All Files” switch again.
  • Double click the file in order to open it in the resource designer.
  • Make sure that the Access Modifier is set to “No Code Generation”.
  • Replace the original resource strings and icons with the appropriate translations.
  • Make sure that the Neutral Language in My Project / Application / Assembly Information is set to the language you used for the original version of the resources.
  •  

    Conclusion

    While support for .Net resources is completely missing from WPF, one can create an experience that feels equally nice at design time and at run time by using some simple custom markup extensions.

Using .Net Localized String Resources in WPF

December 5, 2009 1 comment

When localizing an application, the most obvious feature is that labels, texts and pictures in the UI are displayed in another language. This is done through localized string resources, and .Net has a whole framework for handling these. In Windows Forms, this framework is quite well supported by Visual Studio. However, in WPF this support is considerably lacking. This is about how you can use them anyway.

Creating Localized Resources

You can edit the localized resources of your application if you choose the Resources tab in the My Project editor. However, these are the resources for the neutral or invariant culture, the .Net culture description that is not connected to any natural language. These resources are embedded into the executable of your application. If you want culture specific resources which are compiled into .Net satellite DLLs which you can distribute with your application in order to let it support multiple languages, you have to create them manually. Unfortunately, the documentation on this is not very clear. In my tests, I did the following:

  • Close Visual Studio
  • Go to the My Project / MyExtensions folder in the project directory
  • Copy the file Resources.resx and rename the copy to the correct name for the culture, e.g. Resources.de-DE.resx for German or Resources.en-US.resx for US English
  • Open the Project again in Visual Studio
  • Select Add Existing Item and add the file to the project.
    After you have done that, the Resource designer will display if you open the file from Solution Explorer. Just make sure that the Access Modifier combo box on top says “No Code Generation”, otherwise you might get error messages about conflicting class names on compilation.

Since you copied the original resource file, all resources will already be there in the original language; so, if you do this towards the end of the project, you will already have the correct resource keys and their original translations to help you…

The Visual Studio Resource designer was originally intended for Windows Forms. As a result, the image resources it produces (at least at the time of writing, Visual Studio 2008 SP1) are tied to the Windows Forms image types, which are not working with WPF. For this reason, I decided to not support image resources. Instead, one can create bitmap images from URIs which are stored as resource strings, so that they can point to different sources for different languages. These URIs should be pack URIs if you provide the images with your program; pack URIs are explained very nicely here.

Using Localized Resources in Your WPF Pages

WPFGlue offers four different MarkupExtensions for integrating localized resources in XAML. Their names are StringExtension, URIExtension, BitmapImageExtension, and FlowDirectionExtension. StringExtension is the base class. Its ProvideValue method, which is called whenever the XAML that contains the extension is instantiated, looks like this:

Public Overrides Function ProvideValue(ByVal serviceProvider As System.IServiceProvider) As Object
    Dim result As Object = "[" & Key & "]"
    If _ResourceManager Is Nothing Then
        Try
            If Assembly IsNot Nothing AndAlso Not Assembly.FullName.StartsWith("PresentationFramework,") Then
                _ResourceManager = New System.Resources.ResourceManager(BaseName, Assembly)
            End If
        Catch x As Exception
            result = Assembly.FullName
        Finally
        End Try
    End If
    If _ResourceManager IsNot Nothing Then
        Try
            result = _ResourceManager.GetString(Key)
        Finally
        End Try
        If result Is Nothing Then
            result = "[" & Key & "]"
        End If
    End If
    Return result

End Function

It reads strings from the application’s resources; URIExtension converts them into URIs on top of that, and BitmapImageExtension creates a BitmapImage from the URI it looked up in the resource. FlowDirectionExtension checks whether the current UI language is written from right to left, and returns the correct FlowDirection value. All these markup extensions behave like StaticResource. This means that they return their value at the time the XAML is instantiated, and don’t create a binding to the properties they are applied to. It also means that you can use them everywhere in your XAML, not only with DependencyProperties.

<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" >
    <TextBlock Text="{l:String TestString}" />
    <TextBlock Text="{Binding Source={StaticResource Value}}"/>
    <TextBlock Text="{l:Binding Source={StaticResource Value}}"/>
    <Image Stretch="None" Source="{l:BitmapImage FlagURI}"/>
</StackPanel>

So far so good. Using these extensions, you make sure that the users see the application in their system’s language, if you provided the correct resources. But what if you want to change the language at runtime?

This is where it gets sticky. The UILanguage Sticky Property is used to read and set the current UI language, which is the same language the resource manager uses in order to find the resources it wants to display. However, changing this property will not automatically change all the labels. I didn’t think this necessary; in this solution, changes of the UI language will take effect only after you navigate to a different page.

Public Shared ReadOnly UILanguageProperty As DependencyProperty = DependencyProperty.RegisterAttached("UILanguage", GetType(String), GetType(Localization), New PropertyMetadata(Nothing, AddressOf OnUILanguageChanged, AddressOf CoerceUILanguage))
Public Shared Function GetUILanguage(ByVal d As DependencyObject) As String
    Return d.GetValue(UILanguageProperty)
End Function
Public Shared Sub SetUILanguage(ByVal d As DependencyObject, ByVal value As String)
    d.SetValue(UILanguageProperty, value)
End Sub
Private Shared Sub OnUILanguageChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
    If e.NewValue IsNot Nothing Then
        System.Threading.Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.GetCultureInfo(CStr(e.NewValue))
    End If
End Sub
Private Shared Function CoerceUILanguage(ByVal d As DependencyObject, ByVal baseValue As Object) As Object
    Dim result As String = baseValue
    If baseValue Is Nothing Then
        result = System.Globalization.CultureInfo.CurrentUICulture.IetfLanguageTag
    End If
    Return result
End Function

Notice that setting this property to {x:Null} explicitly will coerce its value to the current UI language. You can use this to initialize a language selection box to the correct start value.

Setting UILanguage on any element in XAML will change the UI language for the whole application. However, if you want a common and consistent access point for this, consider setting it on the topmost XAML element in your application, i.e. the main Window or NavigationWindow.

Using Resources from Different Assemblies

You can use l:String etc. just out of the box, without having to tell the system where it would find the resources. This is possible because the MarkupExtension uses the type of the current Application object in order to find the assembly of the main program.

In the designer, obviously the main program is Visual Studio, so the markup extension doesn’t know where to look. If it isn’t able to discover the ResourceManager it needs to provide the string it is looking for, or if the key it receives does not have a corresponding resource, it just returns the value of the key itself enclosed in brackets.

However, there is a way to tell it where to look. This has to be there in order to be able to read resources from assemblies other than the main program’s assembly. For example, you could have put all your resources into a special resource assembly, or you could use a library which brings along its own resources.

The most convenient way to make a different assembly the resource lookup source is to assign a type from that assembly to the ResourceReferenceType sticky property. This assumes that the basename for resources in this type’s assembly is “Resources”, as is the default with VB.Net. You can also have finer control setting the ResourceAssembly and ResourceBasename properties, respectively. (n.b. I’m not sure whether the automatic detection of the assembly and basename works the same way in VB and C#).

These settings influence the whole application. This means that as long as they point to a certain place, all resource strings in the application are taken from there. That looks like quite a serious limitation; however, if using a Navigation application, every page would be independent and could easily have its own setting, and if you’d divide your page into regions using containing elements, the behaviour would be consistent as long as every containing element and every data template sets the source for its localized resources explicitly. This is so because the markup extensions access the resource DLL only while the XAML is instantiated, and the set properties retain their values even if the resource assembly changes later on (*).

If you want to preview your resource strings in the designer, you can just set the ResourceReferenceType and UILanguage on the window you are working on to your program’s application class and language, respectively.

<Page x:Class="LanguageSelectionPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml&quot;
    xmlns:l="https://wpfglue.wordpress.com/localization&quot;
    xmlns:ex="clr-namespace:LocalizedResourcesExample"
    xmlns:r="clr-namespace:LocalizedResourceAssembly;assembly=LocalizedResourceAssembly"
    Title="LanguageSelectionPage" FlowDirection="{l:FlowDirection}"
      l:Localization.ResourceReferenceType="{x:Type ex:Application}"
      l:Localization.UILanguage="de-DE">

     However, if you want to be able to change the resource assembly and language at runtime, don’t forget to remove these attributes before you build the application.

The LocalizedResourceExample

The example solution you can download from the Downloads page demonstrates how to select the UI language at runtime:

<ComboBox SelectedValue="{Binding RelativeSource={RelativeSource Mode=FindAncestor,
    AncestorType={x:Type NavigationWindow}}, Path=(l:Localization.UILanguage)}"
    ItemsSource="{StaticResource languageList}" DisplayMemberPath="Key"
    SelectedValuePath="Value"/>
</StackPanel>

 

Notice that the ComboBox sets the value of the UILanguage attached property directly on the NavigationWindow that hosts the page. However, you have to navigate away from the page in order to see the changes applied. If you go back to the language selection page, you will see that this page also re-reads its resources and displays differently dependent on what is selected in the language selection box.

The second combobox will select the assembly LocalizedResourceAssembly in order to access its resources. Apart from the resources themselves, this assembly contains only an empty class called “Dummy” which serves as reference class to find the resources. Since the LocalizedResourceAssembly doesn’t contain all the resources defined in the main assembly, you can watch the fallback behaviour for the resource lookup: If the resource isn’t there in the specified language, it is taken from the main culture of the application (which is de-DE German in the example), and if it is not found there, the key of the resource will be displayed. Since this fallback mechanism only works within the same resource assembly, you will see different fallback behaviour if you choose the external resources.

Look out for..

The methods described here help in localizing the descriptive elements of your user interface, and given the resources are already there, everything is controlled completely through XAML. In order to localize value formats, you need some other techniques. They are already contained in the example, but I will explain them in a later post.

Credits

I learned how to write a MarkupExtension from this article by Christian Moser. Even though I changed his approach considerably, I found it extremely helpful, and especially if you want to take changes in the UI Language to take effect immediately, you should look at his example.

 

 

(*) Footnote: In order to make this fool proof, one could make the ResourceReferenceType property inheritable, so that it is copied to all child elements. However, I try to avoid FrameworkPropertyMetadataOptions.Inherit if possible, even if I don’t think that looking around for the correct ResourceManager on every localized string is very expensive in terms of performance.