Archive
Localization Revisited
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:
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:
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.
Localized Value Formatting in WPF
When I introduced the WPFGlue localization components, I concentrated mainly on displaying localized strings and other static information in the user interface. Today I want to complete the WPFGlue localization section by discussing localized value formatting.
The Problem
Number and date formats vary from culture to culture. 30/12/2009, 12/30/2009 and 30.12.2009 all refer to the same day, and 12.345,67 and 12,345.67 are actually the same number, but will turn out very different if the computer doesn’t know which format the user assumed. Since the early days of computers, this problem has been solved by defining the culture under which the system runs as a system property. Then there would be a set of predefined formats for each given culture which implement the usual standards in that culture. Applications would take number and date formats from these system settings, so they could use the local standards even though they were developed in a different language.
A special case of this problem are enumerated lists. Usually, there is a short text that describes each option. In a localized application, this text should be translated according to the current culture, and it would also be necessary to sort the list differently in different languages in order to keep up an alphabetic order of the items.
Specifying the Formatting Culture in WPF
In WPF, value formatting is part of data binding: if a control needs to display a bound value, it converts it into a string and displays the string. The culture which is used can come from three sources:
- Default: if you don’t do anything special to tell the system from which culture it should derive its number and date formats, it will just take standard US English. Makes sense to define one culture so that by default the application will look the same on whatever system it runs. But actually, in the rest of Dot.NET the rule is that the application should take the installed system culture as default for these purposes, so WPF breaks the expectations of especially us poor Non-Americans…
- The Language property or xml:lang attribute: WPF FrameworkElements have a Language property, which can accept a IETF language tag like en-US or de-DE, and will set the date and number formats according to this language and culture’s properties. The definitions for the formats come from a set of preinstalled cultures which are part of the Dot.Net framework. The Language property is inherited, so if it is specified on top of a page, it will take effect on the whole page.
- The ConverterCulture property on the Binding: This property accepts a Globalization.CultureInfo object. This object might come from any source. Especially, if the user changed the standard number and date formats on the system using the Language and Regional Settings in Control Panel, these changes will be visible in the CurrentCulture object of the application, while they won’t be visible if one just sets the Language property.
So, setting the ConverterCulture on the bindings would be the preferred way to respect the system’s and user’s formatting preferences. However, there are two drawbacks: first of all there is no built-in way to get at the system culture in XAML, and second one would have to specify the culture on each and every binding, since there is no way to apply this setting in a way that is global to the application.
In order to solve this problem, I added another MarkupExtension to the WPFGlue localization namespace: Binding. This is actually nothing more than a normal Binding with the ConverterCulture preset to the CurrentCulture of the system:
Public Class Binding
Inherits System.Windows.Data.Binding
Public Sub New()
MyBase.New()
ConverterCulture = System.Globalization.CultureInfo.CurrentCulture
End Sub
Public Sub New(ByVal path As String)
MyBase.New(path)
ConverterCulture = System.Globalization.CultureInfo.CurrentCulture
End Sub
End Class
End Namespace
So, one can use it everywhere instead of the default Binding, and get date and number formats that behave according to the system settings. I’m not especially proud of this solution, but for the time being it seems to be the best compromise.
The LocalizationExample which you find in the WPFGluePublished solution on the Downloads page demonstrates this behaviour. This XAML code:
<Page.Resources>
<s:DateTime x:Key="Value">#12/30/2009#</s:DateTime>
</Page.Resources>
<Grid>
<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>
</Grid>
</Page>
produces the following output:
Notice that setting the page’s Language property to the UILanguage (German in my case) causes the first binding to display its value in the default German date format, while using the localized binding causes the same value to be displayed in the format YYYY-MM-DD, which is what I specified in Control Panel as the standard short date format.
Localized Lists
In Dot.NET, sets of options can and should be summarized in enumeration types, with a named constant for each available option. The Dot.NET formatting engine, which is called through the String.Format method, supports converting the numeric enumeration values to strings and vice versa using the constant names that are defined for the enumeration in code. However, this option is only useful for debug output. If the values must be displayed to users, they must be translated into their languages, and actually this is not the job of the code which defines the enumeration.
A user interface which wants to use enumeration values needs the following functions:
- translate numerical values to string descriptions (for read-only display of values).
- translate string descriptions into numerical values (for cases where the user can enter text).
- create a mapped list of values and descriptions (for ComboBoxes and the like where the user just picks a value from a list).
The LocalizedList class provides these functions. It works both as a collection of value / description pairs and as a IValueConverter that converts the descriptions forwards and backwards.
In order to define a LocalizedList, you have several options:
- Just set the ValueEnum to an enumeration type and let the LocalizedList generate the entries from the enumeration’s definition (actually, this is not localizing anything yet…)
- Set the LocalizedList’s Definition property to a string in the format “value1=name1,value2=name2…”, where the values are the constant names or values from the enum definition, and the names the localized display names. Typically, the definition string would itself be a localized string resource.
- Define Entry objects directly in the XAML, which map string keys to arbitrary typed values.
The definition string format is actually quite flexible. So, you can change the default list and value separator characters (“,” and “=”). You could also leave out the values and just specify the enumeration type, so that the LocalizedList assumes that you specified the descriptions for the numeric values in their natural order. On the other hand, you could specify both names and values, but change the order, or use a subset of the available values, only. And finally, you could leave out the ValueEnum altogether and use String codes instead, as it is done in the language selection list in the example:
where e.g. the English version of the referenced string resource is
de-DE=German,en-US=English,ar-SA=Arabic
The other LocalizedList in the example maps String keys directly to objects:
<l:Entry Key="{l:String InternalLabel}" Value="{x:Null}"/>
<l:Entry Key="{l:String ExternalLabel}" Value="{x:Type r:Dummy}"/>
</l:LocalizedList>
Using this list, it is possible to change the resource assembly of the example purely in XAML, with no code behind:
DisplayMemberPath="Key" SelectedValuePath="Value"
SelectedValue="{Binding RelativeSource={RelativeSource
Mode=FindAncestor, AncestorType={x:Type NavigationWindow}},
Path=(l:Localization.ResourceReferenceType)}"/>
Conclusion
Using localized resources on the one hand and providing localized value formatting on the other hand are about all that comes to my mind under the heading of localization. I’ll be glad to react to feedback or criticism…
The Last Word on Localized Resources in WPF
After finishing my last post on using localized resources in WPF, I had the following idea, which I should have included from the start:
If you want to maintain your resources in a more WPF-ish and less WinForms-ish way, just do this:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="http://wpfglue.wordpress.com/localization"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="{l:URI MyUriToLocalizedDictionaries}"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
Set up your localized resource satellite assemblies like described in the other post, but don’t put anything into them except the pack URI’s of your localized dictionary in the respective language. This way, you can include every type of resource that’s available in WPF, don’t have any WinForms dependencies, and still have the standard .Net way of selecting the right localized resources. You can maintain the consistency of the dictionaries using a normal diff tool, and are no longer annoyed by the Visual Studio resource designer’s bad habit of interrupting your typing while auto-saving and swallowing what you just typed if you are not lucky.
On the downside, however, all localized resources will be part of your main assembly, either directly or by reference, and it is less convenient to add support for new languages, except if you decide to store the resource dictionaries as uncompiled Content files.
Using .Net Localized String Resources in WPF
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:
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.
<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 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.
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="http://wpfglue.wordpress.com/localization"
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:
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.
Localization in WPF
There are lots of good articles on localizing WPF applications out there, so here it is more a matter of commenting and choosing different approaches than of creating new value. But before I start, I’d like to discuss the jobs of localization a bit, and explain my choices in how to handle them.
Providing a Localized User Interface
The most obvious effect of localization is to provide labels, descriptions and pictures which contain texts in the current user’s language. So, the two bits of information one needs in order to provide this are the user’s language and the source of the texts. In .Net, localized resources are contained in resource DLLs, which are provided together with the main program executable, and are accessed through the ResourceManager class internally, already taking into account the language of the current thread.
Visual Studio supports two ways of editing localized resources: the one is directly through the My Project / Resources editor, the second is indirectly as part of the Windows Forms designer. Unfortunately, there is no such support for WPF. There is a command line tool named LocBAML which is supposed to extract all localizable strings from compiled XAML and to allow to create the localized resource DLLs from it, but this is not very integrated into the development process and requires changes to the XAML itself using UI ids (which are a good thing to have but a nightmare to maintain). So, most examples I found ignore this approach and instead make for a way to make the usual localized resources from the Resource editor available to WPF.
The obvious solution is a MarkupExtension that accepts the name of the resource as a parameter and returns its value. Some of the published components allow to change the language at runtime; I didn’t think this necessary, so my implementation just sets the value, which means you have to close and reopen forms in order to make a language change visible. However, it has the neat little feature of being able to find the ResourceManager of the main program’s assembly by itself, so you don’t have to configure anything in order to be able to use it. Find it explained here.
Formatting and Parsing Values According to Localized Formats
Formatting and parsing values that are displayed to or edited by the user needs to take into account local variations in formatting conventions. 12.456,70 and 12,456.70 are actually the same number, but if you’d ask an American and a German they’d probably disagree on what number that is.
Recognizing these formats has been a job for localization components ever since computers have been invented, so it’s quite well supported in .Net. However, for reasons unknown WPF’s default language for all these purposes is US English, regardless of the system’s settings, let alone the culture of the current thread. In order to change the language for formatting, you have to set the Language attribute. There are some ways to do it; one way one frequently sees is to override its default value to be the system’s culture on application startup, but I find this a little bit harsh, and I think you can’t undo it if you want to change the language at runtime. So I prefer to set the Language attribute on top of the page or window, so that it is inherited by all the controls. In order to get at the language more conveniently, I implemented MarkupExtensions that return the current language or the current UI language, respectively.
There are cases where this behaviour isn’t enough. As I learned recently, even setting the Language property to the CurrentCulture doesn’t make WPF acknowledge changes in date and number formats users might have made in the Regional Settings of their computer. In order to work around this, one would have to use a custom Binding extension, which might be a good idea anyway, since it provides a way to hook all sorts of useful little helpers to bindings without changing the XAML code too much. But actually, I don’t like this solution and hope there will be a fix soon…
A special case of localized formatting is providing translations for Enum values. .Net can parse Enum values from their string equivalents, but unfortunately those string equivalents are hardcoded with the definition of the Enum. Therefore, WPFGlue contains a LocalizedList, which fills two jobs: It works as a Converter, converting the Enum values into localized strings and vice versa, and it can be used as ItemsSource for a ComboBox or other Selector in order to let the user pick from the available values. Its intended way of usage is defining an instance of it in a ResourceDictionary, using an initialization string which maps the invariant string representations to localized counterparts. As an extra, it can produce and parse strings that are combinations of multiple Enum values.
You can find descriptions and examples for these components here.
Switching the Keyboard to Different Languages
Handling different languages with different keyboard layouts within the same document has only recently become a common feature in text processing applications. However, since I frequently have to work with data in different languages, I wouldn’t like to miss this feature.
In WPF, the class responsible for managing different keyboard languages is called InputLanguageManager. It exposes an attached property, InputLanguage, that accepts a culture identifier and switches the keyboard to the defined layout for this language, if one is installed. Unfortunately, this only works when the control receives focus. So, if you are typing something in a TextBox and change the InputLanguage while you are typing, e.g. through a keyboard shortcut, this change doesn’t become active unless the keyboard focus leaves the TextBox and returns again.
Also, in principle you can define different languages for paragraphs in a RichTextBox by setting the InputLanguageManager.InputLanguage on the Paragraph elements in the RichTextBoxes FlowDocument, but the keyboard doesn’t automatically change to the appropriate layout when the selection enters a section in a different language. This feature is extremely useful, so I decided to create a Sticky Property which allows to change the keyboard language effective immediately, and a Sticky Behaviour that sets the current input language according to what is set in this attached property on the FrameworkContentElements inside a RichTextBox’s FlowDocument. This property is explained here. Recently I found a post on Connect which suggests that there might be a native solution for this problem in WPF 4.
Links
Christian Moser describes a good example for localization by MarkupExtension here.
Switching the Keyboard to Different Languages
Handling different languages with different keyboard layouts within the same document has only recently become a common feature in text processing applications. However, since I frequently have to work with data in different languages, I wouldn’t like to miss this ability.
In WPF, the class responsible for managing different keyboard languages is called InputLanguageManager. It exposes an attached property, InputLanguage, that accepts a culture identifier and switches the keyboard to the defined layout for this language, if one is installed. Unfortunately, this only works when the control receives focus. So, if you are typing something in a TextBox and change the InputLanguage while you are typing, e.g. through a keyboard shortcut, this change doesn’t become active unless the keyboard focus leaves the TextBox and returns again.
Also, in principle you can define different languages for paragraphs in a RichTextBox by setting the InputLanguageManager.InputLanguage on the Paragraph elements in the RichTextBoxes FlowDocument, but the keyboard doesn’t automatically change to the appropriate layout when the selection enters a section in a different language. This feature is extremely useful, so I decided to create a Sticky Property which allows to change the keyboard language effective immediately, and a Sticky Behaviour that sets the current input language according to what is set in this attached property on the FrameworkContentElements inside a RichTextBox’s FlowDocument.
Changing the Keyboard Language at Runtime
The way the KeyboardLanguage property is implemented is a typical example for the Sticky Property design pattern. A Sticky Property is an attached property that has an OnPropertyChanged handler which does some useful work, so that the property has an effect aside from storing data with the object it is set on. In the case of the KeyboardLanguage property, this work would be looking up the available keyboard languages, checking whether one of them fits the value, and activating this keyboard layout.
Public Shared Function GetKeyboardLanguage(ByVal d As DependencyObject) As String
Return d.GetValue(KeyboardLanguageProperty)
End Function
Public Shared Sub SetKeyboardLanguage(ByVal d As DependencyObject, ByVal value As String)
d.SetValue(KeyboardLanguageProperty, value)
End Sub
Public Shared Sub OnKeyboardLanguageChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
If TypeOf d Is FrameworkElement Then
Dim l As System.Globalization.CultureInfo = FindKeyboardLanguage(e.NewValue)
If String.IsNullOrEmpty(e.NewValue) Or l Is System.Globalization.CultureInfo.InvariantCulture Then
d.ClearValue(InputLanguageManager.InputLanguageProperty)
d.ClearValue(InputLanguageManager.RestoreInputLanguageProperty)
Else
d.SetValue(InputLanguageManager.InputLanguageProperty, l)
d.SetValue(InputLanguageManager.RestoreInputLanguageProperty, True)
InputLanguageManager.Current.CurrentInputLanguage = l
End If
End If
End Sub
Public Shared Function FindKeyboardLanguage(ByVal tag As String) As System.Globalization.CultureInfo
Dim result As System.Globalization.CultureInfo = System.Globalization.CultureInfo.InvariantCulture
If Not String.IsNullOrEmpty(tag) Then
For Each c As System.Globalization.CultureInfo In InputLanguageManager.Current.AvailableInputLanguages
If c.IetfLanguageTag.ToUpper.StartsWith(tag.ToUpper) Then
result = c
End If
If c.IetfLanguageTag.ToUpper = tag.ToUpper Then
Exit For
End If
Next
End If
Return result
End Function
This gives the added flexibility of supporting language-only IETF tags, so that you don’t have to know exactly which flavour of the language the target system supports.
Keeping Track of Different Languages in a FlowDocument
This is done through a sticky (or attached) behaviour. A Sticky Behaviour is an attached property that registers event handlers on the control it is attached to. In this case, the behaviour will be specialized for RichTextBoxes, because their ability to edit FlowDocuments is the most convenient way to edit structured documents in WPF.
The Sticky Behaviour is activated by setting the TracksKeyboardLanguage property to true. Whenever the selection changes, this will cause the RichTextBox to check whether there are KeyboardLanguage attributes on the parent element of the current selection, and to activate the appropriate keyboard layout:
Public Shared Function GetTracksKeyboardLanguage(ByVal d As DependencyObject) As Boolean
Return d.GetValue(TracksKeyboardLanguageProperty)
End Function
Public Shared Sub SetTracksKeyboardLanguage(ByVal d As DependencyObject, ByVal value As Boolean)
d.SetValue(TracksKeyboardLanguageProperty, value)
End Sub
Private Shared Sub OnTracksKeyboardLanguageChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
If TypeOf d Is RichTextBox Then
Dim b As RichTextBox = d
If CBool(e.OldValue) Then
RemoveHandler b.SelectionChanged, AddressOf OnSelectionChanged
End If
If CBool(e.NewValue) Then
AddHandler b.SelectionChanged, AddressOf OnSelectionChanged
End If
End If
End Sub
Private Shared Sub OnSelectionChanged(ByVal sender As Object, ByVal e As RoutedEventArgs)
Dim b As RichTextBox = sender
Dim f As FrameworkContentElement = TryCast(b.Selection.Start.Parent, FrameworkContentElement)
If f IsNot Nothing Then
Dim tag As String = GetKeyboardLanguage(f)
If Not String.IsNullOrEmpty(tag) Then
If Not InputLanguageManager.Current.CurrentInputLanguage.IetfLanguageTag.ToUpper.StartsWith(tag.ToUpper) Then
InputLanguageManager.Current.CurrentInputLanguage = FindKeyboardLanguage(tag)
End If
End If
End If
e.Handled = False
End Sub
It is always wise to make these event handlers static, so that they don’t create any references which could keep the control from being freed. Setting e.Handled to false makes sure that the RichTextBox still can accept other handlers for the SelectionChanged event.
Downloading the Example
The WPFGluePublished solution on the Downloads page contains a project called KeyboardLanguageExample which demonstrates the use of the KeyboardLanguage and TracksKeyboardLanguage properties. This example assumes that you have German, Arabic and English installed as Input languages on your computer. If you don’t have any additional keyboards installed, you can do so using the Regional and Language Options control panel in Windows.
