blog post image
Andrew Lock avatar

Andrew Lock

~10 min read

Localising the DisplayAttribute and avoiding magic strings in ASP.NET Core

This post follows on from my previous post about localising an ASP.NET Core application. At the end of that article, we had localised our application so that the user could choose their culture, which would update the page title and the validation attributes with the appropriate translation, but not the form labels. In this post, we cover some of the problems you may run into when localising your application and approaches to deal with them.

Previous localised image

Brief Recap

Just so we're all on the same page, I'll briefly recap how localisation works in ASP.NET Core. If you would like a more detailed description, check out my previous post or the documentation.

Localisation is handled in ASP.NET Core through two main abstractions IStringLocalizer and IStringLocalizer<T>. These allow you to retrieve the localised version of a key by passing in a string; if the key does not exist for that resource, or you are using the default culture, the key itself is returned as the resource:

public class ExampleClass  
{
    public ExampleClass(IStringLocalizer<ExampleClass> localizer)
    {
        // If the resource exists, this returns the localised string
        var localisedString1 = _localizer["I exist"]; // "J'existe"

        // If the resource does not exist, the key itself  is returned
        var localisedString2 = _localizer["I don't exist"]; // "I don't exist"
    }
}

Resources are stored in .resx files that are named according to the class they are localising. So for example, the IStringLocalizer<ExampleClass> localiser would look for a file named (something similar to) ExampleClass.fr-FR.resx. Microsoft recommends that the resource keys/names in the .resx files are the localised values in the default culture. That way you can write your application without having to create any resource files - the supplied string will be used as the resource.

As well as arbitrary strings like this, DataAnnotations which derive from ValidationAttribute also have their ErrorMessage property localised automatically. However the DisplayAttribute and other non-ValidationAttributes are not localised.

Finally, you can localise your Views, either providing whole replacements for your View by using filenames of the form Index.fr-FR.cshtml, or by localising specific strings in your view with another abstraction, the IViewLocalizer, which acts as a view-specific wrapper around IStringLocalizer.

Some of the pitfalls

There are two significant issues I personally find with the current state of localisation;

  1. Magic strings everywhere
  2. Can't localise the DisplayAttribute

The first of these is a design decision by Microsoft, to reduce the ceremony of localising an application. Instead of having to worry about extracting all your hard coded strings out of the code and into .resx files, you can just wrap it in a call to the IStringLocalizer and worry about localising other languages down the line.

While the attempt to improve productivity is a noble goal, it comes with a risk. The problem is that the string values embedded in your code ("I exist" and "I don't exist" in the code above) are serving a dual purpose, both as a string resource for the default culture, and as a key into a resource dictionary.

Inevitably, at some point you will introduce a typo into one of your string resources, it's just a matter of time. You better be sure whoever spots it understands the implications of changing it however, as fixing your typo will cause every other localised language to break. The default resource which is embedded in your code can only be changed if you ensure that every other resource file changes at the same time. That coupling is incredibly fragile, and it will not necessarily be obvious to the person correcting the typo that anything has broken. It is only obvious if they explicitly change culture and notice that the string is no longer localised.

The second issue related to the DisplayAttribute seems like a fairly obvious omission - by it's nature it contains values which are normally highly visible (used as labels for a form) and will pretty much always need to be localised. As I'll show shortly there are workarounds for this, but currently they are rather clumsy.

It may be that these issues either don't bother you or are not a big deal, but I wanted to work out how to deal with them in a way that made me more comfortable. In the next sections I show how I did that.

Removing the magic strings

Removing the magic strings is something that I tend to do in any new project. MVC typically uses strings for any sort of dictionary storage, for example Session storage, ViewData, AuthorizationPolicy names, the list goes on. I've been bitten too many times by subtle typos causing unexpected behaviour that I like to pull these strings into utility classes with names like ViewDataKeys and PolicyNames:

public static class ViewDataKeys
{
    public const string Title = "Title";
}

That way, I can use the strongly typed Title property whenever I'm accessing ViewData - I get intellisense, avoid typos and get renaming safely. This is a pretty common approach, and it can be applied just as easily with our localisation problem.

public static class ResourceKeys
{
    public const string HomePage = "HomePage";
    public const string Required = "Required";
    public const string NotAValidEmail = "NotAValidEmail";
    public const string YourEmail = "YourEmail";
}

Simply create a static class to hold your string key names, and instead of using the resource in the default culture as the key, use the appropriate strongly typed member:

public class HomeViewModel
{
    [Required(ErrorMessage = ResourceKeys.Required)]
    [EmailAddress(ErrorMessage = ResourceKeys.NotAValidEmail)]
    [Display(Name = "Your Email")]
    public string Email { get; set; }
}

Here you can see the ErrorMessage properties of our ValidationAttributes reference the static properties instead of the resource in the default culture.

The final step is to add a .resx file for each localised class for the default language (without a culture suffix on the file name). This is the downside to this approach that Microsoft were trying to avoid with their design, and I admit, it is a bit of a drag. But at least you can fix typos in your strings without breaking all your other languages!

How to Localise DisplayAttribute

Now we have the magic strings fixed, we just need to try and localise the DisplayAttribute. As of right now, the only way I have found to localise the display attribute is to use the legacy localisation capabilities which still reside in the DataAnnotation attributes, namely the ResourceType property.

This property is a Type, and allows you to specify a class in your solution that contains a static property corresponding to the value provided in the Name of the DisplayAttribute. This allows us to use the Visual Studio resource file designer to auto-generate a backing class with the required properties to act as hooks for the localisation.

Creating a resource file in visual studio

If you create a .resx file in Visual Studio without a culture suffix, it will automatically create a .designer.cs file for you. With the new localisation features of ASP.NET Core, this can typically be deleted, but in this case we need it. Generating the above resource file in Visual Studio will generate a backing class similar to the following:

public class ViewModels_HomeViewModel {
        
    private static global::System.Resources.ResourceManager resourceMan;
    private static global::System.Globalization.CultureInfo resourceCulture;
        
    // details hidden for brevity
        
    public static string NotAValidEmail {
        get {
            return ResourceManager.GetString("NotAValidEmail", resourceCulture);
        }
    }
        
    public static string Required {
        get {
            return ResourceManager.GetString("Required", resourceCulture);
        }
    }
        
    public static string YourEmail {
        get {
            return ResourceManager.GetString("YourEmail", resourceCulture);
        }
    }

We can now update our display attribute to use the generated resource, and everything will work as expected. We'll also remove the magic string from the Name attribute at this point and move the resource into our .resx file:

public class HomeViewModel
{
    [Required(ErrorMessage = ResourceKeys.Required)]
    [EmailAddress(ErrorMessage = ResourceKeys.NotAValidEmail)]
    [Display(Name = ResourceKeys.YourEmail, ResourceType = typeof(Resources.ViewModels_HomeViewModel))]
    public string Email { get; set; }
}

If we run our application again, you can see that the display attribute is now localised to say 'Votre Email' - lovely!

Localised display name attribute

How to localise DisplayAttribute in the future

If that seems like a lot of work to get a localised DisplayAttribute then you're not wrong. That's especially true if you're not using Visual Studio, and so don't have the resx-auto-generation process.

Unfortunately it's a tricky problem to work around currently, in that it's just fundamentally not supported in the current version of MVC. The localisation of the ValidationAttribute.ErrorMessage happens deep in the inner workings of the MVC pipeline (in the DataAnnotationsMetadataProvider) and this is ideally where the localisation of the DisplayAttribute should be happening.

Luckily, this has already been fixed and is currently on the development branch of the ASP.NET Core repo. Theoretically that means it should appear in the 1.1.0 release when that happens, but we are at very early days at the moment!

Still, I wanted to give the current implementation a test, and luckily this is pretty simple to setup, as all the ASP.NET Core packages produced as part of the normal development workflow are pushed to various public MyGet feeds. I decided to use the 'aspnetcore-dev' feed, and updated my application to pull NuGet packages from it.

Be aware that pulling packages from this feed should not be something you do in a production app. Things are likely to change and break, so stick to the release NuGet feed unless you are experimenting or you know what you're doing!

Adding a pre-release MVC package

First, add a nuget.config file to your project and configure it to point to the aspnetcore-dev feed:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <add key="AspNetCore" value="https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json" />
    <add key="NuGet" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
</configuration>

Next, update the MVC package in your project.json to pull down the latest package, as of writing this was version 1.1.0-alpha1-22152, and run a dotnet restore.

{
  "dependencies": {
    ...
    "Microsoft.AspNetCore.Mvc": "1.1.0-alpha1-22152",
    ...
  }
}

And that's it! We can remove the ugly ResourceType property from a DisplayAttribute, delete our resource .designer.cs file and everything just works as you would expect. If you are using the magic string approach, that just works, or you can use the approach I described above with ResourceKeys.

public class HomeViewModel
{
    [Required(ErrorMessage = ResourceKeys.Required)]
    [EmailAddress(ErrorMessage = ResourceKeys.NotAValidEmail)]
    [Display(Name = ResourceKeys.YourEmail)]
    public string Email { get; set; }
}

As already mentioned, this is early pre-release days, so it will be a while until this capability is generally available, but it's heartening to see it ready and waiting!

Loading all resources from a single file

The final slight bugbear I have with the current localisation implementation is the resource file naming. As described in the previous post, each localised class or view gets its own embedded resource file that has to match the file name. I was toying with the idea of having a a single .resx file for each culture which contains all the required strings instead, with the resource key prefixed by the type name, but I couldn't see any way of doing this out of the box.

You can get close to this out of the box, by using a 'Shared resource' as the type parameter in injected IStringLocalizer<T>, so that all the resources using it will, by default, be found in a single .resx file. Unfortunately that only goes part of the way, as you are still left with the DataAnnotations and IViewLocalizer which will use the default implementations, and expect different files per class.

As far as I can see, in order to achieve this, we need to replace the IStringLocalizer and IStringLocalizerFactory services with our own implementations that will load the strings from a single file. Given this small change, I looked at just overriding the default ResourceManagerStringLocalizerFactory implementation, however the methods that would need changing are not virtual, which leaves us re-implementing the whole class again.

The code is a little long and tortuous, and this post is already long enough, so I won't post it here, but you can find the approach I took on GitHub. It is in a somewhat incomplete but working state, so if anyone is interested in using it then it should provide a good starting point for a proper implementation.

For my part, and given the difficulty of working with .resx files outside of Visual Studio, I have started to look at alternative storage formats. Thanks to the use of abstractions like IStringLocalizerFactory in ASP.NET Core, it is perfectly possible to load resources from other sources.

In particular, Damien has a great post with source code on GitHub on loading resources from the database using Entity Framework Core. Alternatively, Ronald Wildenberg has built a JsonLocalizer which is available on GitHub.

Summary

In this post I described a couple of the pitfalls of the current localisation framework in ASP.NET Core. I showed how magic strings could be the source of bugs and how to replace them with a static helper class.

I also showed how to localise the DisplayAttribute using the ResourceType property as required in the current 1.0.0 release of ASP.NET Core, and showed how it will work in the (hopefully near) future.

Finally I linked to an example project that stores all resources in a single file per culture, instead of a file per resource type.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?