Walkthrough - Localizing a WPF project

For this tutorial, I looked for a suitable WPF project on GitHub, in order to demonstrate Multi-Language with a real world application. The specific application was not very important, but I wanted a standalone application which would run without a back-end server.

The project I am using is the AppsTracker by Marco Devcic, from the repository:
https://github.com/deva666/AppsTracker

After you have installed Multi-Language for Visual Studio, there will be a new command in the Tools menu in Visual Studio.

Screenshot of the Multi-language command in the tools menu

This menu item activates Multi-Language and shows it in a tool-window. You can dock this window at the side of the main Visual Studio window, or drag it into a tab group, like other tool windows in Visual Studio. It's probably best to place it at the bottom, in a wide format.

Screenshot of Multi-Language window docked at the bottom of the main window

Your Visual Studio solution probably contains multiple projects. With Multi-Language you usually work with a single project. To get started, you must first select the project. There are two ways to do this.

From a dialog

When you select Multi-Language from the tools menu for the first time, it will show a list of the projects in a dialog.

Simply select the project and click on OK.

Screenshot of the Select Project dialog

In practice, I always disable this dialog using the checkbox at the bottom, and use the drop-down list shown below.

From a drop-down list

At the top of Multi-Language's main window, just below the toolbar, there is a drop down project list.

Click on one of the items to select a project.

Screenshot of the Project List

You can ignore the buttons in the items for now.

When you select a project for the first time, you must specify what the original language of the project is. Multi-Language will show a list of the languages supported by Windows.

By default, it only shows neutral languages, like English and Spanish.

If you want to specify the exact language, like English (United States) then uncheck the option Only show neutral cultures.

You can show additional columns, by clicking on the names above the list.

Screenshot of the original language selection

As a second step, you can select one of two ways to handle strings in the source code:

  • Named resources
  • ml_string()

Named resources is the standard method in Visual Studio. This is recommended for all new projects.

ml_string() is a custom implementation used in older versions of Multi-Language.

Screenshot of the source code option

Multi-Language will add some new files to the project:

Multi-Language will now scan the project. This is performed in two phases:

In the first phase, the elements in each XAML file are scanned for texts which may require translation. In the second phase, the source code of the project is scanned for texts which may require translation.

In both cases, this is almost certain to detect texts which should not be translated. We will be looking at how to handle these texts lower down.

Note:
This actually works better with WinForms projects, because all localizable properties are marked with the LocalizableAttribute. In WPF, it is not so clear cut.

The results of the scan are shown in the Multi-Language window in two separate tabs for "Controls" and "Source Code".

The grids are organized as an expandable tree structure. If you want to expand all nodes in the tree, you can so this via the +/- button on the toolbar.

Screenshot the scan results showing the controls and source code tabs

Now lets start selecting some texts for translation.

If you click on an element in either of the grids, the corresponding file will be opened and the text will be hightlighted in an editor window.

Below you can see that I have selected a line in the controls grid, following which it opens the designer window and highlights the appropriate text.

Screenshot showing a line selected in the controls grid and in the designer window

Similarly, when you select a line in the source code grid, it opens the file in the editor and selects the text.

Screenshot showing a line selected in the source code grid and in the editor

To select a text for translation, use the check boxes in the grid. Below you can see that I have selected 4 texts in the first XAML component.

Screenshot indicating where to select strings for translation

Already this shows some interesting points:

Background colors

The cells containing the selected texts have a blue background, which indicates that it is the original text from the project.

Here is a summary of some other styles that are used to indicate the status of the text or the translation:

Colour Meaning
Original text
Original text from the project.
Global database
Text from the translation memory.
Edited text
Text edited by the user.
Spreadsheet import
Text from a spreadsheet import.
Online translation
Text from online translation.
Out of date
Out of date translation.
Resource fallback
Text from resource fallback.

Icon for the flags colum Flags column

For two of the texts, the flags column contains the icon Different case flat which indicates that the text occurs multiple times in the project, but with different case.

In this example, the texts are "About apps tracker" and "about apps tracker".

This is not an error, it is just an indication that you might want to review the texts, and possibly to update one of them.

Bear in mind, that this difference might get lost in translation.

Resource name

Whenever you select a text for translation, a named resource will be generated in the file Resources.resx. The name of the resource is generated automatically.

For example, the resource name for the text "All rights reserved" is All_rights_reserved.

If, for some reason, you are not happy with the resource name, you can edit in the grid, and the usage in the project will be updated.

ID column

The ID column shows the internal numeric ID assigned to the string. In most cases this uninteresting for user.

To add a new language to the project, click on the + symbol on the Multi-Language toolbar.

Screenshot the plus symbol on the multi-language toolbar

This brings up a dialog very similar to the one used to select the project's original language.

Screenshot the add language dialog

Select the new language from the list.

In this case I have selected German.

At the bottom of the dialog, there are some options related to automatic translation of the texts. We will discuss these below.

Click OK to close the dialog and add the selected language to the project.

Some languages, such as German or French, are considered to be neutral languages. The regional variations, such as German(Germany), German(Austria), French(France) or French(Canada) are referred to as specific languages. It's usually best to add the neutral language before adding a related specific language.

The .NET framework uses a resource fallback mechanism. For example, if a resource is defined for German, but not for German(Austria), then an application using German(Austria) will automatically use the resource defined for German. The best practice is usually to specify all translations for the neutral language, and only regional differences (if any) for the specific language.

After adding the second language, Multi-Language will add a new column to the grids for the new language. Below you can see that the grid contains columns for English and German.

Screenshot of the grids after adding a second language

The texts in light grey are the texts which would be selected by the resource fallback mechanism. If there is no German resource for a text, then the original text will be shown instead (in this case English).

In the screenshot above, you can see that two of the texts have already been translated into German.

The text "OK" hardly counts, because it has been translated into "OK" in German. However, the text "All rights reserved" has been translated into "Alle Rechte vorbehalten", which is a real translation.

In this case, the translations come from a global database of translations, which is also called the Translation Memory. This database if prefilled with some common terms, like "OK" and "Cancel". By default, any new translation that you enter is also stored to the global database.

If you look back at the screenshot of the dialog to select a new language, you will see that there is an option "Get translations from global database", which is selected by default.

Next to that option there are checkboxes for three online translation services:

If we had selected, for example, "Get translations from Google Translator", then the other texts would also have been translated, using the online translation service.

Note:
The Microsoft translator is only enabled if you provide your own API key.

The checkboxes for the online translation services are only enabled if the service supports translation from the original language to the new language.

There are multiple ways to enter translations:

The simplest way to enter a translation is to type it into the grid. This is great, if you - the programmer - speak that language, but has obvious limitations.

A second method, is to click in cell for the translation, and then hit F3 to bring up the translation memory dialog.

Screenshot of the translation memory dialog

The fields at the top show 1 the original text and 2 the translation. If the text has not yet been translated, then they will be the same.

In this case I used the button 3 "Google Translator" to translate the text, which is shown as the translation. If I click on OK then the translation will be saved.

If you want, then you can edit the translation before saving it. (In fact, you can also edit the original text before using the online translation, but that is for specialists.)

At the bottom of the dialog 4 is a set of texts generated from the global database of translations, showing translations containing one or more of the same words. This is not a particularly good example, because it only shows the word about.

This list is particularly useful for specialist vocabulary and for using consistent translations.

After using online translation, the texts are shown in the grid with a red background.

Screenshot of the controls grid showing online translations

This is meant to indicate the danger, that online translation might wrong. If you are translating a document, then the quality of online translation is now very good. However, in this case, we are translating short phrases or single words, with very little context. Whilst some translation might be fine, others might be completely wrong.

If you know that a translation is correct, click in the cell and then hit ALT+Enter, ALT+Up or ALT+Down to change the status to edited which has a white background.

If you have selected many texts, but not translated them, you can use online translation for all of the selected texts.

This function is available via the context menu on the column for the specific language.

Screenshot of the context menu for the google translator

We've already selected some texts in the XAML files. Selecting texts for translation in the source code is basically the same.

For example, the following screenshot shows three texts in the source code which should be tranlated.

Screenshot of the source code grid before selecting texts for translation

To select the texts for translation, we simply click on the checkbox in the source code grid. Multi-Language will generate a string resource in the project, and replace the string with a reference to the resourse. You can see this in the next screenshot.

Screenshot of the source code grid after selecting texts for translation

Accessing resources in WPF projects

In non-WPF projects, Multi-Language inserts a using statement for the Resources class and references the resources in the form Resources.<resource name>.

using AppsTracker.Presentation.Properties;
...
Resources.Wrong_current_password

This can lead to compilation errors in a WPF project, because any class which inherits from FrameworkElement has a Resources property which conflicts with the Resources class name.

For this reason, Multi-Language always inserts the fully qualified resource name in WPF projects, for example:

AppsTracker.Presentation.Properties.Resources.Wrong_current_password

Technically, Multi-Language might be able to determine if a class inherits from FrameworkElement and to use the shorter syntax where possible. This might be added in a future version.

Remember that when you click on an text in the grid, Multi-Language will automatically show the corresponging text in the editor.

If, on the other hand, you can already see a text in the editor, and you want to find the corresponding position in the Multi-Language window, then you can use the context menu.

Screenshot the context menu command

We already looked at how to select texts in a XAML file for translation, but we didn't look at exactly what changes were made to the XAML file.

On initialising the project for localization, Multi-Language added the file MLExtension.cs to the project. When you select a string for translation, the first thing Multi-Language does is to add the following namespace to the XAML file

xmlns:m="clr-namespace:MultiLanguageMarkup"

Multi-Language then replaces the text with the markup extension m:Lang, for example, the string

Title="About apps tracker"

is replaced with

Title="{m:Lang About_apps_tracker}"

where About_apps_tracker is the name of the resource string.

All the magic of handling localization, with support for runtime language switching, is handled in this markup extension.

One of the most critical parts of localization is to select the strings which require translation.
At the same time it makes sense, to mark the texts which do not require translation.

You can do this both in the source code and in the XAML code. Let's start with the source code.

In this application, the text "MASTER PASSWORD" does not require translation. To mark it as not requiring translation, click on the sun symbol in the grid.

Screenshot of the sun symbol in the source code tab

Note:
After clicking on the sun symbol, you may see a dialog asking you to select the default way to hide strings in the source code.
I suggest that you accept the default selection of using the comment //MLHIDE.

After clicking on the sun symbol, it will be replaced by a moon symbol which indicates that the string has been hidden.
You can see the moon symbol 1 in the following screenshot.

In the source code, the string is hidden by adding the comment //MLHIDE 2 to the end of the line.

Screenshot of the moon symbol in the source code tab

On the Multi-Language toolbar, there is a button which toggles between a sun and a moon symbol 3.

In the state shown above, the moon symbol signifies that hidden texts are visible in the grid.
If you click on the button, it will toggle to the sub symbol , and at the same time, the hidden texts will be removed from the grid.

So this is the general idea:
All texts in the project should be:

After that, you can actually hide the hidden texts using the toggle button on the toolbar.

This may seem like an unnecessary step, but the alternative is that you, or your colleagues, will repeatedly look at these texts and wonder whether they should be translated or not.

The //MLHIDE comment is not a perfect solution, but it is a simple solution. If there are multiple texts on the same line (for example as function parameters), then it will hide them all. That is obviously not ideal, but the simplicity of this approach probably outweighs its shortcomings.

You can, of course, add the //MLHIDE comment yourself in the editor.

You can also mark a block of code as not requiring translation, using the comments //MLHIDEON and //MLHIDEOFF:

  //MLHIDEON
  ... code which does not require localization
  //MLHIDEOFF

and you can effectively disable localization of a complete file using the comment //MLHIDEFILE.

You can also mark texts in XAML as not requiring translation.

Here is a good example, where the name of an Event has been detected as a string. This should fairly obviously not be translated.

Just as in the source code, you can hide the text by clicking on the sun symbol .

Screenshot of the sun symbol in the controls tab

This makes use of a second class in the file MLExtension.cs, the static class hide. This class only serves to mark XAML elements as hidden. It has no other functionality.

After clicking on the sun symbol , the XAML attribute m:hide.option="node" is insereted:

Screenshot of the sun symbol in the controls tab

This indicates to Multi-Language, that the strings in this node will be hidden.

There are in fact three options:

The attribute m:hide.option="node"applies to all string attributes in the node. Here is an example where this might be a problem.

In the SortableGridViewColumn elements, we would want to want to translate the Header attribute, but hide the SortPropertyName attribute.

  <GridView ColumnHeaderContainerStyle="{StaticResource HeaderStyleWOFilter}">
      <views:SortableGridViewColumn Header="Date"
                                    CellTemplate="{StaticResource appTemplate}"
                                    SortPropertyName="DateTime" />
      <views:SortableGridViewColumn Header="Percentage"
                                    CellTemplate="{StaticResource usageTemplate}"
                                    SortPropertyName="Usage" />
      <views:SortableGridViewColumn Header="Duration"
                                    CellTemplate="{StaticResource durationTemplate}"
                                    SortPropertyName="Duration" />
  </GridView>

It is logically better, to select the Header attribute for translation first, before hiding the nodes. The result would look like this:

  <GridView ColumnHeaderContainerStyle="{StaticResource HeaderStyleWOFilter}">
      <views:SortableGridViewColumn Header="{m:Lang _Date}"
                                    CellTemplate="{StaticResource appTemplate}"
                                    SortPropertyName="DateTime" m:Hide.Option="node" />
      <views:SortableGridViewColumn Header="{m:Lang Percentage}"
                                    CellTemplate="{StaticResource usageTemplate}"
                                    SortPropertyName="Usage" m:Hide.Option="node" />
      <views:SortableGridViewColumn Header="{m:Lang Duration}"
                                    CellTemplate="{StaticResource durationTemplate}"
                                    SortPropertyName="Duration" m:Hide.Option="node" />
  </GridView>

Selecting texts by clicking on the checkbox can seem a bit slow. There are a couple of ways to speed it up.

Keyboard shortcuts

Shortcut Function
Space Select or unselect for translation
Shift + Space Hide or unhide the text

Multiple Selection

If you select multiple lines in the Multi-Language window, the keyboard shortcuts will apply to all of the selected lines.

In the following screenshot, you can see that I have selected multiple texts in a XAML file. Using the space bar, I can select all of them for translation.

Screenshot of multiple selection in the controls tab

The same applies to hiding texts. In the following screenshot, you can see that I have selected a set of properties which should not be translated. Using Shift + Space, I can mark them all as not requiring translation.

Screenshot of multiple selection in the controls tab

In this example, the texts are related to the columns in a table. The texts for are the column headers, wheras the texts which other texts are property names.
The decision about which texts require translation must be made by the programmer. A translator will not understand this difference.

Other shortcuts

The following shortcuts apply in the grids, but not in edit mode (see below).

Shortcut Function
Up Arrow Move up in the grid
Shift + Up Arrow Move up in the grid, extending the current selection
Down Arrow Move down in the grid
Shift + Down Arrow Move down in the grid, extending the current selection
Left Arrow Collapse node in the grid
Right Arrow Expand node in the grid
Control + Shift + Space Ignore the complete file

The following shortcuts apply if you are editing a text in a cell in the grid.

Shortcut Function
F3 or F12 Open the translation memory dialog
Control + Up Move up in the grid (not in the cell text)
Control + Down Move down in the grid (not in the cell text)
Alt + Up Move up in the grid, setting the status to edited.
Alt + Down Move down in the grid, setting the status to edited.
Alt + Left Move left in the grid, setting the status to edited.
Alt + Right Move right in the grid, setting the status to edited.
Enter Save the current text and move down in the grid.
Alt + Enter Save the current text, and move down in the grid, setting the status to edited.

Setting the status of a text to edited is - in particular - intended for use with translations which were generated using online translation.
Texts from online translation have a red background. Translations with the status edited have a white background.
The idea behind setting the status to edited, you can indicate that you have looked at the translation decided that it is correct.
Obviously, you can only do this if you understand the language of the translatins.

In many cases, you will be able to select, or alternativly hide, texts in the source code based on simple rules. For example you will probably not want to translate SQL strings, or the parameters to a logging function. On the other hand, you will probably always want to translate the parameters to the MessageBox function.

You can handle operations like this easily with the feature "Filter with regular expressions". You can select this option from the tools menu as shown below, or from the context menu in the source code grid.

Screenshot of filter using regex command in the tools menu

This command shows the filter dialog, in which you can enter a search string as a regular expression. For example in the screen shot shown below, the search string "^select " will search for any string starting with the word select, with the option to hide the string.

Screenshot of filter using regex command dialog with the option

In this case, the option String only is selected. This means that the search stirng will only be matched in the string literal.

If, instead, you select the option Source code line, then the search string will match anything on the complete line in the source code. For example, this project uses the function PropertyChanging in its implementation of the INotifyPropertyChanged interface. For example:

    PropertyChanging("Settings");

Calls to this function should never be translated.

Note:
Of course it would be better practice to use PropertyChanging(nameof(Settings)) 😃

So by searching for "PropertyChanging" with the option Source code line, we can locate calls to this function and then exclude them from localization.

Screenshot of filter using regex command dialog with the option

.NET applications use a system of satellite DLLs to support localization.

In the output directory of your project, MSBuild will create a subdirectory for each supported language, using the IETF tag of the language, and containing a DLL with the resources for that language.

These so called satellite DLLs must be installed with your application, keeping the same directory structure.

Windows will automatically use the satellite DLLs for the default language, if they are present in the correct directory. For example, if the default language of Windows is German, the Windows will automatically look for resources in the [de] subdirectory.

Note:
There may well be subdirectories for other languages, which are provided by NuGet packages used in your project.

First of all, this step is not necessary. As mentioned above, Windows will automatically use the appropriate resource DLLs according to the current default language.

Using Multi-Language, you can add a dialog to your application, to select the language. This dialog will be shown when the application starts.

To add this dialog, select the menu command "Add language selection form to your project", as shown in the screenshot below.

This command only works on a project which creates an application. It makes no sense on class library.

Screenshot of menu command to add a language selection dialog to your project

This command brings up a dialog which shows the actions it will perform.

Screenshot of the dialog to add the language selection dialog to your project

After you click on the button Add Now it will show that the required actions have been completed.

Screenshot of the dialog to add the language selection dialog to your project

Now when you start the project, the first thing that appears should be a form to select the language of the application:

Screenshot of the language selection dialog on starting the application

After selecting the language, the application will start in the selected language.

For now, it's best to keep the option Next time: Show this form again. I'll come back to this option later.

A specific problem in this application

The idea is, that the language selection dialog appears before the main window of the application opens. In this specific application, that doesn't actually work. Here's what the problem is.

Multi-Language specifies a handler for the Startup event in app.xaml:

<Application ...
             Startup="Application_Startup">

and defines the handler in the file app.xaml.cs (or app.xaml.vb in a VB.NET project):

private void Application_Startup ( object sender, StartupEventArgs e )
{
    //Show the language select dialog
    System.Windows.ShutdownMode sm = this.ShutdownMode ;
    this.ShutdownMode = System.Windows.ShutdownMode.OnExplicitShutdown ;
    MultiLang.SelectLanguage sl = new MultiLang.SelectLanguage() ;
    sl.LoadSettingsAndShow() ;
    this.ShutdownMode = sm ;
}

However, this project defines the Startup Object as AppsTracker.EntryPoint, which shows a splash screen and then effectively starts the application in a constructor of the App class.

public App(ReadOnlyCollection<string> args)
{
    ...
}

I have therefore moved the Multi-Language startup code into that constructor.

public App(ReadOnlyCollection<string> args)
{
    //Show the language select dialog
    System.Windows.ShutdownMode sm = this.ShutdownMode ;
    this.ShutdownMode = System.Windows.ShutdownMode.OnExplicitShutdown ;
    MultiLang.SelectLanguage sl = new MultiLang.SelectLanguage() ;
    sl.LoadSettingsAndShow() ;
    this.ShutdownMode = sm ;
    ...
}

This now runs reliably before the main window is opened.

The language selection dialog (SelectLanguage.xaml) is added to your project, and you can modify it in any way that you want.

The default implemenation provides three options for what to do the next time that the application is started:

  1. Show the form again
  2. Use the selected language again, without showing the form
  3. Use the Windows default language, without showing the form

As things stand, if you select the second or third option, you will never see the dialog again and you cannot change the selection.

Obviously, that is useless, unless you add a button or a menu item to your application, to show the language selection again. In this case, you can show it with the code:

this.ShutdownMode = System.Windows.ShutdownMode.OnExplicitShutdown ;
MultiLang.SelectLanguage sl = new MultiLang.SelectLanguage(true) ;

The parameter true ensures that the dialog is actually opened (overriding the stored option).

I have added a speech bubble icon in the title bar, to show the language selection form again.

Screenshot of the AppsTracker application in German

You may have noticed that texts in the project are underlined, indicating some kind of warning.

Screenshot of the a text underlined by the code analyzer

This warning is generated by a Code Analyzer which is part of Multi-Language.

The idea is, that every text in the project should be either:

Several code fixes are associated with this warning, in particular to:

This screenshot shows the code fix to localize the text:

Screenshot of the code fix to localize a text with a named resource

If you select this code fix then:

This screenshot shows the code fix to disable localization using the MLHIDE comment.

Screenshot of the code fix to disable localization with the MLHIDE comment

If you select this code fix then the comment //MLHIDE will be inserted, exactly as shown.

The code analysis and code fixes are only available after a project has been initialized for localization using Multi-Language.

After that, the project does not have to be selected in the Multi-Language window.

If the project is open in the Multi-Language window, then any changes should automatiaclly appear in the source code grid.

Although this seems like a great way to replace string literals with resource strings, it is not really how code analyzers and code fixes are expected to work.