Siripongz
Siripongz

Reputation: 89

ASP.NET MVC 3 Localization using route and view

I have searched and tried many localization approaches but all is not exactly what I want. Basically, I want to have my url like this

Then I want these url to map with different View structure as below

/Views
    /_Localization
        /cn
            /Home
                /About.cshtml
                /Index.cshtml
            /Shared
                /_Layout.cshtml
                /Error.cshtml
        /th
            /Home
                /About.cshtml
            /Shared
    /Home
        /About.cshtml
        /Index.cshtml
    /Shared
        /_Layout.cshtml
        /_LogOnPartial.cshtml
        /Error.cshtml
    _ViewStart.cshtml
    Web.config

As you can seen, Thai doesn't have it own Index.cshtml, _Layout.cshtml and Error.cshtml. So, I would like this to fallback to use the default instead. But chinese will use it own.

I have tried to MapRoute like this

routes.MapRoute(
    "DefaultLocal",
    "{lang}/{controller}/{action}/{id}",
    new { lang = "th", controller = "Home", action = "Index", id = UrlParameter.Optional }
);

routes.MapRoute(
    "Default",
    "{controller}/{action}/{id}",
    new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

but I don't know how to point to different View structure. And in this example, Brian Reiter, it use Cookie not url.

So, how can I achieve this. note that I use RazorViewEngine. Thank you for any help and thought.

Upvotes: 1

Views: 873

Answers (1)

ILya
ILya

Reputation: 2778

Due to large amount of code needed i will only illustrate an idea of how it could be done.

You can subclass from RazorViewEngine like this:

public class I18NRazorViewEngine : RazorViewEngine
{
    public I18NRazorViewEngine() : this(null)
    { }

    protected string[] I18NAreaViewLocationFormats;
    protected string[] I18NAreaMasterLocationFormats;
    protected string[] I18NAreaPartialViewLocationFormats;
    protected string[] I18NViewLocationFormats;
    protected string[] I18NMasterLocationFormats;
    protected string[] I18NPartialViewLocationFormats;

    public I18NRazorViewEngine(IViewPageActivator viewPageActivator)
        : base(viewPageActivator)
    {
        this.I18NAreaViewLocationFormats = new string[]
        {
            "~/Areas/{3}/{2}/Views/{1}/{0}.cshtml",
            "~/Areas/{3}/{2}/Views/{1}/{0}.vbhtml",
            "~/Areas/{3}/{2}/Views/Shared/{0}.cshtml",
            "~/Areas/{3}/{2}/Views/Shared/{0}.vbhtml"
        };

        this.I18NAreaMasterLocationFormats = new string[]
        {
            "~/Areas/{3}/{2}/Views/{1}/{0}.cshtml",
            "~/Areas/{3}/{2}/Views/{1}/{0}.vbhtml",
            "~/Areas/{3}/{2}/Views/Shared/{0}.cshtml",
            "~/Areas/{3}/{2}/Views/Shared/{0}.vbhtml"
        };

        this.I18NAreaPartialViewLocationFormats = new string[]
        {
            "~/Areas/{3}/{2}/Views/{1}/{0}.cshtml",
            "~/Areas/{3}/{2}/Views/{1}/{0}.vbhtml",
            "~/Areas/{3}/{2}/Views/Shared/{0}.cshtml",
            "~/Areas/{3}/{2}/Views/Shared/{0}.vbhtml"
        };

        this.I18NViewLocationFormats = new string[]
        {
            "~/Views/{2}/{1}/{0}.cshtml",
            "~/Views/{2}/{1}/{0}.vbhtml",
            "~/Views/{2}/Shared/{0}.cshtml",
            "~/Views/{2}/Shared/{0}.vbhtml"
        };

        this.I18NMasterLocationFormats = new string[]
        {
            "~/Views/{2}/{1}/{0}.cshtml",
            "~/Views/{2}/{1}/{0}.vbhtml",
            "~/Views/{2}/Shared/{0}.cshtml",
            "~/Views/{2}/Shared/{0}.vbhtml"
        };

        this.I18NPartialViewLocationFormats = new string[]
        {
            "~/Views/{2}/{1}/{0}.cshtml",
            "~/Views/{2}/{1}/{0}.vbhtml",
            "~/Views/{2}/Shared/{0}.cshtml",
            "~/Views/{2}/Shared/{0}.vbhtml"
        };
    }

    public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
    {
        var langValue = controllerContext.Controller.ValueProvider.GetValue("lang");
        if (langValue == null || String.IsNullOrEmpty(langValue.AttemptedValue))
            return base.FindView(controllerContext, viewName, masterName, useCache);

        //Code here
    }

    public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
    {
        var langValue = controllerContext.Controller.ValueProvider.GetValue("lang");
        if (langValue == null || String.IsNullOrEmpty(langValue.AttemptedValue))
            return base.FindPartialView(controllerContext, partialViewName, useCache);

        //Code here
    }        
}

The next what you should do is to look inside VirtualPathProviderViewEngine on FindView and FindPartialView inplementations. The reflected code is like this:

public virtual ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
    if (controllerContext == null)
    {
        throw new ArgumentNullException("controllerContext");
    }
    if (string.IsNullOrEmpty(partialViewName))
    {
        throw new ArgumentException(MvcResources.Common_NullOrEmpty, "partialViewName");
    }
    string requiredString = controllerContext.RouteData.GetRequiredString("controller");
    string[] searchedLocations;
    string path = this.GetPath(controllerContext, this.PartialViewLocationFormats, this.AreaPartialViewLocationFormats, "PartialViewLocationFormats", partialViewName, requiredString, "Partial", useCache, out searchedLocations);
    if (string.IsNullOrEmpty(path))
    {
        return new ViewEngineResult(searchedLocations);
    }
    return new ViewEngineResult(this.CreatePartialView(controllerContext, path), this);
}

and

public virtual ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
    if (controllerContext == null)
    {
        throw new ArgumentNullException("controllerContext");
    }
    if (string.IsNullOrEmpty(viewName))
    {
        throw new ArgumentException(MvcResources.Common_NullOrEmpty, "viewName");
    }
    string requiredString = controllerContext.RouteData.GetRequiredString("controller");
    string[] first;
    string path = this.GetPath(controllerContext, this.ViewLocationFormats, this.AreaViewLocationFormats, "ViewLocationFormats", viewName, requiredString, "View", useCache, out first);
    string[] second;
    string path2 = this.GetPath(controllerContext, this.MasterLocationFormats, this.AreaMasterLocationFormats, "MasterLocationFormats", masterName, requiredString, "Master", useCache, out second);
    if (string.IsNullOrEmpty(path) || (string.IsNullOrEmpty(path2) && !string.IsNullOrEmpty(masterName)))
    {
        return new ViewEngineResult(first.Union(second));
    }
    return new ViewEngineResult(this.CreateView(controllerContext, path, path2), this);
}

both methods rely on private GetPath method:

private string GetPath(ControllerContext controllerContext, string[] locations, string[] areaLocations, string locationsPropertyName, string name, string controllerName, string cacheKeyPrefix, bool useCache, out string[] searchedLocations)
{
    searchedLocations = VirtualPathProviderViewEngine._emptyLocations;
    if (string.IsNullOrEmpty(name))
    {
        return string.Empty;
    }
    string areaName = AreaHelpers.GetAreaName(controllerContext.RouteData);
    List<VirtualPathProviderViewEngine.ViewLocation> viewLocations = VirtualPathProviderViewEngine.GetViewLocations(locations, (!string.IsNullOrEmpty(areaName)) ? areaLocations : null);
    if (viewLocations.Count == 0)
    {
        throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, MvcResources.Common_PropertyCannotBeNullOrEmpty, new object[]
        {
            locationsPropertyName
        }));
    }
    bool flag = VirtualPathProviderViewEngine.IsSpecificPath(name);
    string text = this.CreateCacheKey(cacheKeyPrefix, name, flag ? string.Empty : controllerName, areaName);
    if (useCache)
    {
        return this.ViewLocationCache.GetViewLocation(controllerContext.HttpContext, text);
    }
    if (!flag)
    {
        return this.GetPathFromGeneralName(controllerContext, viewLocations, name, controllerName, areaName, text, ref searchedLocations);
    }
    return this.GetPathFromSpecificName(controllerContext, name, text, ref searchedLocations);
}

What you should do is to reimplement it. Most of the code you can reuse, but you should create your own method instead of VirtualPathProviderViewEngine.GetViewLocations. Here its reflected code:

private static List<VirtualPathProviderViewEngine.ViewLocation> GetViewLocations(string[] viewLocationFormats, string[] areaViewLocationFormats)
{
    List<VirtualPathProviderViewEngine.ViewLocation> list = new List<VirtualPathProviderViewEngine.ViewLocation>();
    if (areaViewLocationFormats != null)
    {
        for (int i = 0; i < areaViewLocationFormats.Length; i++)
        {
            string virtualPathFormatString = areaViewLocationFormats[i];
            list.Add(new VirtualPathProviderViewEngine.AreaAwareViewLocation(virtualPathFormatString));
        }
    }
    if (viewLocationFormats != null)
    {
        for (int j = 0; j < viewLocationFormats.Length; j++)
        {
            string virtualPathFormatString2 = viewLocationFormats[j];
            list.Add(new VirtualPathProviderViewEngine.ViewLocation(virtualPathFormatString2));
        }
    }
    return list;
}

You can also reuse most of the code but instead of VirtualPathProviderViewEngine.ViewLocation and VirtualPathProviderViewEngine.AreaAwareViewLocation you should use your own classes. They could be like this:

class ViewLocation
{
    protected string _virtualPathFormatString;
    public ViewLocation(string virtualPathFormatString)
    {
        this._virtualPathFormatString = virtualPathFormatString;
    }
    public virtual string Format(string viewName, string controllerName, string areaName, string lang)
    {
        return string.Format(CultureInfo.InvariantCulture, this._virtualPathFormatString, new object[]
        {
            viewName,
            controllerName,
                    lang
        });
    }
}

and:

class AreaAwareViewLocation : VirtualPathProviderViewEngine.ViewLocation
{
    public AreaAwareViewLocation(string virtualPathFormatString) : base(virtualPathFormatString)
    {
    }
    public override string Format(string viewName, string controllerName, string areaName, string lang)
    {
        return string.Format(CultureInfo.InvariantCulture, this._virtualPathFormatString, new object[]
        {
            viewName,
            controllerName,
            areaName,
                    lang
        });
    }
}

and then when you will call Format methods you should pass langValue.AttemptedValue (it is from scope of FindView and FindPartialView in the first code block) to lang parameter. Normally it's called in VirtualPathProviderViewEngine.GetPathFromGeneralName.

The main advice is to use ILSpy or another disassembler to explore the code of System.Web.Mvc (or even better - download its sources). The goal is to reimplement FindView and FindPartialView. The rest code provided is to illustrate how it's already done in mvc framework.

It's also important to to seek through arrays declared in our new view engine instead of those without I18N prefix which are already there and used by default classes

Hope it will help despite answer is indirect. You can ask additional questions if you will face any difficulties.

P.S. Don't foreget to register you view engine in global.asax.cs after it will be developed.

protected virtual void Application_Start()
{
   ViewEngines.Engines.Clear();
   ViewEngines.Engines.Add(new I18NRazorViewEngine());
}

Upvotes: 3

Related Questions