Create a new solution, containing 2 projects. One is MVC project and another one is class library

image

 

Localized resources supposed to be in the LocalizedResources project. MvcDemoApp has dependencies from LocalizedResources project.

Add new DisplayResources.resx file to the LocalizedResources project.

 

image

 

Add new T4 template to LocalizedResources  project and rename  it to match your ResX file:

image

 

 

Add the following content to the T4 template:

 

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Windows.Forms" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Resources" #>
<#@ import namespace="System.IO" #>
<#@ output extension=".cs" #>
<#
		var nameSpace = Host.ResolveParameterValue("directiveId", "namespaceDirectiveProcessor", "namespaceHint");
		var className = "Keys";
		var resName = System.IO.Path.GetFileNameWithoutExtension(Host.TemplateFile);
		var resourceFileName =  System.IO.Path.GetDirectoryName(Host.TemplateFile) 
			+ "\\" 
			+ resName + ".resx";
		var resourceDesignFileName =  System.IO.Path.GetDirectoryName(Host.TemplateFile) 
			+ "\\" 
			+ resName + ".Designer.cs";
		var rr = new System.Resources.ResXResourceReader(resourceFileName);
		rr.UseResXDataNodes = true;
		var dict = rr.GetEnumerator();
		var content = File.ReadAllText(resourceDesignFileName);
		if(!content.Contains("partial"))
		{
			content = content.Replace("class " + resName, "partial class " + resName);
			File.WriteAllText(resourceDesignFileName, content);
		}

#>

using System;

namespace <#= nameSpace#>
{
	partial class <#= resName#> {
		/// <summary>
		/// Autogenerated constants for <#= resName #> resources
		/// </summary>
		public static class <#= className #>
		{
	<#
		  while (dict.MoveNext()){
		  string value = ((string)((System.Resources.ResXDataNode)dict.Value).GetValue((System.ComponentModel.Design.ITypeResolutionService)null));
		  string comment = ((System.Resources.ResXDataNode)dict.Value).Comment;
	#>
		/// <summary>
            /// <#=value.Replace("\r\n", " ")#>
			<#if(!System.String.IsNullOrEmpty(comment) && comment != " ") 
				Write("/// " + comment.Replace("\r\n", " ") + "\r\n\t\t\t/// </summary>");
			  else
			    Write("/// </summary>");	
			#>
            
			public const string <#= dict.Key#>="<#= dict.Key#>";

	<#
	}
	#>
		}
	}


}

Save the T4 Template file. A new class, containing the constants will be generated for you:

 

image

 

Every time you add or modify ResX file you need to rerun T4 template in order to generated constant keys for your messages.

Now the constants are ready to be used with DataAnnotations attributes.

Add a new Person class to your MVC Project:

 

image

Use generated constants for providing a resource key for DisplayAttribute. The Display Attribute requires additional parameter ResourceType to run, but we will replace it generally for all our Model classes.

For this purpose add a new DataAnnotationsLocalizer class to LocalizedDataAnnotations project:

 

image

 

replace its content with following C# code:

public sealed class DataAnnotationsLocalizer
{
    public static void SetDefaultResourceType(Assembly assembly, Type resources, params Type[] localizableAttributeTypes)
    {
        foreach (var item in GetTypesInNamespace(assembly, null))
        {
            SetDefaultResourceType(item, resources, localizableAttributeTypes);

        }
    }

    public static void SetDefaultResourceType(Assembly assembly, string nameSpace, Type resources, params Type[] localizableAttributeTypes)
    {
        foreach (var item in GetTypesInNamespace(assembly, nameSpace))
        {
            SetDefaultResourceType(item, resources, localizableAttributeTypes);
        }
    }

    public static void SetDefaultResourceType(Type item, Type resources, params Type[] localizableAttributeTypes)
    {
        foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(item))
        {
            foreach (var localizableAttrType in localizableAttributeTypes)
            {
                Attribute dAttr = ((Attribute)prop.Attributes[localizableAttrType]);
                if (dAttr != null)
                {
                    if (dAttr is DisplayAttribute && (dAttr as DisplayAttribute).Name != null)
                        ((DisplayAttribute)dAttr).ResourceType = resources;
                    if (dAttr is ValidationAttribute && (dAttr as ValidationAttribute).ErrorMessageResourceName != null)
                        ((ValidationAttribute)dAttr).ErrorMessageResourceType = resources;

                }
            }

        }
    }

The full version of this file can be loaded from here: http://ldawt4.codeplex.com/SourceControl/changeset/view/24598#602072

This class is a helper, that will allow us to set a resource source for our DataAnnotationAttributes.

It can be done, for example, in the Global.asax:

protected void Application_Start() 
{
          DataAnnotationsLocalizer.SetDefaultResourceType(typeof(Person).Assembly, typeof(DisplayNames), typeof(DisplayAttribute));

The SetDefaultResourceType call above, sets a ResourceType  propery of DisplayAttribute to point to the proper Resource type. There are differend methods that allow to set a default resource type for individual type, all types in the sinle namespace of the assembly or for all types within an assembly.

Now our application is ready to run with localized DisplayAttribute. There is an Example of Controller and View classes:

  

Controller:

public class DemoController : Controller
{
    //
    // GET: /Demo/

    public ActionResult Index()
    {
        return View(new Person());
    }

    [HttpPost]
    public ActionResult Save(Person p)
    {
        return View("Index", p);
    }

    public ActionResult SetLanguage(string language)
    {
        Session["SelectedCultureId"] = language;
        return RedirectToAction("Index");
    }

   

    protected override void Initialize(System.Web.Routing.RequestContext requestContext)
    {
        string culture = (string)requestContext.HttpContext.Session["SelectedCultureId"];
        if (culture != null)
        {
            System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo(culture);
            System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(culture);
        }
        base.Initialize(requestContext);
    }

}

 

virtual Initialize method is the best place to switch between different languages, 
In the real-world apllication you will override Initialize method in the base Controller class.

 

View:

@model MvcDemoApp.Models.Person
@{
    ViewBag.Title = "Demo";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>Localization via T4 Template Demo</h2>

@Html.ActionLink("Englisch", "SetLanguage", "Demo", new { language = "en-us" }, null)
@Html.ActionLink("Deutsch", "SetLanguage", "Demo", new { language = "de-de" }, null)

@using(Html.BeginForm("Save", "Demo"))
{
    @Html.ValidationSummary(true);
<fieldset>
    <legend>Person</legend>
    @Html.LabelFor(m => Model.FirstName)
    @Html.EditorFor(m => Model.FirstName)
    @Html.ValidationMessageFor(m => Model.FirstName)
    <br />
    @Html.LabelFor(m => Model.LastName)
    @Html.EditorFor(m => Model.LastName)
    @Html.ValidationMessageFor(m => Model.LastName)
    <br />
    @Html.LabelFor(m => Model.Phone)
    @Html.EditorFor(m => Model.Phone)
    @Html.ValidationMessageFor(m => Model.Phone)
    <br />
    @Html.LabelFor(m => Model.Address)
    @Html.EditorFor(m => Model.Address)
    @Html.ValidationMessageFor(m => Model.Address)
    <br />
    <input type="submit" name="btnSubmit" id="SubmitButton" value="Submit" />
</fieldset>
}

 

If you run this application, and switch the language to German, you will see the following result:

 

image

 

 

That works fine, although if we post a form, we will see that the Validation messages are not localized:

 

image

 

The problem is that the default .NET installation does not contain  a translation for other languages. In order to get ValidationAttributes to be localized you need to install .NET language pack

http://www.microsoft.com/de-de/download/details.aspx?id=3324

 

What is if you are not happy with default Microsoft localization for ValidationAttributes?

Actually, it is not a problem. Create your local copy of messages for each ValidationAttribute:

image

German messages:

image

And then use DataAnottationsLocalizer class to set your custom Validation messages:

protected void Application_Start()
{
    DataAnnotationsLocalizer.SetDefaultResourceType(typeof(Person).Assembly, typeof(DisplayNames), typeof(DisplayAttribute));

    DataAnnotationsLocalizer.ReplaceDefaultLocalizedMessage<RequiredAttribute>(
        typeof(Person).Assembly, typeof(Person).Namespace, typeof(ErrorMessages), ErrorMessages.Keys.RequiredMessage);

 

image

 

In this way you can overwrite each individual ValidationAttribute that default message you want to be changed.

DataAnnotationsLocalizer.ReplaceDefaultLocalizedMessage<RequiredAttribute>( typeof(Person).Assembly, 
typeof(Person).Namespace, 
typeof(ErrorMessages), 
ErrorMessages.Keys.RequiredMessage); 

DataAnnotationsLocalizer.ReplaceDefaultLocalizedMessage<MaxLengthAttribute>( typeof(Person).Assembly,
typeof(Person).Namespace, 
typeof(ErrorMessages), 
ErrorMessages.Keys.MaxLengthMessage);
 

That works fine, but what is if you need a custom localized validation message for a certain field? It works also. Specify  a ErrorMessageResourceName on the required ValidationAttribute

public class Person  
{
    [Display(Name = DisplayNames.Keys.FirstName)]
    [Required(ErrorMessageResourceName=ErrorMessages.Keys.CustomModelError)]
    public string FirstName { get; set; }

 

  And call a DataAnnotationsLocalizer for required Attribute in Allpication_Start

 

DataAnnotationsLocalizer.SetDefaultResourceType(typeof(Person).Assembly, typeof(ErrorMessages),typeof(RequiredAttribute));

 

image

 

 

Provided solution works fine in both MVC and non MVC scenarios. For example if you want to validate an Object outside of MVC, you will get the same localized messages:

class Program
{
    static void Main(string[] args)
    {
        DataAnnotationsLocalizer.SetDefaultResourceType(typeof(Person).Assembly, typeof(DisplayNames), typeof(DisplayAttribute));
        DataAnnotationsLocalizer.SetDefaultResourceType(typeof(Person).Assembly, 
            typeof(ErrorMessages),
            typeof(RequiredAttribute), 
            typeof(MaxLengthAttribute));

        DataAnnotationsLocalizer.ReplaceDefaultLocalizedMessage<RequiredAttribute>(
            typeof(Person).Assembly, typeof(Person).Namespace, typeof(ErrorMessages), ErrorMessages.Keys.RequiredMessage);
        DataAnnotationsLocalizer.ReplaceDefaultLocalizedMessage<MaxLengthAttribute>(
            typeof(Person).Assembly, typeof(Person).Namespace, typeof(ErrorMessages), ErrorMessages.Keys.MaxLengthMessage);


        try
        {
            System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("de-de");
            System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("de-de");

            Person p = new Person { Address = "asdkjalsdkjalskdjasldkjalskdjaskldjaskldjalasdasdasdasdasdasdskdjalskdjaskldjalsdkjalsdk" };

            List<ValidationResult> res = new List<ValidationResult>();
            Validator.TryValidateObject(p, new ValidationContext(p, null, null), res, true);

            foreach (var item in res)
            {
                Console.WriteLine(item.ErrorMessage);
            }
        }
        catch (ValidationException)
        {

        }

        Console.ReadLine();

    }
}

 

 

image

Download a full working example from Source Code session.

http://ldawt4.codeplex.com/SourceControl/changeset/view/24598

Last edited Sep 4, 2012 at 8:09 AM by Andruha, version 28

Comments

No comments yet.