Category Archives: Localizing

Multilingual App Toolkit (MAT)

I’ve previously looked at the MAT in the post Adventures in UWP – Globalization & Localization UWP – Globalization & Localization. I’ve also used this within mobile applications using Xamarin Forms which I was about to update, but then realised I’d forgotten how this stuff worked.

Requirements

Before we start, we need to install a couple of things.

  • From Visual Studio 2019 (if not already installed) select Extensions | Manage Extensions and download the Multilingual App Toolkit. You’ll need to restart Visual Studio for the installation to take place.
  • Install (if not already installed) the Multilingual app toolkit 4.0 Editor as this is extremely useful for managing our translated files, but is NOT required.

Note: This does not currently seem to be supported in Visual Studio for Mac.

Mobile Application

Let’s start by creating a Xamarin Forms application to see how things work.

  • Create a Mobile App (Xamarin.Forms) in Visual Studio
  • Add a Resources folder to the shared project and then add a Resource File item to this folder, named AppResources.resx
  • Right mouse click on the shared project and select Properties, now select Package and towards the bottom of this screen, change the Assembly neutral language to your default language – for example mine’s English (United Kingdom). In other words our project culture will be set to en-GB
  • Enable MAT by selecting Tools | Multilingual App Toolkit | Enable on your solutions

At this point we now have the basics in place to begin localizing our application. We can now start adding strings to our AppResources.resx or alternatively we could start adding new languages, so let’s do that.

  • Right mouse click on your shared project and select Multilingual App Toolkit and from this select, Add translation languages…

Note: You may get and error regarding the need to install translation providers, this requires us to add an Azure subscription, but we don’t actually need this to use the tools, so just ignore this error for now.

From the Translation Languages check on any languages you want to support (you can also add languages at any point so don’t worry if you need to add more at a later date).

In my case I’m going to add German [de] and French (France) [fr-FR]. Once you press OK you’ll see two additional AppResources files added to the Resources folder, AppResources.de.resx for German and AppResources.fr-FR.resx for French.

You’ll also see the MultiligualResources folder with two .xlf files, one for German and one for French, these are used by MAT to generate our new AppResources files.

Note: Do not edit the localized AppResources.*.resx files by hand. Only AppResources.resx should be edited.

Editing our resources

Now that we have the xlf files and the default culture AppResources.resx file, we’ll want to start adding some strings.

Open the AppResources.resx file in Visual Studio and add some named strings, the Name being the identifier or key, the Value being the string to display and we can also add comments which might be useful for somebody who handles the subsequent translations.

So I’m going to create a basic login page, hence we’ll add the following

  • Name: Welcome, Value: Welcome to the application
  • Name: Login, Value: Login
  • Name: Username, Value: Username or email address
  • Name: Password, Value: Enter your password

Now build your application before we look to create some translations.

There’s a couple of ways to proceed with this, the simplest is as follows

  • From Visual Studio. Select you MultilingualResources folder or each .xlf in turn.
  • Right mouse click on the folder or file then select Multilingual App Toolkit | Generate Machine Translations.

You may find that some values are not translated, for example in my case Welcome to the application was not translated, we can edit the de.xlf file directly. In this case I change the target element to Willkommen bei der Anwendung and changed the target state to needs-review-translation

Once you’ve made your changes then build the project and MAT will generate the AppResources.*.resx files.

The second way to handle the translations and probably one that would be useful for people who are working on translations of your application. In this case we use the Multilingual Editor that was installed earlier. Open each of your xlf files in turn. For each one we’ll see a list of the strings as entered into the AppResources.rex file. So let’s concentrate on editing the .de.xlf German translation file.

  • Having opened the .de.xlf file, select the Strings tab at the bottom of the screen to ensure all the strings were added, now here we could manually translate each row by selecting the each row in turn, then changing the Translation in the editor in the middle of the editor UI.
  • If you edit the translation yourself then the State on the right of the screen will switch to Translated. This state will tell the MAT editor whether it should try to translate the value. If the user has marked the state as Translated we can assume this is correct and the editor will not attempt to translate the string using it’s generate translation option. You can change this State back to New if you want the auto-translation to take place.
  • If you prefer or like me you’re simply going to use something like Bing or Google to do the translations, i.e. https://www.bing.com/translator/. Then the Editor offers the Translate button. Clicking this will generate translations for all the text and mark them with the State Needs Review which is basically saying that the translation was made but needs somebody to review whether this makes sense in each language. You can leave this State as is or change to Translated if you’re happy with these translations.
  • Once completed, save the file(s) and build your solution to get the AppResources.*.rexr updated.

Displaying our translations in an application

Note: This post has been updated as the previously used Multilingual plugin is no longer required.

We can display translations is XAML using the AppResources generated code, for example

Title="{x:Static resources:AppResources.Settings}

and in code just as easily we use

var title = AppResources.Settings;

To test your translations you can set things up in code, i.e.

In your App.xaml.cx, after the InitializeComponent you can place the following code to set the culture to that of the device

var culture = CrossMultilingual.Current.DeviceCultureInfo;
AppResources.Culture = culture;

If you want to test your German translation, for example, replace these two lines with

var culture = new CultureInfo("de");
AppResources.Culture = culture;
CrossMultilingual.Current.CurrentCultureInfo = culture;

See https://github.com/putridparrot/blog-projects/tree/master/MatTestMobile for a sample application.

If you prefer to use the Android Emulator to set your language – which ofcourse makes a lot of sense, then go to Android Settings | System | Languages & input | Languages then Add a language if the one you want is not listed, then drag and drop that language to the top of the Languages list and Android will switch to that language.

A quick look at WPF Localization Extensions

Continuing my look into localizing WPF application…

I’ve covered techniques used by theme’s to change a WPF application’s resources for the current culture. I’ve looked at using locbaml for extracting resources and localizing them. Now I’m looking at WPF Localization Extensions.

The WPF localization extension take the route of good old resx files and markup extensions (along with other classes) to implement localization of WPF applications.

Getting Started

  • Create a WPF application
  • Using NuGet, install the WPFLocalizeExtension
  • Open the Properties section and copy Resources.resx and rename the copy Resources.fr-FR.resx (or whatever culture you wish to support)

As with my other examples of localizing WPF applications, I’m not going to put too much effort into developing a UI as it’s the concepts I’m more interested in at this time.

First off let’s add the following XAML to MainWindow.xaml within the Window namspaces etc.

xmlns:lex="http://wpflocalizeextension.codeplex.com"
lex:LocalizeDictionary.DesignCulture="en"
lex:LocalizeDictionary.OutputMissingKeys="True"
lex:ResxLocalizationProvider.DefaultAssembly="Localize2"
lex:ResxLocalizationProvider.DefaultDictionary="Resources"        

The DefaultDictionary needs to have the name of the resource file, whether it’s Resources (exluding the .resx) or if you’ve created one names Strings or whatever, just exclude the extension.

The DefaultAssembly is the name of the assembly to be used as the default for resources, i.e. in this case it’s the name of my project’s assembly.

Next up, within the Grid, we’re going to have this

<TextBlock Text="{lex:Loc Header}" />

Header is a key – obviously on a production ready application with more than one string to translate, we’d probably implement a better naming convention.

A tell tale sign things are wrong is you’ll see the text displayed as Key: Header, this likely points to one of the namespace values being incorrect, such as the DefaultDictionary name.

That’s it for the UI.

In the Resources.resx, add a string named Header and give it a Value, for example mine should default to English and hence will have Hello as the value. In the Resources.fr-FR.resx, add a Header name and give it the value Bonjour.

That’s the extent of our strings for this application. If you run the application you should see the default Resources string, “Hello”. So now let’s look at testing the fr-FR culture.

In App.xaml.cs create a default constructor and place this code within it

LocalizeDictionary.Instance.SetCurrentThreadCulture = true;
LocalizeDictionary.Instance.Culture = new CultureInfo("fr-FR");

Run the application again and the displayed string will be take from the fr-FR resource file.

To allow us to easily switch, at runtime, between cultures, we can use the LocalizeDictionary. Here’s a combo box selector to do this (taken from the sample source on the WPF Localization Extensions github page).

<ComboBox ItemsSource="{Binding Source={x:Static lex:LocalizeDictionary.Instance}, Path=MergedAvailableCultures}"
   SelectedItem="{Binding Source={x:Static lex:LocalizeDictionary.Instance}, Path=Culture}" DisplayMemberPath="NativeName" />

We also need to be able get strings from the selected resource in our code, here’s a simple static class from StackOverflow which allows us to get a string (or other type) from the currently selected resources

public static class LocalizationProvider
{
    public static T GetLocalizedValue<T>(string key)
    {
        return LocExtension.GetLocalizedValue<T>
(Assembly.GetCallingAssembly().GetName().Name + ":Resources:" + key);
    }
}

The string “Resources” should obviously be changed to the name of your resource files (for example if you’re using just strings in a “Strings” resource etc).

This is certainly simpler to set-up than locbaml, the obvious drawback with this approach is that the strings at design time are not very useful. But if, like me, you tend to code WPF UI primarily in XAML then this probably won’t concern you.

Localizing a WPF application using locbaml

This post is going to mirror the Microsoft post How to: Localize an Application but I’ll try to add something of value to it.

Getting Started

Let’s create a WPF Application, mine’s called Localize1. In the MainWindow add one or more controls – I’m going basic at this point with the following XAML within the Window element of MainWindow.xaml

<Grid>
   <TextBlock>Hello</TextBlock>
</Grid>

According to the Microsoft “How To”, we now place the following line in the csproj

<UICulture>en-US</UICulture>

So locate the end of the <PropertyGroup> elements and put the following

<PropertyGroup>
    <UICulture>en-GB</UICulture>
</PropertyGroup>

See AssemblyInfo.cs comment on using the UICulture element also

Obviously put the culture specific to your default locale, hence mine’s en-GB. Save the altered csproj and ofcourse reload in Visual Studio if you have the project loaded.

The inclusion of the UICulture will result (when the application is built) in a folder en-GB (in my case) with a single satellite assembly created, named Localize1.resources.dll.

Next we’re going to use msbuild to generate Uid’s for our controls. So from the command line in the project folder of your application run

msbuild /t:updateuid Localize1.csproj

Obviously replace the project name with your own. This should generate Uid’s for controls within your XAML files. They’re not very descriptive, i.e. Grid_1, TextBlock_1 etc. but we’ll stick with following the “How To” for now. Ofcourse you can implement your own Uid’s and either use msbuild /t:updateuid to generate any missing Uid’s or ignore them – and have Uid’s for those controls you wish to localize only.

We can also verify that Uid’s exist for our controls by running

msbuild /t:checkuid Localize1.csproj

At this point we’ve generated Uid’s for our controls and msbuild generated as part of the compilation a resource DLL for the culture we assigned to the project file.

We now need to look at generating alternate language resource.

How to create an alternate language resource

We need to download the LocBaml tool or it’s source. I had problems locating this but luckily found source on github here.

So if you don’t have LocBaml already, download and build the source and drop the locbaml.exe into your bin\debug folder. Now run the following command

locbaml.exe /parse en-GB/Localize1.resources.dll /out:Localize1.csv

You could ofcourse copy the locbaml.exe to the en-GB folder in my example if you prefer. What we’re after is for locbaml to generate our Localize1.csv file, which will be then add translated text to.

Here’s what my csv file looks like

Localize1.g.en-GB.resources:mainwindow.baml,Window_1:Localize1.MainWindow.$Content,None,True,True,,#Grid_1;
Localize1.g.en-GB.resources:mainwindow.baml,Window_1:System.Windows.Window.Title,Title,True,True,,MainWindow
Localize1.g.en-GB.resources:mainwindow.baml,Window_1:System.Windows.FrameworkElement.Height,None,False,True,,350
Localize1.g.en-GB.resources:mainwindow.baml,Window_1:System.Windows.FrameworkElement.Width,None,False,True,,525
Localize1.g.en-GB.resources:mainwindow.baml,TextBlock_1:System.Windows.Controls.TextBlock.$Content,Text,True,True,,Hello

If you view this csv in Excel you’ll see 7 columns. These are in the following order (decriptions copied from the How To document)

  • BAML Name. The name of the BAML resource with respect to the source language satellite assembly.
  • Resource Key. The localized resource identifier.
  • Category. The value type.
  • Readability. Whether the value can be read by a localizer.
  • Modifiability. Whether the value can be modified by a localizer.
  • Comments. Additional description of the value to help determine how a value is localized.
  • Value. The text value to translate to the desired culture.

For our translators (if we’re using an external translator to localize our applications) we might wish to supply comments regarding expectations or context for the item to be localized.

So, go ahead and translate the string Hello to an alternate culture, I’m going to change it to Bonjour. Once completed, save the csv as Localize1_fr-FR.csv (or at least in my case translating to French).

Now we want to get locbaml to generate our new satellite assembly for the French language resources, so again from the Debug folder (where you should have the generated csv from the original set of resources as well as the new fr-FR file) create a folder named fr-FR (or whatever your new culture is).

Run the locbaml command

locbaml.exe /generate en-GB/Localize1.resources.dll /trans:Localize1_fr-FR.csv /out:fr-FR /cul:fr-FR

This will generate a new .resource.dll based upon the Localize1.resource.dll but using our translated text (as specified in the file Localize1_fr-FR.csv). The new DLL will be written to the fr-FR folder.

Testing our translations

So now let’s see if everything worked by testing our translations in our application.

The easiest way to do this is to edit the App.xaml.cs and if it doesn’t have a constructor, then add one which should look like this

public App()
{
   CultureInfo ci = new CultureInfo("fr-FR");
   Thread.CurrentThread.CurrentCulture = ci;
   Thread.CurrentThread.CurrentUICulture = ci;
}

you’ll obviously requires the following using clauses as well

using System.Globalization;
using System.Threading;

We’re basically forcing our application to use fr-FR by default when it starts. If all went well, you should see the TextBlock with the text Bonjour.

Now change the Culture to one which you have not generated a set of resources for, i.e. in my case I support en-GB and fr-FR, so switching to en-US and running the application will have an undesirable affect, i.e. an IOException occurs, with additional information “Cannot locate resource ‘mainwindow.xaml'”. This is not very helpful, but basically means we do not have a “fallback” or “neutral language” resource.

Setting a fallback/neutral language resource

We, obviously don’t want to have to create resource files for every possible culture. What we need is to have a fallback or neutral language resource which is used when a culture is not supported via a translation DLL. To achieve this, open AssemblyInfo.cs and locate the commented out line which includes NeutralResourcesLanguage or just add the following either way

[assembly: NeutralResourcesLanguage("en-GB", UltimateResourceFallbackLocation.Satellite)]

obviously replace the eb-GB with your preferred default language. Run the application again and no IOException should occur and the default resources en-GB are used.

What about strings in code?

Well as the name suggests, locbaml is really localizing our BAML and when our WPF application starts up, in essence it loads our XAML with the one’s stored in the resource DLL.

So the string that we’ve embedded in the MainWindow.xaml, is not separated from the XAML (i.e. it’s embedded within the TextBlock itself). So we need to move our strings into a shared ResourceDictionary file and reference them from the UI XAML. For example in our App.xaml let’s add

<ResourceDictionary>
   <system:String x:Uid="Header_1" x:Key="Header">TBC</system:String>
</ResourceDictionary>

Now, change our MainWindow.xaml to

<TextBlock Text="{StaticResource Header}" />

This allows us to use FindResource to get at the string resource using the standard WPF FindResource method, as per

var headerString = Application.Current.FindResource("Header");

This appears to be the only “easy” way I’ve found of accessing resources and requires the resource key, not the Uid. This is obviously not great (if it is the only mechanism) as it then requires that we maintain both Uid and Key on each string, control etc. However if we ensure strings are stored as string resources then this probably isn’t too much of a headache.

References

https://msdn.microsoft.com/en-us/library/ms745650.aspx
https://msdn.microsoft.com/en-us/library/ms788718(v=vs.110).aspx

Localizing a WPF application using dynamic resources

There are several options for localizating a WPF application. We can use resx files, locbaml to create resource DLL’s for us, or why not just use the same technique used in theme’s, i.e. DynamicResource and ResourceDictionary’s.

In this post I’m going to look at the DynamicResource and ResourceDictionary approach to localization. Although this technique can obviously be used with images etc., we’ll concentrate on dealing with strings, which usually are the main area of localization.

Let’s start with some code

Create a simple WPF application which will use the standard DynamicResource to set text on controls. We will create a “default” set of string resources to allow us to develop our initial application with and we will create two satellite assemblies which will contain the same string resources for en-GB and en-US resources.

  • Create a WPF Application
  • Create a class library named Resources_en-GB and another class library named Resources_en-US
  • Add the references PresnetationCore, PresentationFramework, WindowsBase and System.Xaml to these class libraries
  • Change the class library output folders for Debug and Release to match those for the WPF application so the DLL’s will be built to the same folder as the application

Now in the WPF application, add a ResourceDictionary, mine’s named Strings.xaml and this will act as our default/design-time dictionary, here’s mine

<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="Header">TBC</system:String>
    
</ResourceDictionary>

and my MainWindow.xaml looks like this

<Window x:Class="WpfLocalizable.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfLocalizable"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <TextBlock Text="{DynamicResource Header}" FontSize="24"/>
        <StackPanel Orientation="Horizontal" VerticalAlignment="Bottom">
            <Button Content="US" Width="100" Margin="10" />
            <Button Content="GB" Width="100" Margin="10" />
        </StackPanel>
    </StackPanel>
</Window>

I didn’t say this was going to be exciting, did I?

Now if you run the application as it currently stands, you’ll see the string TBC – our design-time string.

Next, copy the Strings.xaml file from the application to both the Resources_en-GB and Resources_en-US and change the string text to represent your GB and US strings for header – I used the word Colour in GB and Color in US – just to demonstrate the common language differences.

Now if you build and run the application, you’ll see the default header text still, so we now need to make the application set the resources at start-up and allow us to easily switch them. So change the buttons in MainWindow.xaml to these

<Button Content="US" Width="100" Margin="10" Click="US_OnClick"/>
<Button Content="GB" Width="100" Margin="10" Click="GB_OnClick"/>

We’re going to simply use code behind for changing the resource in this demo. So in MainWindow.xaml.cs add the following code

private void LoadStringResource(string locale)
{
   var resources = new ResourceDictionary();

   resources.Source = new Uri("pack://application:,,,/Resources_" + locale + ";component/Strings.xaml", UriKind.Absolute);

   var current = Application.Current.Resources.MergedDictionaries.FirstOrDefault(
                    m => m.Source.OriginalString.EndsWith("Strings.xaml"));


   if (current != null)
   {
      Application.Current.Resources.MergedDictionaries.Remove(current);
   }

   Application.Current.Resources.MergedDictionaries.Add(resources);
}

private void US_OnClick(object sender, RoutedEventArgs e)
{
   LoadStringResource("en-US");
}

private void GB_OnClick(object sender, RoutedEventArgs e)
{
   LoadStringResource("en-GB");
}

and finally in the constructor let’s default to en-GB, so simply add this line after the InitializeComponent

LoadStringResource("en-GB");

Now run the application, be default you should see en-GB strings and then press the US button to see the en-US version etc.

Finishing touches

In some situations we might want to switch the languages strings used via an option (very useful when debugging but also in you’re natural language is not the same as the default on your machine). In most cases, we’re likely to want to switch the language at start-up to match the machines’s culture/language.

Using ResourceDictionary might look a little more complex than CSV files, but should be easy for your translators to use and being, ultimately, XML – we could ofcourse write a simple application to allow the translators to view strings etc. in a tabular format.

We can deploy as many or as few localized resources as we need on a machine.

References

Checkout this a useful document WPF Globalization and Localization Overview from Microsoft.