Saturday, March 8, 2014

Multilingual site on Orchard CMS with single codebase and content database depending on domain name

When we need to create multilingual site on Orchard we have several options:

  1. Create site on first basic language, then copy content database and translate all content. In this case we will have 2 separate databases and will need to maintain them separately, although codebase can be the same (however you will still need to create separate sites in IIS in order to specify different connection strings in \App_Data\Sites\Default\Settings.txt). Also you will need new content database for each new language;
  2. After creating the site on basic language go to Admin panel > Settings and add necessary languages to the list of available languages (via “Add or remove supported cultures for the site” link). After that in Modules install and enable Localization module which allows to create languages variations for each content item, like pages. There will be “New translation” under each page:

image

Using other module called “Culture layer” you may create new layers in Widgets for separate languages. E.g. this is how you may create layer for front page for Russian and English languages:

Name Rule
TheHomepage ru-RU url("~/") and lang("ru-RU")
TheHomepage en-US url("~/") and lang("en-US")

(lang rule is added with “Culture layer” module). After that you may add different widgets and define localized content in different layers.

Now when you will change language in Settings > Default site culture, you will see localized content. But in this case it still will be needed to have different site in IIS and different copies of content databases for each site which will have only one difference in Default site culture, which isn’t good. The better way would be to make it so that site language will be selected automatically based on the domain name of the site? I.e. when we visit site by http://example.ru it will use Russian, but when by http://example.com – English, while both host headers will point to the single IIS site. The answer is yes, it is possible with Orchard.

In order to do it we will need to create new Module for our site. Into Content folder our module we will put text file with list of domains of 1st level and appropriate locale names:

ru:ru-RU;com:en-US

Then we create new class which inherits ICultureSelector interface from Orchard framework. It will parse text file with locales and depending on current host name will choose the correct one:

   1: public class CultureSelector : ICultureSelector
   2: {
   3:     private const string DEFAULT_CULTURE = "ru-RU";
   4:     private const int CULTURE_PRIORITY = 1000;
   5:  
   6:     public CultureSelectorResult GetCulture(HttpContextBase context)
   7:     {
   8:         var defaultCulture = new CultureSelectorResult {Priority = CULTURE_PRIORITY,
   9: CultureName = DEFAULT_CULTURE};
  10:         try
  11:         {
  12:             var locales = this.getLocales();
  13:             if (!locales.Any())
  14:             {
  15:                 return defaultCulture;
  16:             }
  17:  
  18:             string host = HttpContext.Current.Request.Url.Host;
  19:             int idx = host.LastIndexOf(".");
  20:             if (idx < 0)
  21:             {
  22:                 return defaultCulture;
  23:             }
  24:             var currentDomain = host.Substring(idx + 1).ToLower();
  25:             var currentLocale = locales.FirstOrDefault(l =>
  26: l.Domain == currentDomain);
  27:             if (currentLocale == null)
  28:             {
  29:                 return defaultCulture;
  30:             }
  31:             return new CultureSelectorResult {Priority = CULTURE_PRIORITY,
  32: CultureName = currentLocale.Locale};
  33:         }
  34:         catch
  35:         {
  36:             return defaultCulture;
  37:         }
  38:     }
  39:  
  40:     private List<dynamic> getLocales()
  41:     {
  42:         string path = HttpContext.Current.Server.
  43: MapPath("~/Modules/Example.Localization/Content/locales.txt");
  44:         string locales = File.ReadAllText(path);
  45:         if (string.IsNullOrEmpty(locales))
  46:         {
  47:             return new List<dynamic>();
  48:         }
  49:  
  50:         string[] pairs = locales.Split(new[] { ';' },
  51: StringSplitOptions.RemoveEmptyEntries);
  52:         if (!pairs.Any())
  53:         {
  54:             return new List<dynamic>();
  55:         }
  56:  
  57:         var result = new List<dynamic>();
  58:         foreach (string pair in pairs)
  59:         {
  60:             int idx = pair.IndexOf(":");
  61:             if (idx < 0)
  62:             {
  63:                 continue;
  64:             }
  65:  
  66:             result.Add(new {Domain = pair.Substring(0, idx),
  67: Locale = pair.Substring(idx + 1)});
  68:         }
  69:         return result;
  70:     }
  71: }

Now if we will open our site in ru domain, we will see Russian content, while in com – English.

9 comments:

  1. Hello, I have used your example but for a single domain. I am adding a drop down box to set a session variable which I then use inside CultureSelectorResult GetCulture to set my desired culture.

    This is working great when the user is logged in but if is anonymous user, the GetCulture method does NOT get called (I set a breakpoint to confirm)

    Do you have any idea why this is happening and how I can work around it ?

    Thanks.

    ReplyDelete
  2. José,
    it may be so that it uses default culture selector in this case. If you get instance of ICultureSelector from IoC (you may check it by setting breakpoint to default ICultureSelector implementation which you may find in Orchard sources as far as I remember), try to create instance of your class explicitly instead. It is not elegant solution, but may work. Another way is to try to unregister default ICultureSelector from IoC.

    ReplyDelete
  3. Hello Alexey, thanks for your reply and hints.

    I've been programming mostly in traditional ASP.NET web pages and my experience with MVC and concepts like IoC is limited.

    I am using the latest version of Orchard 1.8 and based on your feedback I think these are the sources you refer to:

    All in \src\Orchard\Localization\Services:

    ICultureSelector.cs (Interface definition)
    SiteCultureSelector.cs (Default implementation ?)
    DefaultCultureManager.cs (Default implementation ?)

    I have set a breakpoint in both SiteCultureSelector.cs & DefaultCultureManager.cs and the breakpoint IS hit when I am logged on but NOT if I log off. My idea was to change SiteCultureSelector.cs to set my culture from a session variable or cookie. However since the GetCulture() does not get called for anonymous this will not work.

    You mentioned 2 possibilities: Creating an instance explicitly and Unregister default ICultureSelector from IoC - can you give me an exemple on how I could do this ?

    ReplyDelete
  4. I used Orchard 1.7.2 and didn't face with this behavior to be true. It was enough to add custom implementation of ICultureSelector interface into the module and site started to use my culture selector both for authenticated and anonymous users. It may be so that something was changed in the new version. I recommend to ask this question on stack overflow. Orchard community is quite good and maybe someone faced with this problem already. As another workaround you may try your code on v.1.7.2 which worked for me. In this case you will be know for sure is it caused by changes in engine or something else.

    ReplyDelete
  5. I'm going to install 1.7.2 and use your example above - except on a single site only. I will use Culture Layer module to filter the widget translations and add a module to set the culture hardcoded.

    If I run into problems are you interested in working to help me on this ? Is yes please let me know your conditions and your email contact. Thanks.

    ReplyDelete
  6. José,
    yes try to use v.1.7.2. I'm extra busy at the moment, so unfortunately can't work over this problem regardless of any conditions. What I can do is to answer questions from time to time here in the blog or by email, but can't guarantee that it will be fast.

    ReplyDelete
  7. Alexey, thanks very much for your reply and for your hints. I am going to advance with 1.7.2 first with one single domain. Will also try two domains, one for each culture, as in your example above.

    I realize you are extra busy and appreciate your help. If possible please send me your email address.

    ReplyDelete
  8. the same as my blog name at gmail.com

    ReplyDelete
  9. Hi Alexey. I believe I found a solution. It’s in the Localization module razor script named \Orchard.Web\Modules\Orchard.Localization\Views\Admin\Translate.cshtml.

    There is a jquery function with a regex function which formats the permalink. I changed it to format according to “culture/slug” and it works fine !

    Maybe it’s not the best but it works. Creating an Autoroute pattern token would work but requires more knowledge than I have right now.

    ReplyDelete