Asp.Net MVC3 Razor and RESX Localisations/Localizations

I was just applying for a small two week contract that was based on Localisation. This is the work I did to give myself a quick refresh. It reminded me on how well Asp.Net MVC3 works with RESX localisations.

imageAs a start, you will need someway to change your Local Language quickly. My preferred choice is using the Quick Locale Switcher for Firefox which is in the image.

I have (over the last few hours) built up a fully translated version of the default MVC3 project which is viewable at http://resxstuff.adventureswith.net and is viewable as 2 languages available English (Which is default) and French (which is terribly translated BTW).

Turn On The World – Enable Globalisation

To enable globalisation, you will need to add this line to your web.config  in the <system.web></system.web> section:

<globalization uiCulture="auto" culture="auto"/>

Add the RESX files; and let the class be seen

If you are coming from Web Forms, note that for MVC you DO NOT HAVE TO (and perhaps should not) add your RESX to App_GlobalResourses or even App_LocalResources.

Just add them wherever you want and they can still be used, but only if you do the following:

  • When you adda new RESX, Change the Access Modifier to Public

Otherwise you will be wondering why your resources don’t show up.

image

imageI followed the advice given by a few others to use the “RESX to a View” method, where you mimic the View folder structure within the Resources folder; as seen in the picture.

This usually means it is fairly easy to find the right file for the string that you want to change in a hurry.

 

Views – Changing out the text

I went through all the views and changed every “message to the user” into a string resource in a RESX file.

All the Html.LabelFor calls were replaced with resources calls
e.g. @Html.LabelFor (x => x.Username) became @Resources.Account.Register.Username

//Resource file is set with the Name as the Key
[Display(Name = "NewPassword", ResourceType = typeof(Resources.Language))]
public string NewPassword { get; set; }
//Razor syntax stays the same
@Html.LabelFor(x => x.NewPassword)

All boilerplate text was replaced with Resources e.g. “Login was unsuccessful. Please correct the errors and try again.” was pulled out of LogOn.cshtml and moved to Resources.Account.LogOn.Login_Unsuccessful

@Html.ValidationSummary(true, Resources.Account.LogOn.Login_Unsuccessful)

Controllers – Fixing up user messages

Essentially controllers are the same deal as the views. Take the static text, place it in a RESX file where it makes sense.

I used the files related to the views that were to be returned e.g. If an action method returned the Logon view, then I used the Logon.resx file for that text.

using Resources;

....
....
....
[HttpPost]
       public ActionResult Register(RegisterModel model)
        {
            if (ModelState.IsValid)
            {
                // Attempt to register the user
                MembershipCreateStatus createStatus;
                Membership.CreateUser(model.UserName, model.Password, model.Email, null, null, true, null, out createStatus);

                if (createStatus == MembershipCreateStatus.Success)
                {
                    FormsAuthentication.SetAuthCookie(model.UserName, false /* createPersistentCookie */);
                    return RedirectToAction("Index", "Home");
                }
                else
                {
                    ModelState.AddModelError("", ErrorCodeToString(createStatus));
                }
            }

            // If we got this far, something failed, redisplay form
            return View(model);
        }

       private static string ErrorCodeToString(MembershipCreateStatus createStatus)
        {
            // See http://go.microsoft.com/fwlink/?LinkID=177550 for
            // a full list of status codes.
            switch (createStatus)
            {
                case MembershipCreateStatus.DuplicateUserName:
                    return ErrorStrings.Username_Exists;  //these are the resource strings
                case MembershipCreateStatus.DuplicateEmail:
                    return ErrorStrings.Email_Exists;
                case MembershipCreateStatus.InvalidPassword:
                    return ErrorStrings.Invalid_Password;
                case MembershipCreateStatus.InvalidEmail:
                    return ErrorStrings.Invalid_Email;
                case MembershipCreateStatus.InvalidAnswer:
                    return ErrorStrings.Invalid_Answer;
                case MembershipCreateStatus.InvalidQuestion:
                    return ErrorStrings.Invalid_Question;
                case MembershipCreateStatus.InvalidUserName:
                    return ErrorStrings.Invalid_Username;
                case MembershipCreateStatus.ProviderError:
                    return ErrorStrings.Provider_Error;
                case MembershipCreateStatus.UserRejected:
                    return ErrorStrings.User_Rejected;
                default:
                    return ErrorStrings.Default_Error;
            }
        }

Models & Validation

The only tricky part is this one. And it’s not even that tricky.

Instead of where you would normally put ErrorMessage=”Something bad happened here”, you would instead point it the RESX files.

All of the Validation attributes have the Properties ErrorMessageResourceType and ErrorMessageResourceName which are explained as follows:

  • ErrorMessageResourceType – Points to the resource file type
  • ErrorMessageResourceName – Points to the KEY within that resource file

It is sad that the key is not strongly typed, like in the rest of the code; this is the annoying nature of attributes.

So here we have the example of a model with resource friendly validation:

public class LogOnModel     {      

[Required(ErrorMessageResourceType = typeof(ErrorStrings), ErrorMessageResourceName = "Required")]
public string UserName { get; set; }         

[Required(ErrorMessageResourceType = typeof(ErrorStrings), ErrorMessageResourceName = "Required")]
[DataType(DataType.Password)]
public string Password { get; set; }  

}

RESX – Adding more languages

So now you have all the files in place to use the RESX files that have been created as you went along.

Adding RESX files for other languages is easy. Just copy, paste, rename and it works.

The naming convention is as follows (from lowest to highest priority):

  • Default – mytranslation.resx
  • Language – mytranslation.en.resx  or mytranslation.fr.resx
  • Regional – mytranslation.en-GB.resx or mytranslation.en-US.resx

So how do you create these for more languages? Luckily there is a fantastic free opensource tool that is available. ResxTranslator from Codeplex is able to read all your RESX files in the project and help you out by creating the other languages.

After they are created, just include them in the project.

Just a note that I haven’t been able to create another language from inside ResxTranslator, by which I mean add a language that doesn’t already exist somewhere in the resx files already.

First, I have to first create a file with the language in the filename, as shown above, for it to show up as an option in the “Add Language” tool. But once I have added a language/dialect for one RESX, it is available for all files. Maybe I am missing something here.

image

Further Reading

Nadeem Afana – Internationalising MVC3
http://afana.me/post/aspnet-mvc-internationalization.aspx

K. Scott Allen – Resource Files and ASP.NET MVC Projects
http://odetocode.com/Blogs/scott/archive/2009/07/16/resource-files-and-asp-net-mvc-projects.aspx

This entry was posted in ASP.Net and tagged , , . Bookmark the permalink.

6 Responses to Asp.Net MVC3 Razor and RESX Localisations/Localizations

  1. Great post! :-)

    I liked the Firefox extension that I didn’t know about. It helps a lot.

    I thought about avoid removing @Html.LabelFor (x => x.OldPassword) for example. One can replace the string using the Display Name property in the model .cs class using DataAnnotations:


    [Required]
    [DataType(DataType.Password)]
    [Display(Name = ChangePassword.OldPassword)]
    public string OldPassword { get; set; }

  2. Oh, just forget my assumption about the code from the previous comment. It won’t compile. The best thing is to remove LabelFor indeed.

  3. Dann says:

    @Leniel – Yeah, I was hoping to do the same thing. As you figured out, the problem being that attributes produce compile time metadata so won’t take anything in their constructors other than compile-time primitives simple types. http://stackoverflow.com/a/1235690/59532

  4. Marcelo Delgado says:

    You can add the labels to your models by specifying the resource type:

    [Display(Name = "NewPassword", ResourceType = typeof(Resources.Language))]

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>