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