mccalltd / AttributeRouting

Define your routes using attributes on actions in ASP.NET MVC and Web API.
http://mccalltd.github.io/AttributeRouting/
MIT License
416 stars 89 forks source link

Localization Needed #20

Closed mdmoura closed 12 years ago

mdmoura commented 12 years ago

Hello Tim,

I am using Attribute Routing on a CMS I develop and I have a few questions.

1 - CONVENTIONS

  I am planning to use the RestfulRouteConvention but I have a few questions:      

  A) Do you consider Post / Index as Post / List?
       Or it can be just the "entry" page?

  B) Instead of:

       Edit — GET ControllerName/{id}/Edit     (Post/34/Edit)

       Wouldn't be better to have:

       Edit — GET ControllerName Pluralized/{id}/Edit     (Posts/34/Edit)

       Reference: http://edgeguides.rubyonrails.org/routing.html

       You pluralize the controller or add an attribute to the controller? 

       I suppose both can be used, correct?

 C) I see that all forms must have the HttpMethodOverride.

       I need to a "Delete" link in each row of a table. How would you do it?

       Maybe using Ajax? But can I do the HttpVerbs.Delete?

        $('table.Data a.Delete').click(function (e) {
          e.preventDefault();
          if (confirm("Delete this item?")) {
            $.ajax({ type: 'POST', url: $(this).href });
          };
        });

2 - SLUG IN URLs

  I would like to have friendly slugs on the URLs. A few options:

  A) posts/124/learn-about-attribute-routing         (SLUG = TITLE)

       In this case when the TITLE / SLUG changes I will not have broken links.

       This is because the post is retrieved using the ID.

       The only problem is if the title is changed I get 2 URLs indexed to the same page:

         OLD: posts/124/learn-about-attribute-routing

         NEW: posts/124/learn-about-mvc

       I think this is penalized by Search Engines. In fact, aren't you penalized when using:

        [GET("", Order = 1)]
        [GET("Posts", Order = 2)]
        [GET("Posts/Index", Order = 3)]
        public void Index()
        {
           return View();
        }

        You have multiple routes to the same page.

  B) posts/124-learn-about-attribute-routing        (SLUG = ID + TITLE)

       In this case when the TITLE / SLUG changes I will have broken links.

       That is a problem worse then the previous one.

  On both cases the Slug is saved on the Database when creating the post ...

  ... probably it is better then creating it on the fly. What do you think?

  Which approach would you use?

  And is this compatible with Restfull convention?

3 - LOCALIZATION

  We already talked about localization before. In fact created a branch when we talked.

  After reading and researching a lot I think there is a better approach.

  (A) Use different Country Top Level domains for each version.

       ENGLISH: www.domain.com;     PORTUGAL: www.domain.pt;    FRANCE: www.domain.fr

      "At the SMX Confernece in Sydney Australia, Priyank Garg and Greg Grothaus, Yahoo’s and 
       Google’s search engineers, shared some issue:

       If you use multiple country domains (ccTLD), even if content is in the same language (identical content) 
       on all of your localized sites (for instance you have it all in English, as it commonly happens with USA, 
       UK and Australia), you will not experience any duplicated content issues / penalties. This is true for both 
       Google and Yahoo, however, you might get some penalty if you abuse this feature (spammy sites) and 
       if you don’t localize your site properly."

  (B) If Top Level domains are not available then use sub domains.

        ENGLISH: en.domain.com;     PORTUGAL: pt.domain.com;    FRANCE: fr.domain.com

   HOW IT WORKS

   When a page is requested the MVC application detects the domain of the request and sets the culture.

   ADVANTAGES     

   The portuguese version is indexed by Google.pt, the french version by Google.fr, etc.

    I did a search for MSN in Google.com, Google.pt and Google.fr and in fact I got:

    www.msn.com, pt.msn.com and fr.msn.com.

    ISSUES

    A route translation would be necessary to do the following:

    - Go from "www.domain.pt/contacto" to "www.domain.com/contacto"

    - Go from "www.domain.pt/post/ola-mundo" to "www.domain.com/post/hello-world"          

    The problem is that for the last route the title can change ...

    And in fact a post can have no translation so it would be redirected to home/index or other defined route.

    I am not sure if the best option would be to have a table to hold routes.

    What do you think?

Cheers, Miguel

mccalltd commented 12 years ago

Hi Miguel,

For #1 - CONVENTIONS: I'd recommend creating your own RouteConvention, or just not using conventions at all if possible. If you look at the code for the RestfulRouteConvention, you can see how easy it would be to add your own flavor.

Also, you don't HAVE to use PUT or DELETE. But you can if you want to. If you look at the code in /Extensions/MvcExtensions.GetHttpMethod(), you'll see how the verb is detected.

For #2 - SLUGS: You could either create a permalink that does not change, or inside your actions issue a 301to the canonical url. If Google is crawling, and hits /post/123/old-slug and gets a 301 to /post/123/new-slug, then maybe things will work out. Not sure as SEO is not my forte.

For #3 - LOCALIZATION: I think ultimately you'll need to have the route table know when to accept and generate the correct, culture-specific routes. This would have to be a new feature of AR, and I'm not sure the best way to implement it. One idea would be to expose a LocalizationProvider which would let you store your localized routes wherever (db, file, in-memory) and simply write your own provider implementation to supply AR with the correct route information (like url, defaults, constraints). To tell the truth I have not thought of localization much because I haven't had a need for it. But I do see that if there was support for this that AR would be truly awesome.

Regarding #3, I will start thinking of ways forward.

Thanks for your feedback and please feel free to fork the code and hack in your own solution. If you do so, let me know what happens! We could bring it into the project and you could be the first contributor.

t

On Nov 9, 2011, at 4:04 PM, shapper wrote:

Hello Tim,

I am using Attribute Routing on a CMS I develop and I have a few questions.

1 - CONVENTIONS

 I am planning to use the RestfulRouteConvention but I have a few questions:      

 A) Do you consider Post / Index as Post / List?
      Or it can be just the "entry" page?

 B) Instead of:

      Edit — GET ControllerName/{id}/Edit     (Post/34/Edit)

      Wouldn't be better to have:

      Edit — GET ControllerName Pluralized/{id}/Edit     (Posts/34/Edit)

      Reference: http://edgeguides.rubyonrails.org/routing.html

      You pluralize the controller or add an attribute to the controller? 

      I suppose both can be used, correct?

C) I see that all forms must have the HttpMethodOverride.

      I need to a "Delete" link in each row of a table. How would you do it?

      Maybe using Ajax? But can I do the HttpVerbs.Delete?

       $('table.Data a.Delete').click(function (e) {
         e.preventDefault();
         if (confirm("Delete this item?")) {
           $.ajax({ type: 'POST', url: $(this).href });
         };
       });

2 - SLUG IN URLs

 I would like to have friendly slugs on the URLs. A few options:

 A) posts/124/learn-about-attribute-routing         (SLUG = TITLE)

      In this case when the TITLE / SLUG changes I will not have broken links.

      This is because the post is retrieved using the ID.

      The only problem is if the title is changed I get 2 URLs indexed to the same page:

        OLD: posts/124/learn-about-attribute-routing

        NEW: posts/124/learn-about-mvc

      I think this is penalized by Search Engines. In fact, aren't you penalized when using:

       [GET("", Order = 1)]
       [GET("Posts", Order = 2)]
       [GET("Posts/Index", Order = 3)]
       public void Index()
       {
          return View();
       }

       You have multiple routes to the same page.

 B) posts/124-learn-about-attribute-routing        (SLUG = ID + TITLE)

      In this case when the TITLE / SLUG changes I will have broken links.

      That is a problem worse then the previous one.

 On both cases the Slug is saved on the Database when creating the post ...

 ... probably it is better then creating it on the fly. What do you think?

 Which approach would you use?

 And is this compatible with Restfull convention?

3 - LOCALIZATION

 We already talked about localization before. In fact created a branch when we talked.

 After reading and researching a lot I think there is a better approach.

 (A) Use different Country Top Level domains for each version.

      ENGLISH: www.domain.com;     PORTUGAL: www.domain.pt;    FRANCE: www.domain.fr

     "At the SMX Confernece in Sydney Australia, Priyank Garg and Greg Grothaus, Yahoo’s and 
      Google’s search engineers, shared some issue:

      If you use multiple country domains (ccTLD), even if content is in the same language (identical content) 
      on all of your localized sites (for instance you have it all in English, as it commonly happens with USA, 
      UK and Australia), you will not experience any duplicated content issues / penalties. This is true for both 
      Google and Yahoo, however, you might get some penalty if you abuse this feature (spammy sites) and 
      if you don’t localize your site properly."

 (B) If Top Level domains are not available then use sub domains.

       ENGLISH: en.domain.com;     PORTUGAL: pt.domain.com;    FRANCE: fr.domain.com

  HOW IT WORKS

  When a page is requested the MVC application detects the domain of the request and sets the culture.

  ADVANTAGES     

  The portuguese version is indexed by Google.pt, the french version by Google.fr, etc.

   I did a search for MSN in Google.com, Google.pt and Google.fr and in fact I got:

   www.msn.com, pt.msn.com and fr.msn.com.

   ISSUES

   A route translation would be necessary to do the following:

   - Go from "www.domain.pt/contacto" to "www.domain.com/contacto"

   - Go from "www.domain.pt/post/ola-mundo" to "www.domain.com/post/hello-world"          

   The problem is that for the last route the title can change ...

   And in fact a post can have no translation so it would be redirected to home/index or other defined route.

   I am not sure if the best option would be to have a table to hold routes.

   What do you think?

Cheers, Miguel


Reply to this email directly or view it on GitHub: https://github.com/mccalltd/AttributeRouting/issues/20

mdmoura commented 12 years ago

Hello,

For #1: I don't know why (maybe because of Wiki) but I though you were advising the use of conventions.

     My idea is to use on the CMS something similar to what you have on the Wiki:
     https://github.com/mccalltd/AttributeRouting/wiki/1.-Getting-Started

     The only differences are:

     A) I am not sure if I will use PUT and DELETE Verbs or if I will replace them by POST.

     B) I will have another action named:

         [GET("Posts/{id}/flag/Get")]
         public ActionResult Get(int id, string flag) { }

         I have all files (binary data) saved on a single table (using filestream).
         Each file is identified with an ID and a Flag.

         Sometimes I request the file directly, through the FileController, as follows:

         [GET("Files/{id}/flag/Get")]
         public ActionResult Get(int id, string flag) { }

         But in posts controller I can set some authorization levels and other options associated to that post.

         Is there a more common way to name this actions?

For #2: Yes, the 301 is a good idea. I will allow the user do change the Slug but advice him/her not to do it.

For #3: In my opinion saving the URLs in a database might be a problem. It would become really strict and a problem for changes in the routing.

      I think it should be more flexible. Better to use and less headaches for AttributeRouting. 

      1 - Detect the origin of the request and set culture.

           A) Use different domains:

               Contains "domain.com" > en-US
               Contains "domain.pt" > pt-PT                   

           B) Use different sub domains:

                "en.domain.com" > en-US
                "pt.domain.com" > pt-PT

           C) Finally, if people don't like this approach then use:

               "domain.com/en" > en-US
               "domain.com/pt" > pt-PT               

           Option (C), as I explained, does not seem to be recommended.

           And option (A) and (B) raise a problem: how to test the site during development.
           Maybe setting option (C) in DEBUG and (A) or (B) in RELEASE?

       2 - Routes

            (A) Direct translation

                 - Area:  "Store" in EN > "Loja" in PT
                 - Controller:  "Products" in EN > "Produtos" in PT
                 - Action: "New" in EN > "Novo" in PT

            (B) Parameters

                 "Store/Produts/2/Learn-Mvc-Book"

                 The part "Store/Produts/" is taken by (A) and becomes "Loja/Produtos/".

                 And the rest of the URL is taken care by the application itself.

                 @Html.ActionLink(model.ItemName, MVC.Store.Index(model.Item.Id, model.Item.NameSlug))

                 Model alreay have the values translated by the application.

            (C) Switching 

                 A "complex" culture switching from EN to PT:

                 "www.domain.com/Store/Produts/2/Learn-Mvc-Book/Edit"

                And the rules to translate would be become:

                 - "%domain.com%" becomes "%domain.pt%"

                 - "Store" > "Loja", "Products" > "Produtos", "Edit" > "Editar"

                 - Use ProductTranslation(id, slug, culture)   **********

                   In ProductTranslation:

                   > Access service layer: Find Product by ID:

                      > If product is found use ID and Localized SLUG.
                         If it is not found then redirect to route "www.domain.pt/message/5/product-unavailable-in-portugal"

      3 - Implementation    

         IMHO I think the best way would be a flexible solution just like "Plug & Play". :-)

         For example Plug one or more Translation Providers (maybe with Fluent ...)? Example:

         public class TranslationProvider : ITranslationProvider {

             public Initialize(String culture) {

               Culture.Default("en-US").Allow("pt-PT").Allow("fr-FR")

               Maybe some Regex would be interesting to use on the following.
               This is because of domain.com:80, etc

               URL.Domain("domain.com").For("en-US").For(en-GB).Release();
               // OR
               URL.Domain("domain.com").For("en").Release();                   
               // OR
               URL.Domain("en.domain.com").For("en").Release();

               URL.Domain("domain.com/{culture}").Width(culture).Debug()

               Area("Store").For("en").Is("Loja").For("pt").Is("Boutique").For("fr")

               Controller("Products").For("en").Is("Produtos").For("pt") ...

               Action("...

               Route("Store/Products/{id}/{slug}/Action").Use(x => ProductRouteTranslation(x.Id, x.Slug))

               // The ProductRouteTranslation would have the code in **********.

             } 
         }        

         routes.MapAttributeRoutes();
         routes.Translation.AddProvide(new TranslationProvider())

         This is an example. Maybe can be better adapted for AttributeRouting.

         The code could be divided in more than one translation provider:
         AreaTranslationProvider, ControllerTranslationProvider, etc.

         And RouteTranslation : IRouteTranslation could be used to create very specific translations.    

        Well, this is an idea. I tried to make it flexible.
        And remove most of the translation decision from AttributeRouting.

        What do you think?

Cheers, Miguel

mdmoura commented 12 years ago

Just leaving a useful post for localized sites:

http://guides.rubyonrails.org/i18n.html

Check items 2.3, 2.4 and 2.5. Basically this is what I am talking about.

mccalltd commented 12 years ago

Thanks. I'll check this out!

On Nov 21, 2011, at 4:56 AM, shapper reply@reply.github.com wrote:

Just leaving a useful post for localized sites:

http://guides.rubyonrails.org/i18n.html

Check items 2.3, 2.4 and 2.5. Basically this is what I am talking about.


Reply to this email directly or view it on GitHub: https://github.com/mccalltd/AttributeRouting/issues/20#issuecomment-2814321

mdmoura commented 12 years ago

Hello,

After using AttributeRouting for the past 2 months I think I can give more feedback on localization.

I am not sure if this is possible but this is what seems logic to me:

  routes.MapAttributeRoutes(x => {        
    x.SetTranslationProvider(new TranslationProvider());
    x.SetCulturePrefix(CulturePrefix.Culture);
    x.SetCultures("en-US", new { "pt-PT", "fr-FR" });
  });

Notes: 1) SetTranslationProvider would define the class responsible for translation. When creating routes AttributeRouting would call this class.

2) SetCulturePrefix would define the format of the culture prefix:Culture or Language. So when adding a CulturePrefix attribute on a controller the routes would become:

"/en-US/area/controller/action"  or  "/en/area/controller/action"

3) SetCultures would define the default culture (used in routes) and the other ones.

The TranslationProvider could be something like:

public class TranslationProvider {

public void Resolve() {

  .For(Route.Area, x => {
      x.Add("Admin", { "AdminPT", "AdminFR" })
   });

} }

When AttributeRouting is defining the routes it would:

1) Check if CulturePrefix is defined ... If yes then: Add current culture on start of route if CulturePrefix is used.

2) Call translation provider. For example, and area named "Admin" would become "AdminPT" if current culture is "pt-PT".

Basically, before routes are build the culture should be checked and use this configuration to define the route.

Does this make sense?

Thank You, Miguel

mccalltd commented 12 years ago

Miguel, I like your fluent translation provider API as the default, and allowing this to be swapped with something else. Something like:

FluentTranslationProvider translations;

translations
    .ByKey("HomePage_RouteUrl")
        .Add("en-ZZ", "nook")
        .Add("en-AA", "cave")
    .ByKey(...)
        .Add(...)
        .Add(...);

// or

translations
    .ByCulture("en-ZZ")
        .Add("HomePage_RouteUrl", "nook")
        .Add("AboutPage_RouteUrl", "sleeping")
    ....; 

// which you then pass into

routes.MapAttributeRoutes(config => {
    config.TranslationProvider = translations
})

Anyhow - yes, all that you wrote makes sense. Below are some notes I wrote after first reading your comments.


What would be nice to have, and what would AR do?

  1. Support specifying the locale to use during requests via the url with some pattern to match (eg: {culture}.domain.com or domain/{culture}), or with a custom object to provide the logic, or by using the culture of the current thread? Specify via the configuration process or in another attribute somehow?
  2. ITranslationProvider that will translate route areas, prefixes, urls, defaults, and constraints? This provider could have a single method that when given a key, would return a string with the translation. Returning null would tell AR to use whatever is specified via the attributes in code. So the attribute would describe the default culture translation.
  3. Use a conventional approach to specify keys provided to the translation provider? Add a new property to the attributes called TranslationKey, which could simply override the default convention? Convention and configuration. Any need to be able to specify your own convention?
  4. Now, how to make AR use the translation provider to handle inbound requests? Do we need another route for every possible translation? So if you have 100 routes in your app, then having three translations would yield 300 routes? Anyone see this becoming a problem? It would be the easiest to do, and require the least processing after setting up routes.
  5. Next, AR and outbound url generation. Have to use the translation provider to generate the correct url for the current culture. So when browsing a default-English site in Spanish, using the MVC UrlHelper will give the visitor properly translated urls. Would have to add a property to AttributeRoute that stores the translations created during route generation. Then AttributeRoute could place the translation over the default generated route. (Imaging a route with components like
/{areaUrl}/{prefix}/{routeUrl}/{param}

having a translated route of

/easay/apescay/oatgay/{param}

having a default generated route of

/sea/scape/goat/milk

and ending up with

/easay/apescay/oatgay/milk

with milk left alone cause it's a route param, but could have been generated from translation for route default, and would apply translated route constraints as well.)

What do you think?

mccalltd commented 12 years ago

Miguel -- I've added a bunch of stuff to the branch 20-localization. I think localization should be functional now. Maybe not complete, but workable. Please do the following:

$ git clone git@github.com:mccalltd/AttributeRouting.git
$ git checkout origin/20-localization

Open the web app in the solution and look in global.asax for configuration of translations. If you change the default language of your browser to es or fr, you will have a generated link on the Localization page pointing to the translated route. Currently:

var provider = new FluentTranslationProvider();
provider.AddTranslations().ForController<HomeController>()
    .AreaUrl(new Dictionary<string, string> {...})
    .RoutePrefix(new Dictionary<string, string> {...})
    .RouteUrl(c => c.Action(), new Dictionary<string, string> {...})
    ....;
var provider = new FluentTranslationProvider();
provider.AddTranslations()
    .ForKey("Key", new Dictionary<string, string> {
        { "es", "espanol" }
    })....;

Please give me feedback ASAP. If you think this is good enough to use now, and doesn't leave you missing anything important, I want to merge it into the master branch and push it out to nuget.

Thanks for your feedback and suggestions!

mccalltd commented 12 years ago

Just added wiki 6. - Localization. Going to push in the next few days.

mccalltd commented 12 years ago

Now available via nuget v1.5

leniel commented 12 years ago

@mccalltd I just have to say a big THANK YOU for this powerful routing lib. I'm trying it in my bilingual project (pt-BR and en-US) and it's working as expected. I think I'll write a blog post to showcase the new Localization feature that's AWESOME! :D

I tried this just to make sure it worked as I wanted:

[GET("Realty/Details/{id}", TranslationKey = "Realty/Details/{id}")]
public ViewResult Details(int id)
{
  ...
}

Then in AttributteRouting AppStart:

var translations = new FluentTranslationProvider();

translations.AddTranslations()
    .ForKey("Realty/Details/{id}", new Dictionary<string, string>
{
        { "pt", "Imovel/Detalhes/{id}" }
});

routes.MapAttributeRoutes(config =>
{
    config.AddRoutesFromController<RealtyController>();
    config.AddTranslationProvider(translations);
    config.UseLowercaseRoutes = true;
    config.AutoGenerateRouteNames = true;
});

It's working flawlessly.

Is this the best way of configuring the translations?

mccalltd commented 12 years ago

Hey thanks! I'd lean toward using the ForController method rather than the ForKey method, just cause then I have a bit of freedom for refactoring. I also have some changes in master that will affect localization, FYI. Added support for constraining inbound route handling by culture. Will push those changes to nuget soon. Just gotten sidetracked with other things.

leniel commented 12 years ago

@mccalltd Is it possible to translate QueryString parameters with AttributeRouting? For example: http://leniel-pc:8083/imoveis?page=2 would become http://leniel-pc:8083/imoveis?pagina=2 in pt-BR...

In the case above I already translate the route from http://leniel-pc:8083/realties?page=2 to http://leniel-pc:8083/imoveis?page=2.

mdmoura commented 12 years ago

At the moment I don't think it is possible ... And I am not sure how that would be done ...

For now I am using p for page, q for query, etc as an alternative

Tim, when do you expect to push the "constraining inbound route handling by culture" changes?

Cheers, Miguel