Applying an improved multiple languages library to .NET applications

Last Updated: January 16, 2023 | Created: January 16, 2023

The last article covered why I added extra features to the .NET support of multiple languages  (known as localization in .NET) and via the Net.LocalizeMessagesAndErrors library (shortened to Localize-ME library). This article provides the details of how you would use the Localize-ME library in to add localization to your .NET application. The full list of the articles in this series are:

  1. Improving the support of multiple languages in .NET applications
  2. Applying the improved multiple languages support to .NET applications (this article)
  3. A library / pattern for methods that return a status, including localization (coming soon)

TL;DR; – Summary of this article

  • This article assumes you know the terms / concepts of localization. If you don’t, then go to the “A brief view of how .NET localization works” which introduces to the localization concepts.
  • The Localize-ME library adds extra localization features focused on a) making it easier to localize an existing application, and b) stop localization from making your code harder to understand.
  • The Localize-ME library provides two services
    • SimpleLocalizer service is good in small / simple applications and is simple to use.
    • DefaultLocalizer service is good from large applications with many localizations because it uses a more formal pattern that makes it easier to find / set up the localized messages.
  • This article provides eight steps to localize an ASP.NET Core application using either, or both Localize-ME services. Each step provides a summary of what you need to do with links to the Localize-ME documentation for the definitive details.

Super-quick explanation on the Localize-ME library provides

The Localize-ME library adds extra localization features to improve localizing your application. The big difference from the .NET localization is that you keep your existing messages, error strings, etc. in your code (known as the default messages) while .NET localization would replace your messages with a key (referred to as localize key).

Keeping your existing messages in your application has lots of benefits, but the biggest are a) its easier to add localization an existing application, and b) your code is easier to understand and test.

To make this work you provide the language (referred to as culture) that your default messages are written in when the Localize-ME library is registered. This allows the Localize-ME services to return the message in your code when the user’s / app’s culture matches the default messages culture. If the user’s / app’s culture isn’t the same as the default messages culture, then it uses the .NET localisation services to lookup the message in the correct resource file for the required culture.

The diagram below shows this for French (LHS), which isn’t the default messages culture, and English (RHS), which is default messages culture.  The blue italic words in the diagram explains the two different routes for the two cultures.

Setting the scene – the best places to use the Localize-ME library

The Localize-ME library adds two localize services on top of the .NET localization services which provide new (better) ways to localize your application. From my experience I created two services, SimpleLocalizer and DefaultLocalizer, that both localize a message, but works better in different cases. The list below provides my take on where are best used.

1. The SimpleLocalizer service is good for using in Razor pages etc.

The SimpleLocalizer service provides the simplest approach to obtaining a localized message. Its features that make it simpler are:

  • Auto localize key: the SimpleLocalizer service uses your message as the lookup key (which I call localise key) which is unique, while the IStringLocalizer needs you to add a string that must be unique to your message.
  • Simpler injection: the SimpleLocalizer’s TResource part (the later section 3 describes the TResource part) for what is set on startup so you only need ISimpleLocalizer to get an instance, while the IStringLocalizer needs IStringLocalizer<TResource>

2. The DefaultLocalizer service is better on large localizations with backend projects

When I started adding localization to a medium size application, I found string constants for the localize key were hard to remember and hence error prone. The DefaultLocalizer service uses extension methods to create the localize key, which has the following benefits:

  • Formal localize key design: The localize key has a {class}, {method} and {localKey}, which tells you what class and method the message came from in your code.
  • Auto fill in of {class} & {method}: the extension methods will fill in the {class} & {method} parts of the localize key for you.
  • Short, but unique localize key: The library has various ways to make short, but unique localize keys. See this section from the Localize-ME documentation on how that works.

The common features of both the SimpleLocalizer and DefaultLocalizer are:

  • Your code is easier to understand: the .NET localization services replace your messages with a localise key. The SimpleLocalizer / DefaultLocalizer services keep your message
  • Better missing entry handling: If the the .NET localization services can’t find the localized message, then it returns the localize key which isn’t that useful to the user. While SimpleLocalizer / DefaultLocalizer services returns the default message, which might be in the wrong language but can translated by the user (it also logs a warning with very detailed information what was missing – see this section in the Localize-ME documentation.

Things that the Localize-ME library doesn’t provide

The .NET localization services contain features that I don’t try to provide in the Localize-ME library. They are:

Using the Localize-ME library in an ASP.NET Core application

The list below contains the eight steps for adding localization to an .NET application, with examples from an ASP.NET Core application. Each step provides a summary and examples for its steps, with links to the Localize-ME documentation which contains the more detailed information

  1. Setting up the .NET localization
  2. Setting up the Localize-ME services
  3. Adding your resource files and TResource classes
  4. Getting an instance of the SimpleLocalizer
  5. Getting an instance of the DefaultLocalizer
  6. Using the SimpleLocalizer service
  7. Using the DefaultLocalizer service
  8. How to unit test your localized code

1. Setting up the .NET localization

The Localize-ME library relies on the .NET localization services so we start with this (the Localize-ME adds its extra features, which are described later).

On startup you need to register the .NET localization, its resource files (see section 3 later about resource files) and, set up how to obtain the user’s / app’s language (known as culture in .NET). The Localize-ME documentation contains information on how to set this within an ASP.NET Core application in some detail with lots of links to useful information and three example applications, so go to that document for the full information

I did want to point out how the .NET localization obtains user’s / app’s culture, which is needed to return the messages in the correct language. By default the parameter called RequestCultureProviders within the .NET localization options has three ways to try to obtain the culture, which are used in turn until it gets a culture  – see list below:

  1. QueryStringRequestCultureProvider – uses culture data in the query string, which allows you to create a URL that will set the culture.
  2. CookieRequestCultureProvider – looks for a culture cookie, which is useful if want users to select the language they want.
  3. AcceptLanguageHeaderRequestCultureProvider – this picks up data from the browser to set the culture.

These are the main approaches to get the user / app cultures, and they are described here.

2. Setting up the Localize-ME services

I have made the setting up of the Localize-ME library as simple as possible. Here are the two registrations you need to add to your setup code in the Program class.

The code below registers the DefaultLocalizer – click here for information on the two parameters.

builder.Services.RegisterDefaultLocalizer(
    "en", supportedCultures);

The code below registers the SimpleLocalizer – click here for information on the optional parameter

builder.Services.RegisterSimpleLocalizer
    <HomeController>();

NOTE: The DefaultLocalizer service must be registered for the SimpleLocalizer service to work.

3. Adding your resource files and TResource classes

Resource files hold the messages, in a specific language, in the Value column while the localise key is held in the Name column. The .NET localization services uses the culture to pick the correct resource file and then the localize key obtain the correct message to show to the user.

You link a resource file to your localization service via a class, known as a TResource class, in the ASP.NET Core project. The resource file uses part of the TResource class’s FullName to the start of its filename. For instance, if you used the HomeController class as a TResource class and the language is USA English then the resource file name would be (but see this link to see the other file formats)

Controllers.HomeController.en-US.resx

Resource files aren’t the easiest part of the localization service, so I suggest you read the Localize-ME All about resource files documentation. This explains how to register them, and the different ways you can organise the resource files.

The other problem is finding all the localise keys and the appropriate localized message and then adding into the resource files. I give two approaches that I created when I was applying this library in to my AuthP library. See:

4. Getting an instance of the SimpleLocalizer

Obtaining an instance of the SimpleLocalizer service is easy, as you have already defined the TResource class on startup.

  • If you are in C# code, then you use the interface ISimpleLocalizer via dependency injection to get SimpleLocalizer service.
  • If you are in a Razor page (cshtml), you use “@inject ISimpleLocalizer SimpleLocalizer”.

NOTE: See this Localize-ME document for example code and more on ISimpleLocalizerFactory service.

5. Getting an instance of the DefaultLocalizer

The way to get an instance of the DefaultLocalizer service is the same as getting the SimpleLocalizer service, but you need to the TResource class to define which resource file group this service should look up for localized message.

The simplest approach is to use a dependency injection with the IDefaultLocalizer<TResource> interface, e.g. IDefaultLocalizer<HomeController>.

But if you have backend code in other projects you can’t do that because the TResource class must be in the ASP.NET Core project and your backend projects can’t link to these TResource because that would create a circular reference. In this case you need the IDefaultLocalizerFactory service and some options.

To use the IDefaultLocalizerFactory service you need to register singleton class (shown as MyOptions in the code below) which contains the Type of TResource class(es) you need in your backend code, and then use the IDefaultLocalizerFactory service, as shown below within your backend code.

public class MyBackendCode 
{
    private readonly IDefaultLocalizer _defaultLocalizer;

    /// <summary>
    /// ctor
    /// </summary>
    public MyBackendCode(MyOptions options,
        IDefaultLocalizerFactory factory)
    {
        _localizeDefault = factory.Create(
            options.BackendResourceType) 
    }
    //… rest of class left out
}

NOTE: See the Getting an instance of the IDefaultLocalizer service documentation for more details.

6. Using the SimpleLocalizer service

The SimpleLocalizer service is simple to use! It only has only two methods and the localise key is derived from the message (see this section in the Using SimpleLocalizer documentation on how that works). The first method is shown below and handles a string message.

<label for="month">
    @(SimpleLocalizer.LocalizeString(
        "Provide a string (can be null)", this))
</label>

The other method shown below handles FormattableStrings, where you can provide dynamic data into the localized message.

<h1>
    @(SimpleLocalizer.LocalizeFormatted(
       $"List of {Model.NumBooks} books", this))
</h1>

NOTE: These two examples come from Razor pages, using the “@inject ISimpleLocalizer SimpleLocalizer” approach, but you can also use dependency injection within a ASP.NET Core Controller or Page.

7. Using the DefaultLocalizer service

The DefaultLocalizer service has only two methods:

Both methods take in two parts

  1. The localize key that uses a formal design that can contain the {callingClass}, {method} and {localKey}.
  2. The default message, either in a string or a FormattableString.

The localize key is created by one of the localize key extension methods which contain various combinations of the {callingClass}, {method} and {localKey} – this link shows you the various methods / combinations and what situation each one is used for.

This makes calling a DefaultLocalizer method is a bit more work than the SimpleLocalizer method calls, but the extra effort provides you with a localise key that is easy to understand and easily track back to where you made the localize call.

NOTE: There is an interesting section about DefaultLocalizer localize key creation called the balance between readable localize key and being unique, which provides two ways to create both short and unique localize keys.

8. How to unit test your localized code

Once you change your code to use the Localize-ME library, then you will need to provide a ISimpleLocalizer or IDefaultLocalizer service to runs your tests. I recommend using a stubbing out approach (see this Microsoft document about stubbing) in your unit tests as because the stub to return the default message in your code, which makes it easier to update your unit tests and the tests are easier to understand.

The Localize-ME library contains stubs for the ISimpleLocalizer or IDefaultLocalizer services, called StubSimpleLocalizer and StubDefaultLocalizer respectively. The code below shows the using StubDefaultLocalizer (the StubSimpleLocalizer works the same)

[Fact]
public void TestStubDefaultLocalizer()
{
    //SETUP
    var defaultLoc = new StubDefaultLocalizer();

    //ATTEMPT
    var message = defaultLoc.LocalizeStringMessage(
        "MyLocalizeKey".MethodLocalizeKey(this),
        "My message");

    //VERIFY
    message.ShouldEqual("My message");
    defaultLoc.LastKeyData.LocalizeKey.ShouldEqual(
        "TestStubDefaultLocalizer_MyLocalizeKey");
}

NOTE: The unit testing your localized code document gives more information on the StubSimpleLocalizer and StubDefaultLocalizer classes.

8a. Logging Localize-ME localization during unit testing

I also created a more powerful IDefaultLocalizer stub called StubDefaultLocalizerWithLogging, which returns the default message, which optionally logs the full information of the localization data to a database. This provides a quick way to look at the localized messages, and it can find certain problems.

This stub is much more complex to use, but it does provide a very useful list of the localised messages. This helps in checking the localize keys and also speeded up the process of building the resource files – see the this section from the first article where I explain why I found it so useful, but here is a screenshot of a section of the logged localization data. Note the PossibleErrors column which has found an existing entry in the database with the same localize key, but the message format is different. NOTE Click the screenshot to get a bigger version.

NOTE: There is detailed documentation about how to setup and use the StubDefaultLocalizerWithLogging class.

Conclusions

I had a requirement from a developer to add localization to my AuthP library so I started looking at .NET localization services. The .NET localization didn’t have the features to provide an optional localization feature (i.e. your code will still works without resource files) to my AuthP library as it was. So, I started to create the Localize-ME library that makes localization optional in my AuthP library.

Once I knew I had to create a new library, then I could reimagine how I would like to apply localization in a .NET application. For instance, I made it possible to keep your messages are in your non-localized application which means that your code is easier to update and understand. While the .NET localization approach, which would move the messages to the resource files and replace them with a localize key, makes the code (a bit) harder to understand.

Unfortunately, I didn’t come up a way to remove the manual / tedious job of building resource files, but you might like to look at section 8a about capturing the localize data while running your unit tests. Personally, I found this very useful in providing the data to help in building resource files.

Other improvements came as I started to use the Localize-ME library in different ways, from a test application. updating the AuthP library and creating another test example in the AuthP library. Each usage was different which highlight different issues, and each issue often provided new approaches or features. This means it took way longer that I thought it would create the library, but I’m pleased with the final result. I hope the Localize-ME library will help you too.

Extra note: I ran a twitter poll on whether users of AuthP library would use the new localization feature the votes were 10 to 1 in favour of needing the localization feature. This makes sense as the AuthP library’s features is helping developers to create multi-tenant applications, which could be used in any country or countries.

Happy coding.

5 1 vote
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments