views:

88

answers:

2

I'm working with a client that wants the URLs in our web application to be in French. I'm an English developer and we also have English clients. This is an interesting problem but I don't think its something the ASP.NET MVC Framework would support.

Here's the scenario. The route...

Specific EXAMPLE
English URL
www.stackoverflow.com/questions/ask

would also support

French URL
www.stackoverflow.com/problème/poser

Generic EXAMPLE
English URL
http://clientA.product.com/AreaNameEnglish/ControllerNameEnglish/ActionNameEnglish/params

also needs to support

French URL
http://clientB.product.com/AreaNameFrench/ControllerNameFrench/ActionNameFrench/params

So in MVC my Area, Controller and Actions all need to have both English and French translations.

Obviously maintainability would be a HUGE issue if I were to go and hardcode all my Controllers, Views and Action names to French. Is there anyway to localize the route that is presented in the browser without doing this? Keeping in mind there are lots of different routes in the application. A couple Areas each with a handful of Controller each with many Actions?

Thanks,
Justin

EDIT
Thanks to @womp here is what I've come up with so far... Although in the end I took the approach which I posted as an answer.

public class LocalizedControllerFactory : DefaultControllerFactory
{
    public override IController CreateController(RequestContext requestContext, string controllerName)
    {
        if (string.IsNullOrEmpty(controllerName))
            throw new ArgumentNullException("controllerName");

        if (CultureInfo.CurrentCulture.TwoLetterISOLanguageName == "fr")
        {
            controllerName = this.ReplaceControllerName(requestContext, controllerName);
            this.ReplaceActionName(requestContext);
            this.ReplaceAreaName(requestContext);
        }

        return base.CreateController(requestContext, controllerName);
    }

    private string ReplaceControllerName(RequestContext requestContext, string controllerName)
    {
        // would use the language above to pick the propery controllerMapper.  For now just have french
        Dictionary<string, string> controllerMapper = new Dictionary<string, string>()
        {
            {"frenchControllerA", "englishControllerA"},
            {"frenchControllerB", "englishControllerB"}
        };

        return this.ReplaceRouteValue(requestContext, "controller", controllerMapper);
    }

    private void ReplaceAreaName(RequestContext requestContext)
    {
        // would use the language above to pick the propery areaMapper.  For now just have french
        Dictionary<string, string> areaMapper = new Dictionary<string, string>()
        {
            {"frenchAreaX", "englishAreaX"},
            {"frenchAreaY", "englishAreaY"}
        };

        this.ReplaceRouteValue(requestContext, "area", areaMapper);
    }

    private void ReplaceActionName(RequestContext requestContext)
    {
        // would use the language above to pick the propery actionMapper.  For now just have french
        Dictionary<string, string> actionMapper = new Dictionary<string, string>()
        {
            {"frenchAction1", "englishAction1"},
            {"frenchAction2", "englishAction2"}
        };

        this.ReplaceRouteValue(requestContext, "action", actionMapper);
    }

    private string ReplaceRouteValue(RequestContext requestContext, string paramName, Dictionary<string, string> translationLookup)
    {
        if (requestContext.RouteData.Values[paramName] == null)
        {
            return null;
        }

        string srcRouteValue = requestContext.RouteData.Values[paramName] as string;
        if (srcRouteValue != null && translationLookup.ContainsKey(srcRouteValue))
        {
            requestContext.RouteData.Values[paramName] = translationLookup[srcRouteValue];
        }

        return requestContext.RouteData.Values[paramName] as string;
    }
}

A decent start. If I localize just the ControllerName and ActionName in the Url it will find and render the proper View. However I have the following problems.

Area Name can't be translated
Localizing the Area means the Controller.View() method fails to find Views. Even though I've replaced the Area name in the request context the ViewEngineCollection.Find() method doesn't seem to pick it up. Anywhere in my Controller class that does "return View()" fails to find the default view for its action. If I don't localize the Area then the other steps work.

RedirectToAction or Html.ActionLink
Anytime the application calls RedirectToAction or if I use an Html.ActionLink helper or something similiar the Urls generate are the English ones. It looks like I'm going to have to add logic somewhere possibly in multiple spots to convert an English Url to the French (or other language) one.

+3  A: 

The MVC framework supports pretty much any routing scenario you can think of, but not necessarily with the default routing classes.

Most localization solutions I've run across involve using the same Controller and Action method names, but specifying a culture parameter in the route which dictates which translated version of the View is presented. For example,

http://clientA.product.com/AreaName/Controller/Action    //en-US
http://clientB.product.com/es-MX/AreaName/Controller/Action   // spanish

If you really must have translated URL's though, I don't see much other choice then to maintain a mapping table somewhere. If I understand your question correctly, you need to be able to map all the different language translations of "questions" (controller) and "ask" (action) to the same controller/action method combination.

However, once you've built this table somewhere (resource files?), you can easily override the DefaultControllerFactory that the framework is using, and implement your own logic for determining the controller to instantiate. So instead of just matching the {controller} token from the URL as a simple string comparison, you can implement logic to check it against your mapping table to pick the right controller.

For a walkthrough of creating a custom controller factory, check this great blog post. It's actually a localization example as well, but it's based on the user's culture settings, rather than the language of the URL.

womp
Very interesting. I'm looking into it. Thanks for the kick start.
Justin
You're welcome, good luck with it. If my answer helped at all, don't forget to upvote :)
womp
Well I'm stuck on the Request.Header. I'm looking into it but if you can tell me how to configure teh "ex-MX" part to be interpretted as a request header it would help. I couldn't find it the sample blog post. I would have expected it to be part of the RouteTable in the Global.asax.cs file but no go.
Justin
Sorry, I think I may have confused you. That blog post is a separate method from what I described in my first three paragraphs. The blog post is showing how to select a controller based on the user's browser culture settings.The first method I described would be different than using a custom controller factory. In the first method, you would use a route like {culture}/{area}/{{controller}/{action}, and simply choose your view based on the "culture" token. But in that case, all your URL's would be in English.
womp
Ok so the "Accept-Language" comes from the Browser Language. I've already worked around the language thing as you suggested. At this point I have moderate success:) I have 2 remaining issues. I'll update the question to describe them. Check back soon.
Justin
While this was a great idea I think the one I've posted from Maarten Balliauw's blog is a cleaner way to approach the problem. But thanks for the help! and yes I still gave you an upvote!
Justin
+1  A: 

The following blog contains a complete solution this exact problem. Its actually a very elegant solution which I highly recommend.

http://blog.maartenballiauw.be/post/2010/01/26/Translating-routes-(ASPNET-MVC-and-Webforms).aspx

Note to get it working for AREAs I had to add the following extension method to his "TranslatedRouteCollectionExtensions.cs" class:

    public static Route MapTranslatedRoute(this AreaRegistrationContext areaContext, string name, string url, object defaults, object routeValueTranslationProviders, bool setDetectedCulture)
    {
        TranslatedRoute route = new TranslatedRoute(
            url,
            new RouteValueDictionary(defaults),
            new RouteValueDictionary(routeValueTranslationProviders),
            setDetectedCulture,
            new MvcRouteHandler());

        route.DataTokens["area"] = areaContext.AreaName;

        // disabling the namespace lookup fallback mechanism keeps this areas from accidentally picking up
        // controllers belonging to other areas
        bool useNamespaceFallback = (areaContext.Namespaces == null || areaContext.Namespaces.Count == 0);
        route.DataTokens["UseNamespaceFallback"] = useNamespaceFallback;

        areaContext.Routes.Add(route);

        return route;
    }

However, even with this a translated route with an AREA can be read and interpreted the routes generated always seem to include an English AREA name but localized everything else.

I was directed to a blog via the same question asked on the ASP.NET MVC Forums

Justin
Nice find. It's essentially the same concept, except encapsulated in the routing, rather than in the controller instantiation logic. I agree, it's probably a bit more elegant, it seems like a more appropriate domain for the solution.
womp