EPiServer - How to localize your view models

Introduction

In EPiServer CMS, we can decorate page properties with Required attribute and set the error message like this:

[Display(
    GroupName = SystemTabNames.Content,
    Order = 100)]
[Required(ErrorMessage = "/validation/mymessage")]
public virtual string MyProperty { get; set; }

We can then create an XML file to localize our message:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<languages>
  <language name="English" id="en">
    <validation>
        <mymessage>MY MESSAGE - en</mymessage>
    </validation>
  </language>
</languages>

And finally, in Edit mode, when we try to publish the page with an empty value for MyProperty, we should get a nice localized message:

episerver error message

We can do the same for Display attribute and Name value.

Note: in EPiServer.UI 9.8.0.0, this approach for setting Display Name works only in All Properties view, while On-Page Edit view shows "/validation/mymessage".

Public Web

Sometimes we want to re-use the same functionality outside Edit mode.

Let's say we have defined a viewmodel like this:

public class MyViewModel : PageViewModel<MyPage>
{
    public MyViewModel(MyPage currentPage) : base(currentPage)
    {
    }

    [Required(ErrorMessage = "/validation/mymessage")]
    [Display(Name = "/name/myname")]
    public string MyProperty { get; set; }
}

Localized strings in the XML files like this:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<languages>
  <language name="English" id="en">
    <validation>
        <mymessage>MY MESSAGE - en</mymessage>
    </validation>
    <name>
      <myname>MY NAME - en</myname>
    </name>
  </language>
</languages>

And created a view like this:

@model MyViewModel
@{
    Layout = "~/Views/Shared/Layouts/_LeftNavigation.cshtml";
    Html.EnableClientValidation(true);
    Html.EnableUnobtrusiveJavaScript(true);
}

<form>
    @Html.LabelFor(x => x.MyProperty)
    @Html.TextBoxFor(x => x.MyProperty)
    <div class="validation">@Html.ValidationMessageFor(x => x.MyProperty)</div>
    <input type="submit" value="click me" />
</form>

If we now run the application and click on the submit button, we will get unlocalized strings:

EPiServer unlocalized error messages

To make Required and Display attributes work with EPiServer's LocalizationProvider, we need to do the following:

  1. Create custom ModelMetadataProvider, which is responsible for localizing strings inside Display attribute
  2. Create custom ModelValidatorProvider, which is responsible for server-side and client-side error messages from Required attribute
  3. Register our custom ModelMetadataProvider and ModelValidator on application startup

Step 1 - ModelMetadataProvider

ModelMetadataProvider provider is the place where we want to localize strings from DisplayAttribute. We only want to localize strings that start with "/"

public class LocalizableModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
    protected override ModelMetadata CreateMetadata(
        IEnumerable<Attribute> attributes,
        Type containerType,
        Func<object> modelAccessor,
        Type modelType,
        string propertyName)
    {
        var attributesList = attributes as List<Attribute> ?? attributes.ToList();

        if (propertyName != null)
        {
            // we only want to localize DisplayAttribute
            var displayAttribute = attributesList.OfType<DisplayAttribute>().FirstOrDefault();

            if (!string.IsNullOrEmpty(displayAttribute?.Name) &&
                displayAttribute.Name.StartsWith("/"))
            {
                var metadata = new ModelMetadata(this, containerType, modelAccessor, modelType, propertyName)
                {
                    DisplayName = LocalizationService.Current.GetString(displayAttribute.Name)
                };

                return metadata;
            }
        }

        // DisplayAttribute not found
        return base.CreateMetadata(attributesList, containerType, modelAccessor, modelType, propertyName);
    }
}

Step 2 - ModelValidatorProvider and ModelValidator

Our custom ModelValidatorProvider will get all validators that are defined on the model (Required attribute), and localize error messages using our custom ModelValidator.

ModelValidatorProvider:

public class LocalizableModelValidatorProvider : DataAnnotationsModelValidatorProvider
{
    protected override IEnumerable<ModelValidator> GetValidators(
        ModelMetadata metadata,
        ControllerContext context,
        IEnumerable<Attribute> attributes)
    {
        var validators = base.GetValidators(metadata, context, attributes);
        var localizedValidators = validators.Select(validator =>
            new LocalizableModelValidator(validator, metadata, context)).ToList();

        return localizedValidators;
    }
}

ModelValidator:

public class LocalizableModelValidator : ModelValidator
{
    private readonly ModelValidator _innerValidator;

    public LocalizableModelValidator(ModelValidator innerValidator,
                                        ModelMetadata metadata,
                                        ControllerContext controllerContext)
        : base(metadata, controllerContext)
    {
        _innerValidator = innerValidator;
    }

    public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
    {
        // get validation rules
        var rules = _innerValidator.GetClientValidationRules();
        var modelClientValidationRules = rules as List<ModelClientValidationRule> ?? rules.ToList();

        foreach (var rule in modelClientValidationRules)
        {
            // localize the error message if it starts with "/"
            if (!string.IsNullOrEmpty(rule.ErrorMessage) && rule.ErrorMessage.StartsWith("/"))
            {
                rule.ErrorMessage = LocalizationService.Current.GetString(rule.ErrorMessage);
            }
        }
        return modelClientValidationRules;
    }

    public override IEnumerable<ModelValidationResult> Validate(object container)
    {
        // execute inner validation
        var results = _innerValidator.Validate(container);
        var modelValidationResults = results as List<ModelValidationResult> ?? results.ToList();

        foreach (var result in modelValidationResults)
        {
            // localize the message if it starts with "/"
            if (!string.IsNullOrWhiteSpace(result.Message) && result.Message.StartsWith("/"))
            {
                result.Message = LocalizationService.Current.GetString(result.Message);
            }
        }

        return modelValidationResults;
    }
}

GetClientValidationRules method is responsible for client-side validation. If client-side validation is not enabled, this method will not be called.

Validate method is responsible for server-side validation.

If the model does not have any validators defined (for example, Required attribute), neither GetClientValidationRules nor Validate methods will be called.

Step 3 - Registering our custom classes

We can register our custom classes in either Global.asax or initialization module.

Here is the code:

[InitializableModule]
public class ModuleInitiaization : IInitializableModule
{
    public void Initialize(InitializationEngine context)
    {
        ModelMetadataProviders.Current = new LocalizableModelMetadataProvider();

        var existingProvider = ModelValidatorProviders
            .Providers
            .OfType<DataAnnotationsModelValidatorProvider>()
            .FirstOrDefault();

        if (existingProvider != null)
        {
            ModelValidatorProviders.Providers.Remove(existingProvider);
        }

        ModelValidatorProviders.Providers.Add(new LocalizableModelValidatorProvider());
    }

    public void Uninitialize(InitializationEngine context)
    {
    }
}

If we now rebuild the solution and restart the application, we should get localized strings:

episerver localization

comments powered by Disqus