I've recently been working on a web application that required localisation - the display of translated copy depending on which country the user was visiting from. There was a different sub-domain for each country which was used to identify the appropriate one.
It was also a requirement that the editor's of the website could update the translated copy from a simple back-office tool.
For most of the copy of the website this was fairly straightforward to set up. I had a database table called Texts that contained three columns: Country, Key and Value. And hence by looking up for a particular country and key I could get the text to display.
To ensure this was performant in practice what I did for each web request was to look up all the keys and values for the appropriate market and populate a dictionary object. The look-up was cached using so I was only hitting the database when I needed to rather than on every request.
I then created a base view model that all other view models were derived from that looked like this:
public abstract class BaseViewModel
{
public BaseViewModel(IList<Text> dictionary)
{
Dictionary = dictionary;
}
public IList<Text> Dictionary { get; set; }
public string GetText(string key)
{
if (Dictionary != null && !string.IsNullOrEmpty(key))
{
var text = Dictionary.Where(x => x.Key == key).SingleOrDefault();
if (text != null)
{
return text.Value;
}
}
return string.Empty;
}
}
With this in place, rendering the appropriate text from the view was a simple matter of calling the GetText method and passing the required key:
@Model.GetText("copyright")
All very straightforward so far, but the piece that needed a bit more thought was how to handle form validation messages, supporting both client and server side methods. There's a fairly well established method using resource files, but in my case that wasn't ideal as I wanted to support the editor's making amends to the texts, and hence needed to have them in the database rather than baked into a resource.
First step was to apply the usual data annotations, but rather than hard coding the error messages I instead used the keys from my translations table, e.g.
[Required(ErrorMessage = "contact_validation_first_name_required")]
[StringLength(20, ErrorMessage = "contact_validation_first_name_length")]
public string FirstName { get; set; }
In my view I set up the field as follows:
@Html.EditorFor(m => m.FirstName)
@Html.LocalizedValidationMessageFor(m => m.FirstName, Model.Dictionary)
LocalizedValidationMessageFor was a custom helper used to render the appropriate error message for the country, and was implemented as an extension method on the HtmlHelper:
public static class LocalizationHelpers
{
public static MvcHtmlString LocalizedValidationMessageFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IList<Text> dictionary)
{
// Get HTML returned from base ValidationMessageFor method
var html = ValidationExtensions.ValidationMessageFor(htmlHelper, expression);
// Check we are looking at one that is flagging a validation error
if (html != null && dictionary != null && html.ToString().Contains("field-validation-error"))
{
// Get the key from the HTML (it contains a single span tag)
var key = html.Substring(html.IndexOf(">") + 1).Replace("</span>", string.Empty);
// Look up the text value based on the key from the passed dictionary
var text = dictionary.Where(x => x.Key == key).SingleOrDefault();
if (text != null)
{
// Replace the key with the translation
var amendedHtml = html.ToString().Replace(key, text.Value);
return MvcHtmlString.Create(amendedHtml);
}
}
return html;
}
}
That worked nicely for server side validation messages, but the client side ones would still display the key rather than the translated text. To handle this scenario I added a further method the BaseViewModel which would return the part of the dictionary of key/value pairs as a JSON result:
public string GetDictionaryAsJson(string stem = "")
{
if (Dictionary != null)
{
var serializer = new JavaScriptSerializer();
return serializer.Serialize(Dictionary
.Where(x => string.IsNullOrEmpty(stem) || x.Key.StartsWith(stem))
.Select(x => new { Key = x.Key, Value = x.Value }));
}
return string.Empty;
}
From within the view, I made a call to a javascript function, passing in this dictionary in JSON format:
localizeClientSideValidationMessages(@Html.Raw(Model.GetDictionaryAsJson("contact_validation")));
That function looked like the following, where for each tye of validation I was using (required, string length etc.) the key that was currently rendered was replaced with the appropriate translated text retrieved from the dictionary:
function localizeClientSideValidationMessages(dictionary) {
// Convert to JSON arry
dictionary = eval(dictionary);
// Localize fields (need to call for each type of validation)
localizeFieldValidation(dictionary, "data-val-required");
localizeFieldValidation(dictionary, "data-val-length");
localizeFieldValidation(dictionary, "data-val-regex");
localizeFieldValidation(dictionary, "data-val-equalto");
// Reparse form (necessary to ensure updates)
$("form").removeData("validator");
$("form").removeData("unobtrusiveValidation");
$.validator.unobtrusive.parse("form");
}
function localizeFieldValidation(dictionary, validationAttribute) {
// For each form element with validation attribute, replace the key with the translated text
$("input[" + validationAttribute + "],select[" + validationAttribute + "],textarea[" + validationAttribute + "]").each(function (index) {
$(this).attr(validationAttribute, getLocalizedValue(dictionary, $(this).attr(validationAttribute)));
});
}
function getLocalizedValue(dictionary, key) {
// Look up the value for the passed key
for (var item in dictionary) {
if (dictionary.hasOwnProperty(item)) {
if (dictionary[item].Key == key) {
return dictionary[item].Value;
}
}
}
return "";
}
Comments
Post a Comment