Dynamically Localizing a WPF Application at Runtime

Tim Williams Programming, Tutorial Leave a Comment

Today, almost every facet of life deals with technology. Because of this, there is a necessity for applications to be accessible and usable by all people around the world. This means that software developers write need to provide a tailored experience for the end-users that is easy to use, provides output, and effectively captures user input. One important part of that process is localization, which will be explained later on.

The main focus of this article is to provide a solution for localizing a WPF application at runtime. This allows users of the application to change the culture/language of the UI elements through interactions, such as button clicks or menu items.

This blog is intended to provide an in-depth explanation of the coding aspects of localization. However, it is expected that you understand WPF and .NET and possess a moderate level of software engineering knowledge, such as software architecture models and windows development. I will assume that you have this understanding as we go forward.

What is Localization, Globalization, and Internationalization?

Localization, Globalization, and Internationalization have subtle differences, but are all used to achieve a common goal, to ensure products can reach users of different backgrounds and cultures. According to w3.org,

“Localization refers to the adaptation of a product, application or document content to meet the language, cultural and other requirements of a specific target market (a locale).”

There are several considerations that must be made in order to adapt software that meets the requirements of a target locale. Things such as currency, date and time formats, as well as choosing proper text and symbols that could convey different meanings to different cultures. While localization is a complex process, the focus of this article will be on translating UI elements in an application.

Project Overview

The sample project used for this proof of concept is a simple, WPF application with a few, common controls. The only control that contains usable function is the dropdown, language menu items, which allow the user to switch the application’s language.

The source can be found on Github, here. The project can be downloaded and built with VS 2019 or later.

Resource Files

Executables, such as DLLs and applications, have embedded files that allow for different objects, such as strings and images to be deployed with the executable. These resource files are the primary vessel for providing translations. In .NET, resources are packaged in .resx files, which store strings as well as binary data.

In order to be used for localization, each .resx file will have an associated culture1, which can be thought of as information for a specific locale or language. Resource strings are stored as key-value pairs in these files. Each resource file is a composite of two separate files, the .resx file, and a designer file. This allows the resources to be easily viewed and modified in the Visual Studio Designer. While you can manually modify each file in a text editor, it is recommended that it be modified using Visual Studio.

For each language your application will be translated, it will require a corresponding .resx file. This project is localized for two locales, English – American (en-US) and Spanish (es).Localizing a WPF app

Resx files shown in Solution Explorer inside Visual Studio 2019

In order to properly localize the application, every string that should be translated needs to be in the resource file. Typically, this will be all UI and display strings, but it could contain others such as log messages if it is intended to allow the end-user to view those strings in their respective language.

en-US.resx file

Dependency Properties, Objects, and the LocalizationProvider Class

All the “magic” for localizing the different UI elements at runtime happens with the help of the LocalizationProvider class. This is a static class that retrieves and applies the resources to specific elements whenever an update is needed, i.e. a language change is requested. It also keeps track of all controls that need to be updated.

Each element gets translated by updating a DependencyProperty2 on the element. This property is used via data binding, which allows the UI to be updated in response to an event in the associated view model. An example of data binding is when a user interacts with a text box. When the user enters text inside of the textbox element, the data that is bound to the text box is updated in the view model.

In order to properly update the UI elements in the application, custom dependency properties must be created and registered with the LocalizationProvider class. This registration process involves giving the custom property a name, specifying the property type, specifying property owner (the LocalizationProvider) and setting metadata, which contains a callback.

public static DependencyProperty ContentIDProperty = DependencyProperty.RegisterAttached("ContentID", typeof(string), typeof(LocalizationProvider), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsArrange,  new PropertyChangedCallback(OnContentIDChanged)));
public static DependencyProperty TitleIDProperty = DependencyProperty.RegisterAttached("TitleID", typeof(string), typeof(LocalizationProvider), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsArrange, new PropertyChangedCallback(OnTitleIDChanged)));

When the application is first loaded, the associated callbacks of the UI elements with registered dependency properties are invoked. This callback retrieves the associated resource string from the .resx file based on the appropriate culture. The culture is determined by a ResourceAndCulture class (explained later) that is instantiated each time a resource lookup is needed.

The localized string is then applied to the UI element depending on type of control (ie text block, button, group header). That control is then added to a collection that will keep track of DependencyObjects based on the type of the DependencyProperty. DepedencyObjects are just objects that have a registered DependencyProperty.

This collection is maintained by the LocalizationProvider for the lifetime of the application. (This is why the LocalizationProvider is static, so only one instance is created and utilized throughout the entire app lifecycle.)

Once all the controls have been added to their respective collections in the LocalizationProvider, the LocalizationProvider will use the cached instance of the control to update the resource string whenever a request to change the culture of the application is made. The steps for updating the UI elements with the LocalizationProvider are as follows:

  1. A change to the culture of the application is requested, typically through user interaction with a button click or keyboard press.
  2. The LanguageChanged event is raised, informing all subscribers that a language change is requested.
  3. Each event subscriber makes a request for the LocalizationProvider to the UpdateAllObjects method. This method iterates over all the collections of DependencyObjects, applying the localized string retrieved via the ResourceAndCulture class.

Outside of applying resource strings to DependencyObjects, the LocalizationProvider also exposes methods that allow for easy retrieval of resource strings directly. This is useful for items that may not have associated UI elements or DependencyProperties, such as messages for logging.

These methods are named GetLocalizedString, which takes a string key as an argument, and GetLocalizedStrings, which takes a list of string keys as an argument. They return a single, localized string and list of localized strings, respectively. Under the hood, they use the ResourceAndCulture class to determine which culture to use when retrieving the string.

ResourceAndCulture Class

The publicly exposed members of the LocalizationProvider class utilize the ResourceAndCulture class to properly retrieve and apply localized strings to UI elements. It’s a relatively simple class that takes two string arguments, a basename and a culture name. The basename is the relative path of the desired .resx file. In this project, the resources are stored in a Resources directory, so the basename for the en-US .resx file would be DynamicLocalizationSample.Resources.en-US.

The culture name3 is simply the name of the culture. The class exposes two, read-only properties, Res and Cul, which are short for resource and culture respectively. The Res property is a ResourceManager4, which provides convenient access to culture-specific resources at runtime. The Cul property is a CultureInfo object that stores just that, information about a specific culture. These properties are set in the ResourceAndCulture’s constructor.

When a localized string is requested, the requesting code creates an instance of the ResourceAndCulture class. It then invokes the GetString method of the Res property, passing a string key and the ResourceAndCulture object’s Cul property. If a string can’t be found in a specified .resx file, then null is returned.

try
{
	rc = new ResourceAndCulture();
	resourceValue = rc.Res.GetString(key, rc.Cul);
}
catch (Exception) { }

Integrating with the View

While the LocalizationProvider does the heavy lifting behind the scenes, it’s the view that the end-user interacts with. Incorporating the dependency properties with UI elements is simple, and it follows the same patterns that come with standard WPF development.

In WPF applications, most UI elements have a Content property, which is what is displayed on the screen. In order to have content that responds to updated events, a custom dependency property is used instead of the Content property in the element’s XAML. This project has two dependency objects, content and title, so two custom dependency properties were created, ContentID and TitleID.

<TextBlock x:Name ="textBlock" HorizontalAlignment = "Left"
 TextWrapping = "Wrap" local:LocalizationProvider.ContentID="This_Is_A_Text_Block"
 VerticalAlignment = "Top" Height = "32" Width = "149" Margin="21,56,0,0" />
<TextBlock x:Name = "textBlock_Copy" HorizontalAlignment = "Left" TextWrapping = "Wrap" 
 local:LocalizationProvider.ContentID = "This_Is_A_Another_Text_Block"
 VerticalAlignment = "Top" Height = "32" Width = "201" Margin="21,88,0,0" />
<TextBlock x:Name = "textBlock_Copy1" HorizontalAlignment = "Left"
 TextWrapping = "Wrap" local:LocalizationProvider.ContentID = "Yet_A_Another_Text_Block"
 VerticalAlignment = "Top" Height = "32" Width = "149" Margin="21,120,0,0" />
<Button local:LocalizationProvider.ContentID="Click_Me" Margin="10,214,240,24"/>
<GroupBox local:LocalizationProvider.ContentID="Grouped_Controls" Width="160" Height="160" Margin="200,46,10,50">
 <StackPanel>
  <RadioButton local:LocalizationProvider.ContentID="First_Option" Margin="5,10,20,5"/>
  <RadioButton local:LocalizationProvider.ContentID="Second_Option" Margin="5,5,20,62"/>
 </StackPanel>
</GroupBox>

The value of the ContentID property is the name of the targeted resource. In the above screenshot, the value of the ContentID for the first text block is This_Is_A_Text_Block. When the dependency property is registered, the string This_Is_A_Text_Block will be used as the key to retrieve the localized string value based on the current culture. This simple pattern is applied to UI elements that should be localized.

Changing the Language

We’ve finally reached the end, and the final piece is the main purpose of this article: changing the language on the fly. As previously mentioned, this is primarily done via user interaction, such as a button click or selecting a menu option. This is the most trivial and configurable part of the project and should be tailored to satisfy the application’s requirements, such as accessibility. This project uses a dropdown menu to select a language and only targets two languages, so the menu design is small and simple.

Main application window showing the language menu options

In order to trigger logic in response to a UI interaction, such as menu item press, a command must be bound to that element. In WPF, that command must implement the ICommand interface. The RelayCommand5 class is a utility class that implements the required methods from the ICommand interface in an easy-to-use form, taking actions (think of delegates) as arguments and applying them to the proper members. Each command is implemented in the ViewModel to which the targeted element belongs and is of type ICommand.

public ICommand EnglishLanguageCmd { get { return new RelayCommand(p => SetLanguageToEnglish()); } }
public ICommand SpanishLanguageCmd { get { return new RelayCommand(p => SetLanguageToSpanish()); } }
private void SetLanguageToEnglish()
{
    AppSettings.AppLanguage = "English";
    OnLanguageChanged(EventArgs.Empty);
}

private void SetLanguageToSpanish()
{
    AppSettings.AppLanguage = "Spanish";
    OnLanguageChanged(EventArgs.Empty);
}

Each menu item has a command that calls the same steps to apply the language settings. Technically, these steps could be broken out into one method, and the different logic could be applied with a switch case, but this could lead to many cases if you choose to expand your application to more languages.

The AppSettings class is self-explanatory; it’s used as a state manager for the application’s different settings. The only property currently exposed is the AppLanguage setting, which is used by the ResourceAndCulture class to determine the culture for resource retrieval.

The OnLanguageChanged method invokes the LanguageChanged event for subscribers. Raising an event allows subscribers to conditionally respond to the event, compared to triggering a method call chain. An example of conditionally responding (or not responding) would be if a control is loaded but not displayed, i.e. hidden from view. The UI elements might not need to be updated if they are currently displayed to the user.

public static event EventHandler LanguageChanged;
protected virtual void OnLanguageChanged(EventArgs e)
{
    LanguageChanged?.Invoke(this, e);
   LocalizationProvider.UpdateAllObjects();
}

Once the event is invoked, subscribers would then call their UpdateUIElements methods, which in turn would call LocalizationProvider.UpdateAllObjects. This sample application only has one view, so the UpdateAllObjects method is called in the same method, resulting in a strange caller is also the callee situation.

The effectiveness of this pattern is observed when your application contains many views. And that’s it. We now have an application that can have all its UI elements updated to another culture/language view a dropdown menu.

Conclusion

Dynamically localizing strings and UI at runtime allows for better accessibility and convenient context switching for end-users of different backgrounds when running an application. The tools and practices mentioned here can be applied to contexts outside of culture translation. For example, having multiple resources for version managing of text if the application’s language requires a change in response to regulatory changes. The strategy outlined here is only one part of a bigger picture to effectively provide usable software to people across the globe.


1 Culture – https://docs.microsoft.com/en-us/dotnet/api/system.globalization.cultureinfo?view=net-6.0#CultureNames

2Dependency Properties – https://docs.microsoft.com/en-us/dotnet/desktop/wpf/properties/dependency-properties-overview?view=netdesktop-6.0

3The culture name could be retrieved from the base name but as of this writing hasn’t been implemented in the project.

4ResourceManager – https://docs.microsoft.com/en-us/dotnet/api/system.resources.resourcemanager?view=net-6.0

5RelayCommand – https://www.c-sharpcorner.com/UploadFile/20c06b/icommand-and-relaycommand-in-wpf/

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments