Click here to Skip to main content
Click here to Skip to main content

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

, 22 Dec 2012
Rate this:
Please Sign up or sign in to vote.
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. --></span />
    @Html.ValidationMessageFor( m => m.Notes, @ui.GetResourceValueFromDatabase( "NotesError" ) )
<!-- Text "NotesError" does not come from the model ErrorMessage. --></span />
</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?

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

scott_liu
Software Developer (Senior)
Canada Canada
No Biography provided

Comments and Discussions

 
QuestionGreat post!! PinmemberMember 108639094-Jun-14 2:51 
QuestionMore hits to DB PinmemberTAMIZH199012-Dec-13 22:08 
AnswerRe: More hits to DB PinmemberMuhammad Sharjeel NARZQ22-Jan-14 1:03 
GeneralMy vote of 5 Pinmembermohamed sobhey mahmoud27-Jun-13 3:26 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140721.1 | Last Updated 23 Dec 2012
Article Copyright 2012 by scott_liu
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid