Create a new solution, containing 2 projects. One is MVC project and another one is class library
Image may be NSFW.
Clik here to view.
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 may be NSFW.
Clik here to view.
Add new T4 template to LocalizedResources project and rename it to match your ResX file:
Image may be NSFW.
Clik here to view.
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 may be NSFW.
Clik here to view.
Every time you add or modify ResX file you need to rerun T4 template in order to generated constant keys for your meesages.
Now the constants are ready to be used with DataAnnotations attributes.
Add a new Person class to your MVC Project:
Image may be NSFW.
Clik here to view.
Use generated constants for providing a resource key for DisplayAttribute. The Display Attribute requires additional parameterResourceType 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 may be NSFW.
Clik here to view.
replace its content with following C# code:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace LocalizedResources
{
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;
}
}
}
}
public static void ReplaceDefaultLocalizedMessage<T>(Assembly assembly, Type resources, string messageKey) where T : ValidationAttribute
{
foreach (var item in GetTypesInNamespace(assembly, null))
{
foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(item))
{
ValidationAttribute dAttr = ((ValidationAttribute)prop.Attributes[typeof(T)]);
if (dAttr != null)
{
dAttr.ErrorMessageResourceType = resources;
dAttr.ErrorMessageResourceName = messageKey;
}
}
}
}
public static void ReplaceDefaultLocalizedMessage<T>(Assembly assembly, string nameSpace, Type resources, string messageKey) where T : ValidationAttribute
{
foreach (var item in GetTypesInNamespace(assembly, nameSpace))
{
ReplaceDefaultLocalizedMessage<T>(item ,resources, messageKey);
}
}
public static void ReplaceDefaultLocalizedMessage<T>(Type item, Type resources, string messageKey) where T : ValidationAttribute
{
foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(item))
{
ValidationAttribute dAttr = ((ValidationAttribute)prop.Attributes[typeof(T)]);
if (dAttr != null)
{
dAttr.ErrorMessageResourceType = resources;
dAttr.ErrorMessageResourceName = messageKey;
}
}
}
private static Type[] GetTypesInNamespace(Assembly assembly, string nameSpace)
{
if (nameSpace != null)
return assembly.GetTypes().Where(t => String.Equals(t.Namespace, nameSpace, StringComparison.Ordinal)).ToArray();
else
return assembly.GetTypes();
}
}
}
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.
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);
}
}
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 may be NSFW.
Clik here to view.
That works fine, although if we post a form, we will see that the Validation messages are not localized:
Image may be NSFW.
Clik here to view.
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 may be NSFW.
Clik here to view.
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);
DataAnnotationsLocalizer.ReplaceDefaultLocalizedMessage<MaxLengthAttribute>(
typeof(Person).Assembly,typeof(Person).Namespace,typeof(ErrorMessages),ErrorMessages.Keys.MaxLengthMessage);
Image may be NSFW.
Clik here to view.
In this way you can overwrite each ValidationAttribute that default message you want to be changed.
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:IValidatableObject
{
[Display(Name = DisplayNames.Keys.FirstName)]
[Required(ErrorMessageResourceName=ErrorMessages.Keys.CustomModelError)]
public string FirstName { get; set; }
And call aDataAnnotationsLocalizer for required attribute type in your ApplicationStart:
DataAnnotationsLocalizer.ReplaceDefaultLocalizedMessage<RequiredAttribute>(
typeof(Person).Assembly, typeof(Person).Namespace, typeof(ErrorMessages), ErrorMessages.Keys.RequiredMessage);
Image may be NSFW.
Clik here to view.
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 may be NSFW.
Clik here to view.
Download a full working example from Source Code session.