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
Activating Multi-Language
After you have installed Multi-Language for Visual Studio, there will be a new command in the Tools menu in Visual Studio.

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.

Selecting a project
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.

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.

You can ignore the buttons in the items for now.
Initialising the project for localization
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.

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.

Multi-Language will add some new files to the project:
resources.resx
This is a resource file for storing strings.MLExtension.cs
This file contains a markup extension which is used to localize strings in XAML files.<project name>_ml.xml
This file is used by Multi-Language to store information about the project and translations.
Scanning the project
Multi-Language will now scan the project. This is performed in two phases:
- scanning controls
- scanning source code
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.

Selecting texts for translation
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.

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

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.

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. |
Flags column
For two of the texts, the flags column contains the icon
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.
Adding a second language
To add a new language to the project, click on the
symbol on the Multi-Language toolbar.

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

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.

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).
Automatic translations
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:
- Microsoft
- DeepL
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.
Entering translations
There are multiple ways to enter translations:
- type the translation into the grid
- use online tranlation
- for the specific text
- for all texts
- export the texts to Excel, get them translated and then import the translated texts
Typing into the grid
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.
Online translation for a single text
A second method, is to click in cell for the translation, and then hit F3 to bring up 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.

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.
Online translation for multiple texts
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.

Selecting texts in the source code
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.

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.

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.

How does localization in XAML work?
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.
Hiding texts which do not require translation
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.
Hiding texts in 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.

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.

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:
- selected for translation
OR - marked as not requiring translation (hidden)
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.
Hiding texts in XAML
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
.

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:

This indicates to Multi-Language, that the strings in this node will be hidden.
There are in fact three options:
m:hide.option="node"to hide strings in the specific XAML nodem:hide.option="tree"to hide strings in the XAML node and all its child nodesm:hide.option="none"to disapply the "tree" option to child node
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>
Keyboard shortcuts and multiple selection
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.

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.

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.
Filtering texts with regular expressions
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.

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.

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 usePropertyChanging(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.

Distributing the localized resource files
.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.
Selecting the language at runtime
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.

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

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

Now when you start the project, the first thing that appears should be a form to select the language of 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.
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:
- Show the form again
- Use the selected language again, without showing the form
- 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.
![]()
Localizing with the Code Analyzer
You may have noticed that texts in the project are underlined, indicating some kind of warning.

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:
- localized or
- marked as not requiring localization (hidden)
Several code fixes are associated with this warning, in particular to:
- localize the string using a named resource
- add the MLHIDE comment to disable localization
Adding a resource using the code fix
This screenshot shows the code fix to localize the text:

If you select this code fix then:
- the code change will be made - as displayed
- the string resource will be added to the resource file (resources.resx)
Hide text using the code fix
This screenshot shows the code fix to disable localization using the MLHIDE comment.

If you select this code fix then the comment //MLHIDE will be inserted, exactly as shown.
Background notes
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.
- Code fixes are only expected to make updates to the current code file. Adding a resource string goes way beyond the normal scope of a code fix.
- Interaction between a code fix and a Visual Studio extension is very very limited.