65.9K
CodeProject is changing. Read more.
Home

A Simple and Effective Way to Localize ASP.NET MVC Data Annotation Texts and Error Messages

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2 votes)

Dec 23, 2012

CPOL

2 min read

viewsIcon

71341

By use of a cutomized metadata provider, MVC model data annotation validation messages can be localized in a simple and elegant way.

Introduction

Recently, I was working on an ASP.NET MVC 4 project which needs language localization. Generally speaking, it can be done in this way...

Design a class file which connects the database (my project uses a database to store localization languages. Of course, one can use resource files), and get the localized text by the resource key.

Suppose we have a Notes view model:

[StringLength(100, ErrorMessage="NotesError"]
[Display(Name = "Notes")]
public string Notes { get; set; }

In the page .cshtml file, we can use the class like this:

@using MyUIResources
{
    var ui = new UiResources();
}

In places where localization is needed, call the class method:

<td>
    @ui.GetResourceValueFromDatabase( "Notes" ) 
<!-- Text "Notes" does not come from the model display name. -->
    @Html.ValidationMessageFor( m => m.Notes, @ui.GetResourceValueFromDatabase( "NotesError" ) )
<!-- Text "NotesError" does not come from the model ErrorMessage. -->
</td>

Then the localized text and validation message (if there is validation error) will be displayed in the table cell.

Though it works, it is not how I want it to work due to comments as above.

There are quite a few samples of how to localize validation messages. But they may be either too heavy weighted, or do not fit into my case.

At first, I wanted to use ErrorMessageResourceName as the resource key, and create a localization provider. So define a view model like this:

[StringLength(100, ErrorMessageResourceName="NotesError", 
	ErrorMessageResourceType = typeof(MyLocalizationProvider)]
[Display(Name = "Notes")]
public string Notes { get; set; }

And then define the resource type like this:

using MyUIResources
{
    public class MyLocalizationProvider
    {
        static readonly UiResources ui = new UiResources();
        public static string NotesError
        {
            get { return ui.GetResourceValueFromDatabase( 'NotesError' ); }
        }
        public static string OtherError
        {
            get { return ui.GetResourceValueFromDatabase( 'OtherError' ); }
        }
        
        ...
    }
}

It works fine with validation error message, but not with display name. The worst with this approach is that it needs every message to be generated by such a GETTER method, which is not practical even for a middle size web site. It is said that this issue can be addressed by T4 Text Template. But I did not know how to use it and it needs approval from the architect.

So I investigated and experimented. Finally I made it by changing the model data annotation metadata and using the display name and error message as resource keys.

using (some other namespaces);
using System.ComponentModel.DataAnnotations;

namespace MyUIResources
{
    public class MyLocalizationProvider : DataAnnotationsModelMetadataProvider
    {
        private static UiResources ui = new UiResources();
        protected override ModelMetadata CreateMetadata(
                             IEnumerable<attribute> attributes ,
                             Type containerType ,
                             Func<object> modelAccessor ,
                             Type modelType ,
                             string propertyName )
        {

            string sKey = string.Empty;
            string sLocalizedText = string.Empty;

            HttpContext.Current.Application.Lock();          
            foreach ( var attr in attributes )
            {
                if ( attr != null )
                {
                    string typeName = attr.GetType().Name;
                    string attrAppKey = string.Empty;

                    if ( typeName.Equals( "DisplayAttribute" ) )
                    {
                        sKey = ( ( DisplayAttribute ) attr ).Name;

                        if ( !string.IsNullOrEmpty( sKey ) )
                        {
                            attrAppKey = string.Format( "{0}-{1}-{2}" , 
                            containerType.Name , propertyName , typeName );
                            if ( HttpContext.Current.Application [ attrAppKey ] == null )
                            {
                                HttpContext.Current.Application [ attrAppKey ] = sKey;
                            }
                            else
                            {
                                sKey = HttpContext.Current.Application [ attrAppKey ].ToString();
                            }

                            sLocalizedText = ui.GetResourceValueFromDb( sKey );
                            if ( string.IsNullOrEmpty( sLocalizedText ) )
                            {
                                sLocalizedText = sKey;
                            }

                            ( ( DisplayAttribute ) attr ).Name = sLocalizedText;
                        }
                    }
                    else if ( attr is ValidationAttribute )
                    {
                        sKey = ( ( ValidationAttribute ) attr ).ErrorMessage;

                        if ( !string.IsNullOrEmpty( sKey ) )
                        {
                            attrAppKey = string.Format( "{0}-{1}-{2}" , 
                            containerType.Name , propertyName , typeName );
                            if ( HttpContext.Current.Application [ attrAppKey ] == null )
                            {
                                HttpContext.Current.Application [ attrAppKey ] = sKey;
                            }
                            else
                            {
                                sKey = HttpContext.Current.Application [ attrAppKey ].ToString();
                            }

                            sLocalizedText = ui.GetResourceValueFromDb( sKey );
                            if ( string.IsNullOrEmpty( sLocalizedText ) )
                            {
                                sLocalizedText = sKey;
                            }

                            ( ( ValidationAttribute ) attr ).ErrorMessage = sLocalizedText;
                        }
                    }
                }
            }
            HttpContext.Current.Application.UnLock();

            return base.CreateMetadata
              (attributes, containerType, modelAccessor, modelType, propertyName);
        }
    }
}

The main point here is to use the Application object as a resource key container to hold the key as the display name and error message will be changed to the corresponding localized texts. In the Application object, string.Format( "{0}-{1}-{2}" , containerType.Name , propertyName , typeName ) will effectively make a unique key for each display name or error message.

This localization provider should be registered in Application_Start() in Global.asax.cs:

protected void Application_Start()
{
    ...... 
    ModelMetadataProviders.Current = new MyUIResources.MyLocalizationProvider();
}

With this provider in place, we can show Notes property in the view in a centralized and kinda view-model coupled way:

@Html.DisplayNameFor( m => m.Notes )
@Html.TextBoxFor( m => m.Notes )
@Html.ValidationMessageFor( m => m.Notes )

If we need to add RequiredAttribute, just simply add the annotation to Notes property:

[Required(ErrorMessage = "RequiredMsg")]
[StringLength(100, ErrorMessage="NotesError")]
[Display(Name = "Notes")]
public string Notes { get; set; }

And @Html.ValidationMessageFor( m => m.Notes ) will also take the responsibility to show Required localized message in addition to StringLength error message, provided that there is the "RequiredMsg" resource key.

Sounds great, eh?