Localizing based on user setting

Topics: Customizing Orchard, General, Localization
Sep 22, 2014 at 3:59 PM
Edited Sep 22, 2014 at 5:04 PM
We need to localize our custom views based on the user that is currently logged in (we retrieve a setting in a custom part that is managed by administrators).

I was planning to retrieve this setting on each request (like with a custom action filter) and set something in the request's context (like the thread culture) but I don't understand how the orchard's localization mechanism works.

If possible, I would like to be able to use the T("text to translate") method (with associated ressource files) used everywhere in orchard, but I don't know if this is suited for my specific need. I have the impression that the culture can only be defined as a site-level parameter or as a content-level parameter, which is not appropriate in our case (each view must be translated in all available cultures, and the culture depends on the user currently connected).
Sep 22, 2014 at 6:42 PM
Edited Sep 22, 2014 at 7:28 PM
You could try this module
https://gallery.orchardproject.net/List/Modules/Orchard.Module.Datwendo.Localization

I implemented an user culture selector, but as orchard is not providing a user profile you will need to use/write a module providing user profile then write an extension based on the protocol I propose to bring back the current user culture.
User selector is for authenticated user mode, it relies on the fact that you are able to provide a ‘preferred culture’ for each user. As this is not implemented by default in Orchard User management, this will rely on an ‘interface IProfileProviderEventHandler’ you have to provide in your code, your implementation should bring back the preferred culture name for the current user (see the interface model).
This is working for me in an e-commerce site.
Sep 22, 2014 at 7:52 PM
I already had a look on your module, it seems promising, but the lack of documentation is a little bit frustrating. Or did I miss it?

I looked into your code and would like to understand better how it works. Basically, when a user requests a page, the HomePageFilter OnActionExecuting() method is called, and a redirection is issued when homePageService.ShouldBeTranslated() returns true (based on whatever logic you implemented, which I also don't understand yet). Does this mean that each translated view should be located under a different URL? How should I organize my views/routes/resources to match your implementation? How do you prioritize culture selection between user preferences / culture cookie / admin / etc.?

I am not really familiar with orchard patterns, except for the most basic ones (custom content parts, shapes, features, etc.), so understanding what the module requires simply by reading the code is a bit too much.
Sep 22, 2014 at 8:17 PM
Yes each translated content needs a different url. That's required by orchard because required by http.
This module does not require a different convention for URLs than default one used by orchard.localization,
Priorities are settings options as explained in doc. Just play with it and reports bugs in Bitbucket if any.
Sep 22, 2014 at 10:40 PM
O.K. I'm starting to get a grasp of the way this works. So let's say I have a custom view and my routes.cs currently exposes it as MyController/MyAction/. The view contains many contents, and all text is displayed using the T() method for localization. If I change the site culture, my view is globally translated (which is the behavior we currently have and want to change for a more dynamic one).

Now let's assume I download and enable your module, and want the following behavior when the user types http://www.mysite.com/MyController/MyAction/ in his browser:
1- If the user is not connected, he is redirected to the site's default culture U.R.L.: http://www.mysite.com/en/MyController/MyAction/
2- If the user is connected, he is redirected to his own culture, so if he is french: http://www.mysite.com/fr/MyController/MyAction/
3- I don't want to duplicate my code files (cshtml and actions) as they are quite complex and change a lot (with many developpers working on them), I only want the T() method calls to display their content based on the overriden culture

If I create a class implementing IProfileProviderEventHandler, which provides the user's culture, and set the selector's priorities correctly, then the HomePageFilter OnActionExecuting() method should redirect the user and validate my first and second requirements (the exact url syntax might be slightly different though, this is not important). Now how do I map en/MyController/MyAction/ and fr/MyController/MyAction/ to my action? Do I need to add new routes? Aliases? Should I add a single catch-all route like {culture}/MyController/MyAction/ mapped to my action?
Developer
Sep 22, 2014 at 11:53 PM
Edited Sep 23, 2014 at 12:11 AM
Some of what you have mentioned is in the branch feature/localizationchanges - https://orchard.codeplex.com/SourceControl/latest?branch=feature/localizationchanges you get the desired behaviour of {culture} for free.

So your routing file will have in it {culture}/MyController/MyAction/ The Selector in the Localization module will know this and set the culture to be that of what you have specified.

If however you do not specify a culture, i.e. {culture} does not exist in the url (or query string i.e. ?culture=en) then it will check for the current page culture. There are some guards/checks to see if you are attempting a POST in which case it will use the referrer url if present,

here is the file. https://orchard.codeplex.com/SourceControl/latest#src/Orchard.Web/Modules/Orchard.Localization/Selectors/DefaultFrontEndCultureSelector.cs

However the homepage redirection is not (yet but will be in the next day or two). The idea is that the feature overrides your homepage Alias, and redirects you to the specified homepage for the specific culture based on settings - what this allows you to do is have a default tenant that redirects to the tenant of choice for that culture, quite a cool little feature.

CSADNT mentions a user preferred language, and I like the idea, and was thinking about the implementation. Up till now the localization changes outside the admin have been based on navigation, this is important because in Europe you have messages that mention cookies, so you want to limit your exposure as much as possible. However with User preferences, the user has already made the decision to login, and hence have a form authentication cookie on their machine, therefore the cookie conversation is now mute at this point.

You could have a UserLocalizationPart - this has a property on it called "UserCulture". When the user logs in a interface is called IUserEventHandler, within the LoggingIn method you would check to see if that user has a UserCulture set, if yes, make sure you inject in ICultureStorageProvider and then just call SetCulture(culture). The User Culture Selector would then look for whatever is set and hey presto, User defined cultures. (This is just off the top of my head). The User Culture would be a Editor Shape, so exposed on the User record in the admin, and then also if you install the Profile module, you would also get to change it.

Let me know what you think

N
Sep 23, 2014 at 4:25 AM
Edited Sep 23, 2014 at 7:43 AM
madmox wrote:
O.K. I'm starting to get a grasp of the way this works. So let's say I have a custom view and my routes.cs currently exposes it as MyController/MyAction/. The view contains many contents, and all text is displayed using the T() method for localization. If I change the site culture, my view is globally translated (which is the behavior we currently have and want to change for a more dynamic one).
I prefer to speak in term of shapes here, so for every content that you display with your route you have a shape to display it. And a hierarchy of shapes to display the whole html content. Ok ?
-Normally, according standards, you should have different urls, so different routes for each language in orchard route table. You can respect this, building dedicated routes using a dedicated code to insert them in orchard tables, because this is not a standard feature of default orchard controllers. But you are not forced to it and could use only one global route displaying translated content according different criterias as user language, browser language, site language, existing content in orchard, etc. This way of behaving doesn't respect the standard but is easier (using T() leads to this)-
I add this distinction concerning the term view you used because sometimes, simple usage of T() does not do the trick, for example I have a site where I need to display english, spanish and 2 chinese variations, using T() with only one hierarchy of shapes was not ok because browsers and their css implementation have a global weakness concerning chinese punctuation, so you often have to write some specific html code for these languages, and this lead me to use several shape hierarchy, and to build this I have been using culture based alternates (shapes/templates). This is a very useful feature that my module introduces. You can adapt any shape in the displayed hierarchy with this concept. Simply enable the feature in orchard modules, then build variations of your templates using the culture differentiator in their name (see my doc for syntax respecting orchard rules in alternates naming)
It allows to adapt the way you display each static and dynamic content.


madmox wrote:
Now let's assume I download and enable your module, and want the following behavior when the user types http://www.mysite.com/MyController/MyAction/ in his browser:
1- If the user is not connected, he is redirected to the site's default culture U.R.L.: http://www.mysite.com/en/MyController/MyAction/
2- If the user is connected, he is redirected to his own culture, so if he is french: http://www.mysite.com/fr/MyController/MyAction/
3- I don't want to duplicate my code files (cshtml and actions) as they are quite complex and change a lot (with many developpers working on them), I only want the T() method calls to display their content based on the overriden culture
When the user is not connected you have not to always defaults to site culture, you can try to use the browser culture selector, it works even for anonymous users.
To do this in my module settings, you enable and give a higher priority to user selector then a lower to browser selector, then lower to cookie (if using it to allow user to fix something different from his browser language), than lower to site (fixed in orchard code). You even can define fallback rules. Fallback rules are triggered when there is no translation for the culture brang back by the best selector, and their role is to replace this culture by the best existing culture, basically it could be the default site culture, but you may also use regex to introduce a more sophisticated process for exemple latin languages -> es--us and anglo-saxon languages-> en-us.
For the usage of T() see my previous comment.



madmox wrote:
If I create a class implementing IProfileProviderEventHandler, which provides the user's culture, and set the selector's priorities correctly, then the HomePageFilter OnActionExecuting() method should redirect the user and validate my first and second requirements (the exact url syntax might be slightly different though, this is not important). Now how do I map en/MyController/MyAction/ and fr/MyController/MyAction/ to my action? Do I need to add new routes? Aliases? Should I add a single catch-all route like {culture}/MyController/MyAction/ mapped to my action?
The answer is in my first comment, in fact there are different routes to display each individual orchard content item, but when you are in a dedicated front-end controller and displaying an html content built from several orchard content items, you end up with a hierarchy of shapes, and here my first comment is the answer


Nick efforts concerning Localization are valuable and will certainly lead to something usable, they started from the point of using rtl languages in tinymce, but I am not sure they actually cover all important requirements of localization, they are the futur view of localization, but still in conception in a branch.
Sep 23, 2014 at 12:44 PM
@Jetski5822:
I don't get the part about the POST request for which you check the referrer URI instead of the actual resource's, but otherwise the idea seems nice. Maybe the content culture part sould be implemented as a separate selector (like in CSADNT's module), but I get the point.

My main issues with this pattern are that:
  • I need to redefine each route I want to make multi-lingual by adding the {culture}/ prefix ;
  • I can't use Controller.Url.Action() without adding the culture parameter in the routeValues dictionary. This is a big problem as it adds a lot of garbage overhead when including a link to another page (need to get the current culture and pass it to the helper method) ; I could define a new method like LocalizedAction() in the UrlHelper class which would pass the parameter implicitly though, and use it everywhere in the application ;
I like the way django deals with this problem by integrating a middleware class which analyzes the uri on input (among other criterias) to set the current culture, and only having to change your urlconf file (as well as registering the middleware) to enable internationalization (culture detection, but also reverse url generation!) is a real plus.

@CSADNT:
Reading your comment, I still don't get what you put in your Routes.cs file when you already have defined some custom urls and want to use your module. It is true that the templates switching / hierarchy mechanism can be powerful, but in our case I think it is not a viable solution, as it would imply a lot of html duplication and impossible maintenance, especially when you use a lot of MVVM javascript libraries in your views. We really need to use the same cshtml files for each pages and deport the localization logic in the resources files only. Or maybe did I miss something again?
Sep 23, 2014 at 1:46 PM
Edited Sep 23, 2014 at 1:46 PM
madmox wrote:
Reading your comment, I still don't get what you put in your Routes.cs file when you already have defined some custom urls and want to use your module. It is true that the templates switching / hierarchy mechanism can be powerful, but in our case I think it is not a viable solution, as it would imply a lot of html duplication and impossible maintenance, especially when you use a lot of MVVM javascript libraries in your views. We really need to use the same cshtml files for each pages and deport the localization logic in the resources files only. Or maybe did I miss something again?
Well, I must agree that on my side, I don't understand what you are displaying ? Seems heavy in orchard contentitems ?
If you have a controller, you have nothing to add in route table, provided you don't want to force a route or an alias ? I don't see your concerns.
Sep 23, 2014 at 3:53 PM
Edited Sep 23, 2014 at 4:02 PM
Let's try with a quick exemple.

My view (~/Modules/MyModule/Views/Test/myview.cshtml):
<!DOCTYPE html>
<html>
    <head></head>
    <body>
        <p>@T("Hello world!")</p>
    </body>
</html>
Sample of my controller (~/Modules/MyModule/Controllers/TestController.cs):
public ActionResult MyAction()
{
    return this.View("myview");
}
Sample of my routes file (~/Modules/MyModule/Routes.cs):
public IEnumerable<RouteDescriptor> GetRoutes()
{
    return new[] {
        new RouteDescriptor {
            Priority = 1000,
            Route = new Route(
                "Custom/Path/",
                new RouteValueDictionary {
                    {"area", "MyModule"},
                    {"controller", "Test"},
                    {"action", "MyAction"},
                },
                new RouteValueDictionary(),
                new RouteValueDictionary {{"area", "MyModule"}},
                new MvcRouteHandler()
            )
        }
    };
}
With this setup, if I enable your localization module, how should I change these 3 files? Should I add new RouteDescriptor instances? Is there anything to do at all (i.e. the controller will respond to culture-prefixed urls as-is)?
Sep 23, 2014 at 4:58 PM
Edited Sep 23, 2014 at 5:01 PM
First point this is a pure MVC sample, not a very 'orchardy way' of managing content, first because you don't use shapes.

But let's say that beyond this Orchard is using shapes, provided you don't change the normal Theme management files like document.cshtml, layout.cshtml, content.cshtml, etc.

For this sample there is no content attached, so there is few concerns related to providing a dynamic content corresponding to some language.
The filter trying to look for some content attached to this url will return null, and the culture will be set according the culture selector with higher priority.
For this kind of basic usage, you have nothing to change to your route. It's up to you to add a culture parameter in your controller and include it in the alias url at any place you want.

Have you something more in the orchard way, involving content item(s) ?
I think that I understand what you are not seeing: their are 2 kinds of urls, the one attached to a content (orchard is a CMS) and the others.
When you deal with an url attached to a content orchard Localization core module propose dedicated routes as
Myhost/mybaseurl/culture/contentId
It will work for every contentitem with the autoroute part attached to it. My module (as all the localization module I have seen respect this rule).
But you may change this rule and provide any path you want.

For the other request, without content, you do what you want, if you attached a culture parameter in your requested url, it could be interpreted by your controller and you do what you want.
The HomePage filter of my module looks, in fact, to every incoming request, before the corresponding controller being found and called, when it doesn't find any content attached, it does not try to change the sent url, so orchard work as if this filter was not here.
But when a content is attached to this url, it tries to find the correct url for this content and redirects to it.
The badly called Homepage filter is just a feature you don't need it but it is a nice feature to adjust url to culture and content, respecting http std.

When you don't use it you are back in a normal orchard with many culture selectors, working independently of your routes.

Is it more clear?