web 2.0

ASP.NET MVC 5 Internationalization · Strings Localization on the client-side

In this post, I will show a technique on how to localize and use strings on the client-side based on the previous article ASP.NET MVC Internationalization.

Nowadays, most web applications rely heavily on Javascript, and it is very likely that a web application needs to display some messages to the end user using Javascript whether for validation or notification purpose.

One way of solving this problem is by creating a unique javascript file that contains all the localization strings per culture (e.g. resouces.en-us.js, resources.es.js), similar to the .resx files. This method violates the DRY principle and is hard to maintain. Adding one extra resource requires all the javascript files to be updated.

A better solution is to get the strings from the server-side dynamically. This conforms with the DRY principle. All the resources come from one single place (resx, database, xml, etc). See How to Store Strings in a Database or Xml

How to get the strings dynamically?

By adding a new controller and action, it is possible to return all the needed resources using ajax.

Add a new Controller called ResourceController and a new action called GetResources.

using Resource = Resources.Resources;
namespace MvcInternationalization.Controllers
{
    public class ResourceController : BaseController
    {
        
        public JsonResult GetResources()
        {
           return Json(
                typeof(Resource)
                .GetProperties()
                .Where(p => !p.Name.IsLikeAny("ResourceManager", "Culture")) // Skip the properties you don't need on the client side.
                .ToDictionary(p => p.Name, p => p.GetValue(null) as string)
                 , JsonRequestBehavior.AllowGet);
        }
    }
}
 

Or the following equivalent non-reflection version

using Resource = Resources.Resources;
namespace MvcInternationalization.Controllers
{
    public class ResourceController : BaseController
    {
        
        public JsonResult GetResources()
        {
            return Json(new Dictionary<string, string> { 
                {"Age", Resource.Age},
                {"FirstName", Resource.FirstName},
                {"LastName", Resource.LastName},
                {"EnterNumber", Resource.EnterNumber}
                   
            }, JsonRequestBehavior.AllowGet);
        }
    }
}
 

Before accessing the localized strings, they need to be fetched from the server and stored in a global variable. I will put the code in _Layout.cshtml for illustration purpose.

`
<script type="text/javascript">
    var resources = {}; // Global variable.
    
    (function ($) {
        $.getJSON("@Url.Action("GetResources", "Resource")", function(data){
            resources = data;
        });
    })(jQuery);
    </script>

All the resources now can be accessed via the resources variable in Javascript. Make sure that the data is fetched from the server first before the resources variable is accessed. This can lead to subtle bugs. If the resources are needed when the page loads immediately, then loading all of them from the server in a mater view (eg _Layout.cshtml) is recommended.

If I type resources in my browser's console window

How to localize the annoying message “The field XXX must be a number.”?

There is no easy way to localize this text on the server-side besides changing the value of the attribute data-val-number manually. However, it is much easier to fix this on the client side.

The following code will not work as you might expect:

             $("input[data-val-number]").attr("data-val-number", resources.EnterNumber);

The reason is that the value is read once and cached by the unobtrusive validation script.

Anyway, here is the working version. It took me a while to figure it out:

    var resources = {}; // Global variable.
        (function ($) {
            $.getJSON("@Url.Action("GetResources", "Resource")", function(data){
                resources = data;
                ReplaceInvalidNumberMessage(resources.EnterNumber);
            });
            ReplaceInvalidNumberMessage = function (message) {
                $("form").each(function () {
                    var $form = $(this);
                    $.each($form.validate().settings.messages, function () {
                        if (this["number"] !== undefined) {
                            this.number = message;
                        }
                    });
                });
            }
        })(jQuery);

How to improve performance?

Reading the resources on every page load is acceptable for small scale web applications. However, it can be quite expensive for highly accessed web sites even though the strings are individually cached on the server side.

The solution is output caching. Output caching prevents JSON serialization on each method call.

The regular output caching does not work because the values vary per culture. The web site ends up serving the same culture for all users. Custom output caching is the way to go.

// GET: /Resource/GetResources
const int durationInSeconds = 2 * 60 * 60;  // 2 hours.
[OutputCache(VaryByCustom = "culture", Duration = durationInSeconds)] 
public JsonResult GetResources()
{
    return Json(
         typeof(Resource)
            .GetProperties()
            .Where(p => !p.Name.IsLikeAny("ResourceManager", "Culture")) // Skip the properties you don't need on the client side.
            .ToDictionary(p => p.Name, p => p.GetValue(null) as string)
         , JsonRequestBehavior.AllowGet);
}

I put the duration value in a separate variable on purpose. It’s not obvious whether Duration is in seconds or milliseconds unless I hover the mouse over it.

Custom output caching needs some explicit handling in the Global.asax.cs file:

public override string GetVaryByCustomString(HttpContext context, string custom)
{            
    if (custom == "culture") // culture name (e.g. "en-US") is what should vary caching
    {
        string cultureName = null;
              
        // Attempt to read the culture cookie from Request
        HttpCookie cultureCookie = Request.Cookies["_culture"];
        if (cultureCookie != null) {
            cultureName = cultureCookie.Value;
        }
        else {
            cultureName = Request.UserLanguages != null 
            && Request.UserLanguages.Length > 0 ? 
            Request.UserLanguages[0] : null; // obtain it from HTTP header AcceptLanguages
        }
                
        // Validate culture name
        cultureName = CultureHelper.GetImplementedCulture(cultureName);
        return cultureName.ToLower(); // use culture name as the cache key, "es", "en-us", "es-cl", etc.
    }
    return base.GetVaryByCustomString(context, custom);
}
    

Tags:

Comments

Stephen Fuqua United States, on 6/17/2014 2:40:04 PM Said:

Stephen Fuqua

I was looking at loading global variables from resource files as needed, in my cshtml files. Loading them all at once with the output cache is an interesting idea and worth exploring. Should be slightly more productive overall.

Minor point for improving this: use "global abatement" to prevent global variable collisions. Just ran across that in someone else's blog, at blog.endpoint.com/.../...-global-variables-in.html

Nick United Kingdom, on 6/23/2014 11:58:18 PM Said:

Nick

I have to say this is pretty wrong, "There is no easy way to localize this text on the server-side besides changing the value of the attribute data-val-number manually. However, it is much easier to fix this on the client side."
All you need to do is override the BeginExecuteCore method on the controller, and switch the UICulture to that of your user. When the model binds, and fails, the error message will be looked up from you resource file in the correct language (assuming you added the error message onto the model as a resource value).

Bill United States, on 6/24/2014 4:03:05 PM Said:

Bill

I truly enjoy reading this article. I am still trying to draw a connection between the article you wrote here with the article on www.hanselman.com/.../...criptAndJQueryPart1.aspx.

Scott talks about Internationalization in ASP.NET MVC 3 too

Nadeem United States, on 6/25/2014 4:46:13 AM Said:

Nadeem

@Nick,
You are confused.

You cannot localize the text The field XXX must be a number simply by changing the UICulture in BeginExecuteCore . It seems to me you never tried it.

DevMec Algeria, on 7/21/2014 12:10:33 AM Said:

DevMec

Hi,

What is IsLikeAny ??? I have an error in this line.

.Where(p => !p.Name.IsLikeAny("ResourceManager", "Culture")) // Skip the properties you don't need on the client side.

IsLiKeAny is not recognzed by the compilator...

Nadeem United States, on 7/21/2014 4:42:49 AM Said:

Nadeem

@DevMec,

IsLikeAny is an extension method defined in the Nuget package http://www.nuget.org/packages/ExtensionMethods/

Add comment


(Will show your Gravatar icon)

  Country flag

Click to change captcha
biuquote
  • Comment
  • Preview
Loading



Google+