ASP.NET MVC, localized routes and the default language for the user

前端 未结 4 577
臣服心动
臣服心动 2020-12-08 05:28

I am using ASP.NET MVC localized routes. So when a user goes to the English site it is example.com/en/Controller/Action and the Swedish site is example.co

相关标签:
4条回答
  • 2020-12-08 06:11

    This is how I would do it.

    ~~ Disclaimer : psuedo code ~~

    global.asax

    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        routes.IgnoreRoute("{*favicon}",
            new { favicon = @"(.*/)?favicon.ico(/.*)?" });
    
        routes.MapRoute(
            "Question-Answer", // Route name
            "{languageCode}/{controller}/{action}", // URL with parameters
            new {controller = "home", action = "index"} // Parameter defaults
            );
    
    }
    

    Take note: the controller and/or action do NOT need to be first and second. in fact, they do not need to exist at all, in the url with parameters section.

    Then ...

    HomeController.cs

    public ActionResult Index(string languageCode)
    {
       if (string.IsNullOrEmpty(languageCode) ||
          languageCode != a valid language code)
       {
           // No code was provided OR we didn't receive a valid code 
           // which you can't handle... so send them to a 404 page.
           // return ResourceNotFound View ...
       }
    
       // .. do whatever in here ..
    }
    

    Bonus suggestion

    You can also add a Route Constraint to your route, so it only accepts certain strings for the languageCode parameter. So stealing this dude's code ....

    (more pseduo code)...

    public class FromValuesListConstraint : IRouteConstraint
    {
        public FromValuesListConstraint(params string[] values)
        {
            this._values = values;
        }
    
        private string[] _values;
    
        public bool Match(HttpContextBase httpContext,
            Route route,
            string parameterName,
            RouteValueDictionary values,
            RouteDirection routeDirection)
        {
            // Get the value called "parameterName" from the 
            // RouteValueDictionary called "value"
            string value = values[parameterName].ToString();
    
            // Return true is the list of allowed values contains 
            // this value.
            return _values.Contains(value);
        }
    }
    

    means you could do this ......

    routes.MapRoute(
        "Question-Answer", // Route name
        "{languageCode}/{controller}/{action}", // URL with parameters
        new {controller = "home", action = "index"} // Parameter defaults
        new { languageCode = new FromValuesListConstraint("en", "sv", .. etc) }
        );
    

    and there you have it :)

    I do something like this for versioning my MVC Api.

    GL :) Hope this helps.

    0 讨论(0)
  • 2020-12-08 06:22

    I know this is a very old question, but having just had to solve the complete set of related issues, I thought I would share my solution.

    Below is a complete solution, including a few extra tricks to allow easy changing of language. It allows for specific cultures, not just specific languages (but only the language part is retained in this example).

    Features include:

    • Fallback to browser locale in determining language
    • Uses cookies to retain language across visits
    • Override language with url
    • Supports changing language via link (e.g. simple menu options)

    Step 1: Modify RegisterRoutes in RouteConfig

    This new routing includes a constraint (as others also suggest) to ensure the language route does not grab certain standard paths. There is no need for a default language value as that is all handled by the LocalisationAttribute (see step 2).

        public static void RegisterRoutes(RouteCollection routes)
        {
            ...
    
            // Special localisation route mapping - expects specific language/culture code as first param
            routes.MapRoute(
                name: "Localisation",
                url: "{lang}/{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
                constraints: new { lang = @"[a-z]{2}|[a-z]{2}-[a-zA-Z]{2}" }
            );
    
            // Default routing
            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
    
        }
    

    Step 2: Create a Localisation attribute

    This will look at controller requests, before they are handled, and change the current culture based on the URL, a cookie, or the default browser culture.

    // Based on: http://geekswithblogs.net/shaunxu/archive/2010/05/06/localization-in-asp.net-mvc-ndash-3-days-investigation-1-day.aspx
    public class LocalisationAttribute : ActionFilterAttribute
    {
        public const string LangParam = "lang";
        public const string CookieName = "mydomain.CurrentUICulture";
    
        // List of allowed languages in this app (to speed up check)
        private const string Cultures = "en-GB en-US de-DE fr-FR es-ES ro-RO ";
    
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            // Try getting culture from URL first
            var culture = (string)filterContext.RouteData.Values[LangParam];
    
            // If not provided, or the culture does not match the list of known cultures, try cookie or browser setting
            if (string.IsNullOrEmpty(culture) || !Cultures.Contains(culture))
            {
                // load the culture info from the cookie
                var cookie = filterContext.HttpContext.Request.Cookies[CookieName];
                if (cookie != null)
                {
                    // set the culture by the cookie content
                    culture = cookie.Value;
                }
                else
                {
                    // set the culture by the location if not specified
                    culture = filterContext.HttpContext.Request.UserLanguages[0];
                }
                // set the lang value into route data
                filterContext.RouteData.Values[LangParam] = culture;
            }
    
            // Keep the part up to the "-" as the primary language
            var language = culture.Split(new char[] { '-' }, StringSplitOptions.RemoveEmptyEntries)[0];
            filterContext.RouteData.Values[LangParam] = language;
    
            // Set the language - ignore specific culture for now
            Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(language);
    
            // save the locale into cookie (full locale)
            HttpCookie _cookie = new HttpCookie(CookieName, culture);
            _cookie.Expires = DateTime.Now.AddYears(1);
            filterContext.HttpContext.Response.SetCookie(_cookie);
    
            // Pass on to normal controller processing
            base.OnActionExecuting(filterContext);
        }
    }
    

    Step 3: Apply localisation to all controllers

    e.g.

    [Localisation]  <<< ADD THIS TO ALL CONTROLLERS (OR A BASE CONTROLLER)
    public class AccountController : Controller
    {
    

    Step 4: To change language (e.g. from a menu)

    This is where it got a little tricky and required some workarounds.

    Add a ChangeLanguage method to your account controller. This will strip out any existing language code from the "previous path" to allow the new language to take effect.

        // Regex to find only the language code part of the URL - language (aa) or locale (aa-AA) syntax
        static readonly Regex removeLanguage = new Regex(@"/[a-z]{2}/|/[a-z]{2}-[a-zA-Z]{2}/", RegexOptions.Compiled);
    
        [AllowAnonymous]
        public ActionResult ChangeLanguage(string id)
        {
            if (!string.IsNullOrEmpty(id))
            {
                // Decode the return URL and remove any language selector from it
                id = Server.UrlDecode(id);
                id = removeLanguage.Replace(id, @"/");
                return Redirect(id);
            }
            return Redirect(@"/");
        }
    

    Step 5: Add language menu links

    The menu options consist of a link with the new language specified as a route parameter.

    e.g. (Razor example)

    <li>@Html.ActionLink("English", "ChangeLanguage", "Account", new { lang = "en", id = HttpUtility.UrlEncode(Request.RawUrl) }, null)</li>
    <li>@Html.ActionLink("Spanish", "ChangeLanguage", "Account", new { lang = "es", id = HttpUtility.UrlEncode(Request.RawUrl) }, null)</li>
    

    The return URl is the current page, encoded so that it can become the id parameter of the URL. This means that you need to enable certain escape sequences that are otherwise refused by Razor as a potential security violation.

    Note: for non-razor setups you basically want an anchor that has the new language, and the current page relative URL, in a path like: http://website.com/{language}/account/changelanguage/{existingURL}

    where {language} is the new culture code and {existingURL} is a URLencoded version of the current relative page address (so that we will return to the same page, with new language selected).

    Step 6: Enable certain "unsafe" characters in URLs

    The required encoding of the return URL means that you will need to enable certain escape characters, in the web.config, or the existing URL parameter will cause an error.

    In your web.config, find the httpRuntime tag (or add it) in <system.web> and add the following to it (basically remove the % that is in the standard version of this attribute):

      requestPathInvalidCharacters="&lt;,&gt;,&amp;,:,\,?"
    

    In your web.config, find the <system.webserver> section and add the following inside it:

    <security>
      <requestFiltering allowDoubleEscaping="true"/>
    </security>
    
    0 讨论(0)
  • 2020-12-08 06:24

    Ok .. another suggestion.

    To make sure I understand, you want ..

    • Every Action needs to be able to figure out what the LanguageCode is?
    • If an invalid languageCode is provided, then it needs to be reset to a valid default one.

    if so .. this answer has three parts:-

    1. Add the route. (this is cut-paste from my previous answer).

    global.asax

    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        routes.IgnoreRoute("{*favicon}",
            new { favicon = @"(.*/)?favicon.ico(/.*)?" });
    
        routes.MapRoute(
            "Question-Answer", // Route name
            "{languageCode}/{controller}/{action}", // URL with parameters
            new {controller = "home", action = "index"} // Parameter defaults
            );
    
    }
    

    Update (based on comments)

    So, if you want to have the route http://www.example.com/sv/account/logon then the above route will work.

    LanguageCode == sv (or en or fr or whatever language you're supporting)

    account == the controller: AccountController

    login == action.

    the fact that i've said controller = "home" and action="index" only mean that those two parameters are defaulted to those values IF none were provided. So, if you goto http://www.example.com/sv/account/logon then the MVC framework is smart enough to know (based on that route) that the languageCode paramters == sv, controller == action and action (method) == index.

    NOTE: The order of your routes is IMPORTANT. critically important. This route needs to be one (if not the) of the first routes (after the IgonoreRoute's) when you register your routes.


    1. You need to create a custom ActionFilter which will get called BEFORE the action is executed. here's my quick attempt...

    .

    using System.Linq;
    using System.Web.Mvc;
    
    namespace YourNamespace.Web.Application.Models
    {
        public class LanguageCodeActionFilter : ActionFilterAttribute
        {
            // This checks the current langauge code. if there's one missing, it defaults it.
            public override void OnActionExecuting(ActionExecutingContext filterContext)
            {
                const string routeDataKey = "languageCode";
                const string defaultLanguageCode = "sv";
                var validLanguageCodes = new[] {"en", "sv"};
    
                // Determine the language.
                if (filterContext.RouteData.Values[routeDataKey] == null ||
                    !validLanguageCodes.Contains(filterContext.RouteData.Values[routeDataKey]))
                {
                    // Add or overwrite the langauge code value.
                    if (filterContext.RouteData.Values.ContainsKey(routeDataKey))
                    {
                        filterContext.RouteData.Values[routeDataKey] = defaultLanguageCode;
                    }
                    else
                    {
                        filterContext.RouteData.Values.Add(routeDataKey, defaultLanguageCode);    
                    }
                }
    
                base.OnActionExecuting(filterContext);
            }
        }
    }
    
    1. Now you'll need to make a BaseController, which all your controllers inherit from. This will then create an easily accessabily property which all your actions can access .. and then display whatever they want, based on that value.

    here we go ... (pseudo code again....)

    public abstract class BaseController : Controller
    {
        protected string LanguageCode
        {
            get { return (string) ControllerContext.RouteData.Values["LanguageCode"]; }
        }   
    }
    

    So then we decorate our controllers like this :)

    [LanguageCodeActionFilter]
    public class ApiController : BaseController
    {
        public ActionResult Index()
        {
            if (this.LanguageCode == "sv") ... // whatever.. etc..
        }
    }
    

    Notice how i've decorated the class .. not just each action. this means ALL actions in the class will be affected by the ActionFilter :)

    Also, you might want to add a new route in the global.asax that handles NO languageCode .. and hardcode default that value ...

    like (also untested) ...

    routes.MapRoute(
        "Question-Answer", // Route name
        "{controller}/{action}", // URL with parameters
        new {controller = "home", action = "index", languageCode = "sv"} // Parameter defaults
    );
    

    Does this help?

    0 讨论(0)
  • 2020-12-08 06:24

    You can ask in the global.asax BeginRequest if the url is well formed for your site. You can try to do this with routes also, but in my experience, your routes will be very unstable if you are not sure that the first param is the lang.

    Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
        Dim lang As String = "es"
        If not Request.Path.ToLower.StartsWith("sv/") and _
           not Request.Path.ToLower.StartsWith("en/")
            ''//ask the browser for the preferred lang
            Select Case Mid(Request.UserLanguages(0).ToString(), 1, 2).ToLower
              Case "en"
                 Response.Redirect("en/")
              Case "sv"
                 Response.Redirect("sv/")
              Case Else
                 Response.Redirect("sv/") ''//the default
            End Select
        end if
     end sub
    

    Untested code. Pardon my VB

    0 讨论(0)
提交回复
热议问题