|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Table of ContentsIntroductionBackground What You Will Need To Follow This Tutorial (Software Prerequisites) Approach #1: Targeted Localization Without Using LocBaml.exe Creating The WPF Application Adding a Button to the Window Adding the string resources Replacing the button text Getting a public resources class Performing the localization Setting the initial startup language Responding to the button click Approach #2: Localization with XAML ResourceDictionary and LocBaml Adding a UICulture tag pair to the project configuration Setting the NeutralResourcesLanguage Attribute in the project Adding the ResourceDictionary to the project Merging the ResourceDictionary into the application Adding UId's to your XAML Extracting the Resources to a .CSV file using LocBaml.exe Generating the localized assembly and installing it Setting the current thread's UI Culture Replacing the button text with a DynamicResource Where are we now? Putting up a message box in response to clicking the button Adding a button handler Localizing to get the new string Approach #3: Linked Localized Baml streams with resgen compiled string resources Adding the .Resx files Compiling and linking to get your satellite resource DLL Fixing the button handler to use a Resource Manager A comparison of the three approaches of localizing WPF applications Conclusion References and Acknowledgements History IntroductionIt is this article's goal to show three different ways of localizing a Windows Presentation Foundation (WPF) .EXE application. Step by step instructions to re-create the three sample applications are provided so that the reader will not get trapped and frustrated by little details; all the i's need to be dotted and all the t's need to be crossed for all of this to work. A previous incarnation of this article only showed one approach which I had discovered; this approach, while having certain advantages will be presented last. After publishing the original article and posing a question on one of the Microsoft Forums, Michael Weinhardt (Microsoft) sent me a second, simpler way of doing localization using LocBaml.exe [LocBaml.exe is a program from Microsoft we use to localize Binary XAML code]. I was just about to jump into explaining how this works, when a collaborator, C.J. Baker, explained to me how he was doing localization without using LocBaml. His approach is by far the simplest way of doing localization and will be presented first. The end of the article will discuss the pros and cons of each approach. As a consequence, I hope that this article will help other developers who wish to, or are required to localize their WPF applications. BackgroundThe company I work for is in the process of developing a new software system, and we decided that WPF would be the presentation layer for the new system. Since we sell our products all over the world, being able to localize the UI has been an important asset in the past and is a requirement for the new software system we are designing. In all three scenarios the example is simple WPF .EXE application that has one main window containing a simple button. Clicking on the button brings up a localized message box. What You Will Need To Follow This Tutorial (Software Prerequisites)
Approach #1: Targeted Localization Without Using LocBaml.exeThe idea with approach #1 is pretty simple. When authoring the XAML file for the UI in question, replace the text with a Creating The WPF Application
Adding A Button To The WindowOpening the created Window1.xaml, we add a button and resize the main window a bit to make it smaller. The simplistic application xaml code is presented below: <Window x:Class="WPF_Localize.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF_Localize" Height="155" Width="194">
<Grid>
<Button Margin="30,20,35,26" Name="button1">Hello World</Button>
</Grid>
</Window>
We can now build the application with Ctrl-Shift-B and run it with Ctrl-F5. You should get a window that looks like this:
Adding the string resourcesNow that have the basic application built, we can start localizing it. Visual Studio has already created a file called
Replacing The Button TextNext, we need to do two things: add an extra namespace xmlns identifier to the Window attributes, and change the button text to point to the resource we have just added. When we do this, the code Window1.xaml code looks as follows: <Window x:Class="WPF_Localize.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF_Localize" Height="155" Width="194"
xmlns:properties="clr-namespace:WPF_Localize.Properties">
<Grid>
<Button Margin="30,20,35,26" Name="button1">
"{x:Static properties:Resources.MainButtonText}"
</Button>
</Grid>
</Window>
Compile, run, and you get this lovely dialog free of charge, courtesy of our friends at Microsoft:
Since we also feel sorry about the inconvenience, we need courage and a debugger to find a way out of this problem. Running in Debug mode within Visual Studio gets us access to the actual System.Windows.Markup.XamlParseException was unhandled
Message="Cannot create instance of 'Window1'
defined in assembly 'WPF_Localize, Version=1.0.2596.37261,
Culture=neutral, PublicKeyToken=null'. Exception has been thrown by the
target of an invocation. Error in markup file 'Window1.xaml' Line 1
Position 9."
Source="PresentationFramework"
etc...
This is actually a pretty good clue to the actual problem: another assembly called PresentationFramework is trying to create something, and the only thing we've added is the reference to the string. We can now take a look at the code-behind file for the resources. Yes, Visual Studio was kind enough to generate a class called Resources; you can find it beneath the [global::System.Runtime.
CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
...
internal static string MainButtonText {
get {
return ResourceManager.
GetString("MainButtonText", resourceCulture);
}
}
Getting A Public Resources ClassIt would be very convenient to have both a public class and public members, so that we can get at the string. Search and replacing Fortunately, another Codeproject author has already tackled the problem with a custom code generator that makes everything public. Using it is very simple; close Visual Studio, download the installer, run it, and make one more change to the Resources.resx properties. You can find the article and its download here. Now locate the file
If you do a build or if you change a string and save the .resx file, Visual Studio invokes the new code generator which produces the the code-behind file with the public class and public string property. // This class was auto-generated by the StronglyTypedResourceBuilderEx
// class via the ResXFileCodeGeneratorEx custom tool.
// To add or remove a member, edit your .ResX file then rerun the
// ResXFileCodeGeneratorEx custom tool or rebuild your VS.NET project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute
("DMKSoftware.CodeGenerators.Tools.StronglyTypedResourceBuilderEx", "2.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public sealed class Resources {
...
public static string MainButtonText {
get {
return ResourceManager.
GetString("MainButtonText", _resourceCulture);
}
If you now attempt to look at your Window in the designer view, the limitations of the Visual Studio 'Orcas' extension become apparent:
However, the latest incarnation of Microsoft Expression/Blend (Beta 2) seems to handle our window fine; if you run the program (which you can by hitting F5 in Expression) you will get your normal English button text being read dynamically from the resource.
Performing The LocalizationBut we are not done yet. We have yet to localize the program into another language, and address what happens when we actually click the button. Let us proceed with the following steps:
If you now examine the Resources.de-DE.Designer.cs file, you will notice that it is completely empty! I struggled with this, and thought that the new Resource Generator tool was broken. It turns out it is not; this is done on purpose. The code-behind file is needed only once, so it only gets generated once. The tool pays attention to the naming convention [basename].[Locale Identifier].resx; if there is a valid locale identifier, the code-behind does not get generated. Another thing to mention, is the fact that Visual Studio is smart enough to generate our satellite DLL called Setting The Initial Startup LanguageNow we only need to do one more thing to get our German version of the button text that we have localized: a call to set Culture property in the automatically generated code-behind file. At this point, it should be noted that this setting of the language/culture needs to happen before the WPF Window/Page is created. The safest place to put this code is in the constructor of the application, because we can be fairly sure that this will be executed before any Window/WPF code gets to run. public partial class App : System.Windows.Application
{
public App()
{
WPF_Localize.Properties.Resources.Culture = new CultureInfo("de-DE");
}
}
Running the program now gives us the much anticipated German version of the program. Since we did not localize the application title, it remains the same for both the English and German versions. Its also possible to start out with one culture, and set the Responding To The Button ClickWhat now remains is to add a private void OnButtonClick(object sender, RoutedEventArgs e)
{
MessageBox.Show(Properties.Resources.MessageBoxText);
}
Of course, in order for this to compile, we need to add a MessageBoxText string to both the
Approach #2: Localization with a XAML ResourceDictionary, and LocBamlThe idea behind solving the problem with Approach #2 is a slightly different one. It involves coercing Visual Studio in creating a satellite DLL, creating a Create a WPF Window application, as shown in Approach #1. The window should have the "Hello World" button ready to go, as described above. Adding a <UICulture> Tag Pair To The Project ConfigurationWe now need to adjust the project configuration, so that the LocBaml tool can be used. For this, close the solution of the project that was just created. This involves opening up the project's .csproj file and adding a <UICulture>en-US</UICulture> line to the <PropertyGroup> section of the file. To open the .csproj file, choose File->Open File... add the line as per image below.
Now save the .csproj file, close the window, and reload the original solution. Setting The NeutralResourcesLanguage Attribute In The ProjectThere are two ways to set the [assembly: NeutralResourcesLanguage("en-US",
UltimateResourceFallbackLocation.Satellite)]
You might ask what this is for? This attribute gives instructions to the CLR loader on how to find the right resource DLL for the current culture, and what to do if it can't find a matching culture. It also basically says to the system: If you can't find a matching culture resource DLL, just use the one with the "en-US" culture. The following link gives a bit more info on the resource fallback process. Recompile and re-run your application, to make sure it still runs OK. If you have edited the .csproj file correctly in step 2 you should get a US culture resource DLL built along with your .EXE file, sitting in its own "en-US" folder under [PathToProject]bin\Debug. Looking at the output window (View->Output) will also show you that AL.exe, the .NET assembly linker tool was invoked to create your satellite resource DLL. Adding the ResourceDictionary to the projectRight click the WPF_Localize project, and add a new file of type <ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="buttonText">Hello World!</system:String>
</ResourceDictionary>
Merging The ResourceDictionary Into The ApplicationBefore we can use the string we have just created in XAML, we need to make it "public" for other XAML code. To do this, modify the App.xaml so that it looks like this: <Application x:Class="StringLocalizationSample.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Window1.xaml" >
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="StringResources.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
Adding UId's To Your XamlThe next step is to go through your .xaml file, and add a UId attribute all fields that need to be localized. That was the bad news. The good news is that a tool, msbuild, can do this for you automatically. This is how to do it:
Check to make sure that the UId's have been embedded in the .xaml file by looking at the xaml source: <Window x:Uid="Window_1" x:Class="WPF_Localize.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF_Localize" Height="155" Width="194">
<Grid x:Uid="Grid_1">
<Button x:Uid="button1" Margin="30,20,35,26" Name="button1">Hello World
</Button>
</Grid>
</Window>
Compile the project to make sure nothing has been broken. There is also a "CheckUid" target to check for duplicates and another target to remove the UIDs if you don't need them anymore. Extracting The Resources To A .CSV File Using Locbaml.exeIf you have not done so already, download LocBaml.exe from the project files (see link at the top of the page). [The file is in the top level folder of the .zip file]. If you are interested in recompiling LocBaml yourself, you'll need the WPF samples from the SDK; LocBaml is located under [PathToWPFSamples]\GlobalizationLocalization\LocBaml. Follow the following steps to extract the localizable resources to a .CSV file.
If you have done things correctly, you should be able to see the .csv file and open it in Excel. Be mindful that not all versions of Excel support saving in Unicode, which could be important if you plan to translate to a far-east language. In this case, use a unicode-compatible text editor. If you don't need to use Unicode, you can save as a normal .CSV file. The file that was just produced by the tool [you will get a garbage file if you did not follow the above instructions first - it is usually not impossible to find the source of the problem by debugging LocBaml.exe] has the following layout:
We will go ahead and modify cell G2, the Window Title and cell G5, the button text, filling the values "Localisation avec WPF", and "Salut tout le monde", the French equivalents. Save the spreadsheet as .CSV[Windows]. You will need to do this for every language you intend to support. Generating The Localized Satellite Dll And Installing ItNext, we need to combine the translated info with the original US satellite DLL to generate our french satellite DLL. Again, this is done through a command line command using locbaml.
Setting the Current Thread's UI CultureWe have completed the localization of the code, however if you attempt to run it, you probably won't see our translated UI. Why not, you might ask?. Only French users will see it, if their default locale is French. How could one programmatically switch between languages?. The trick here is to change the current Thread's CurrentUICulture before any Windows are displayed. This must be done in the constructor of the App class as shown below: public App()
{
string culture = "fr-FR";
// culture = "en-US";
System.Threading.Thread.CurrentThread.CurrentUICulture =
new System.Globalization.CultureInfo(culture);
System.Threading.Thread.CurrentThread.CurrentCulture =
new System.Globalization.CultureInfo(culture);
}
You could, of course read the "fr-FR" string from an application .config file that is stored on a per user basis. Replacing The Button Text With a DynamicResourceNext, we need to replace our static "Hello World" button text with a reference to our newly created ResourceDictionary. The resulting code looks like this: <Window x:Uid="Window_1" x:Class="WPF_Localize.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF_Localize" Height="154" Width="194">
<Grid x:Uid="Grid_1">
<Button x:Uid="Button_1"
Margin="30,20,35,26"
Name="button1"
Content="{DynamicResource buttonText}">
</Button>
</Grid>
</Window>
Where Are We Now?What we have now is a localized application that has all UI elements displayed to the user through XAML translated into the culture(s) we have decided to support. The good thing about this approach is that we can decide to support a single language now, and add other languages after the product has been released. In theory if we have a good XAML design, we just need a new translation, and re-generate the satellite DLL appropriate for the culture we are targeting. We'll also need to update our application installer or add-on installer to install the DLL in the correct folder. As soon as the user switches the current thread to the new locale, and restarts, the UI will display the new localized UI. Of course your new UI will display correctly only if you allow WPF to use its auto-layout features; remember that some label text can be significantly longer in another language. The following MSDN blog as a good article entitled "Best Practices for globalization and Localization in WPF", which speaks to XAML UI design as it relates to localization and globalization; the article can be found here. In practice, it is always important to test the application, and have a native speaker review the finished product; translation glitches do happen and testing is the only way to make sure you end up with a high quality product. Putting Up A Message Box In Response To Clicking The ButtonThe next thing I would like my application to do is to respond to the mouse click and to bring up a little message box. This leads us to the next problem: How do I localize the Adding A Button HandlerFirst, lets add a message handler to the system. Current versions of Visual Studio 2005 are a bit primitive in that regard, users must create the message handler manually. Gone are the "Double Click on the button to get a default handler written automatically. Adding the handle is a two step process, adding a click handler in the XAML, and writing the handler in the code-behind file. This is what the Window1.xaml file looks like after adding the click handler: <Window x:Uid="Window_1" x:Class="WPF_Localize.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF_Localize" Height="154" Width="194">
<Grid x:Uid="Grid_1">
<Button x:Uid="Button_1"
Margin="30,20,35,26"
Name="button1"
Content="{DynamicResource buttonText}"
Click="OnButtonClicked">
</Button>
</Grid>
</Window>
And the corresponding code-behind file: // add this to the Window1.xaml.cs, below the Window1() // constructor
private void OnButtonClick(object sender, RoutedEventArgs e)
{
string localizedMessage = (string)Application.
Current.FindResource("messageText");
MessageBox.Show(localizedMessage);
}
Localizing Again To Get The New StringWe have now used a new string resource, called 'messageText' but we have not made the addition of the string to the file Approach #3: Linking Localized Baml Streams With Resgen Compiled String ResourcesThis approach shares many steps with Approach #2, but it does not use a WPF
Now run your project. If you have done things correctly, you should get a French main UI, and an English message box text. Adding The .Resx FilesWe now need a string table to hold all the strings that we are going to localize. For now we only have one message, but since we intend to support the en-US (which is also the invariant culture) and French (fr-FR), we need two resource files. Let's do this now. Follow the following steps to get your new French resource files:
Compiling And Linking To Get Your Satellite Resource DllNow that we have the .resX files that we need (remember, it is important to use the correct culture identifiers), it is time to compile and link. The goal of the following steps is to get a satellite resource DLL that has both the localized .BAML (compiled XAML) and the string table as assembly module resources. All of this only works if all of the details are correct, an explanation follows: cls
resgen .\Properties\Resources.resx
copy .\Properties\Resources.resources bin\debug\en-US
cd .\bin\Debug\en-US
copy ..\wpf_localize.exe .
REM COPY LOCBAML.EXE to C:\ , otherwise this won't work!
copy C:\LocBaml.exe .
locbaml /generate ..\..\..\obj\debug\WPF_Localize.g.en-US.resources
/tran:trans.csv /cul:en-US /out:.
ren Resources.resources WPF_Localize.Properties.Resources.en-US.resources
al /template:"..\WPF_Localize.exe" /embed:WPF_Localize.g.en-US.resources
/embed:WPF_Localize.Properties.Resources.en-US.resources /culture:en-US
/out:WPF_Localize.resources.dll
cd ..\..\..
The batch file, which is run from the project directory (you can set a post-build step in Visual Studio) does the following. We first compile the string resources. We then copy the main executable to the working folder, which needs to be done so that the tools run properly. Then we copy locbaml.exe into the current folder; woe to you if you forget to do this. We now use locbaml.exe to localize the compiled xaml resources [WPF_Localize.g.en-US.resources] that were generated automatically during the build. We now need rename the actual string resource, so that the ResourceManager from the .exe can actually find the strings as resources. I found out the correct naming sequence by writing and localizing a command line application with strings only, and disassembling the resource assembly manifest. Finally we call the linker, al.exe; we simply ask to combine the string resources along with the translated .baml resources to generate our satellite resource dll. You would add a /keyfile:[path to keyfile.snk] to the al command line to sign you satellite resource. Below, I present the batch file, called doit_fr.bat used to generate the French resource assembly. Note that it points to the US resources when generating the French resources, the US resources are treated as a template, which is copied and localized. cls
resgen .\Properties\Resources.fr-FR.resx
copy .\Properties\Resources.fr-FR.resources bin\debug\fr-FR
cd .\bin\Debug\fr-FR
copy ..\wpf_localize.exe .
REM Copy LOCBAML.EXE to C:\ for the script to work
copy C:\LocBaml.exe .
REM Note: We are referring to the WPF_Localize.g.en-US.resources
REM as the template, even though we are generating the French
locbaml /generate ..\..\..\obj\debug\WPF_Localize.g.en-US.resources
/tran:trans.csv /cul:fr-FR /out:.
ren Resources.fr-FR.resources WPF_Localize.Properties.Resources.fr-FR.resources
al /template:"..\WPF_Localize.exe" /embed:WPF_Localize.g.fr-FR.resources
/embed:WPF_Localize.Properties.Resources.fr-FR.resources /culture:fr-FR
/out:WPF_Localize.resources.dll
cd ..\..\..
Be careful, your diligently crafted resource DLL is usually overwritten [with the US containing-baml resources only] if you compile, its a good idea to do this as a post build step. I suggest that you run the application outside of Visual Studio - this guarantees that your satellite DLL's willl not be overwritten. Fixing The Button Handler To Use A Resource ManagerThe last step, now that we have the resource DLL is to actually use them. Since Visual Studio has already created a Properties.Resources class that exposes our string table, we can just use it. Remember, if you want to change the locale, this is done in the App.xaml.cs. private void OnButtonClick(object sender, RoutedEventArgs e)
{
MessageBox.Show(WPF_Localize.Properties.Resources.String1);
}
A comparison of the three approaches to localizing WPF applicationsThe first approach uses The second approach used a The third approach uses both a .Resx file for strings, and LocBaml for localizing the UI and uses the .NET linker to link the resources together. This is by far the most difficult approach, as it requires some understanding on how resources are named and used. Its advantages are that the Visual Studio designer works, and that you don't need to change your XAML file at all. Another advantage of using this method is that you can first finish your application, and do the localization of the UI afterwards [You still need to isolate all strings given to the user in code-behind files] Disadvantages include having to translate the entire UI because we are using LocBaml. Its also not possible to switch languages on the fly with this approach. ConclusionIts a good idea to worry about localisation early on in a project, developing with at least one other language or pseudo-language. By this I mean that you could add "french-" to the beginning of every identifier; this way you don't need to know french to see if the UI changed. UI designers can also make sure that longer text wrapping will be OK - although WPF is way better than Forms at doing layout, UI designers must still design with auto-layout in mind. The advantage of installing the localized DLL's in their own folder is that you can decide to support a new language just by installing the appropriate resource DLL later on. Finally, if you know of a better/easier way to do this type localization, please, let me know! References and AcknowledgementsThanks to Wei-Meng Lee and his article on www.devx.com which shows how to do a similar thing with Visual Basic .Net. Thanks to C.J. Baker and Michael Weinhardt for providing approaches to solving the WPF localization problem. History
| ||||||||||||||||||||