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" )
<!----></span />
@Html.ValidationMessageFor( m => m.Notes, @ui.GetResourceValueFromDatabase( "NotesError" ) )
<!----></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?