Showing posts with label language. Show all posts
Showing posts with label language. Show all posts

Sitecore Multilingual Item Alias

Sitecore by default provides Item Aliases for single language, refer my earlier post regarding Sitecore Aliases. One good request came to Sitecore SDN Forums for achieving aliases for multiple languages in Sitecore so thought to post about it.

What we want to achieve is, when a request come for an alias, the content of the Linked Item should come with selected language.

How to achieve?

  1. In the Alias template (/sitecore/templates/System/Alias), add a new field Linked Language with DropLink field.
    So, when a request comes like http://domain/idioma/, it should serve the Products page with es-ES language.


  2. Override the AliasResolver like below:
    namespace SitecoreTactics.HttpRequestPipeline
    {
        public class MultilingualAliasResolver : HttpRequestProcessor
        {
            public override void Process(HttpRequestArgs args)
            {
                Assert.ArgumentNotNull((object)args, "args");
                if (!Settings.AliasesActive)
                {
                    Tracer.Warning((object)"Aliases are not active.");
                }
                else
                {
                    Database database = Context.Database;
                    if (database == null)
                    {
                        Tracer.Warning((object)"There is no context database in AliasResover.");
                    }
                    else
                    {
                        Profiler.StartOperation("Resolve alias.");
                        if (database.Aliases.Exists(args.LocalPath) && !this.ProcessItem(args))
                            this.ProcessExternalUrl(args);
                        Profiler.EndOperation();
                    }
                }
            }
    
            private void ProcessExternalUrl(HttpRequestArgs args)
            {
                string targetUrl = Context.Database.Aliases.GetTargetUrl(args.LocalPath);
                if (targetUrl.Length <= 0)
                    return;
                this.ProcessExternalUrl(targetUrl);
            }
    
            private void ProcessExternalUrl(string path)
            {
                if (Context.Page.FilePath.Length > 0)
                    return;
                Context.Page.FilePath = path;
            }
    
            private bool ProcessItem(HttpRequestArgs args)
            {
                bool aliasFound = false;
                string alias = args.LocalPath;
                if (alias.Length > 0)
                {
                    Item obj = ItemManager.GetItem(FileUtil.MakePath("/sitecore/system/aliases", alias, '/'), Language.Invariant, 
                        Version.First, Sitecore.Context.Database, SecurityCheck.Disable);
                    if (obj != null)
                    {
                        LinkField itemField = (LinkField)obj.Fields["linked item"];
                        string language = obj["linked language"];
                        if (!string.IsNullOrEmpty(language) && itemField!= null)
                        {
                            Item langItem = Sitecore.Context.Database.GetItem(new ID(language));
                            if (langItem != null)
                            {
                                Language lang = Language.Parse(langItem.Name);
                                Item item = Sitecore.Context.Database.GetItem(itemField.TargetID, lang);
                                if (item != null)
                                {
                                    this.ProcessItem(args, item);
                                    aliasFound = true;
                                }
                            }
                        }
                        else if (itemField!=null)
                        {
                            Item item = Sitecore.Context.Database.GetItem(itemField.TargetID);
                            if (item != null)
                            {
                                this.ProcessItem(args, item);
                                aliasFound = true;
                            }
                        }
                    }
                }
    
                return aliasFound;
            }
    
            private void ProcessItem(HttpRequestArgs args, Item target)
            {
                if (Context.Item != null)
                    return;
                Context.Item = target;
                Context.Language = target.Language;
            }
        }
    }
    
  3. In Web.config, replace the Sitecore's AliasResolver and add our custom overridden MultilingualAliasResolver as below:
    <!--<processor type="Sitecore.Pipelines.HttpRequest.AliasResolver, Sitecore.Kernel" />-->
    <processor type="SitecoreTactics.HttpRequestPipeline.MultilingualAliasResolver, SitecoreTactics" />
    

Finally we are able to use multilingual aliases.

Note:Above code works only for Linked Items which are Sitecore Internal Links.

Browser Language Based Content Negotiation

Are you willing to change sitecore website content language based on user's browser preferences? We can achieve this using Content negotiation. Content negotiation is a mechanism to serve different versions of a page or document based on browser's preferences.

For example, a user wants to see information in German, if possible, else English will do. Browsers indicate their preferences by headers in the request. To request only German representations, the browser would send below headers:

Accept-Language: de

As an example of a more complex request, this browser has been configured to accept German and English, but prefer German. In such situation, browser would send below headers:

Accept-Language: de; q=1.0, en; q=0.5


Users can set these preferences in the browser itself. Below is given example of Firefox.

How Browser Based Language Content Negotiation will work

Sitecore provides LanguageResolver, which first checks that the Http Request contains language in Query String (using sc_lang parameter) or in File Path. If the language is found, it will use that language as a context language, otherwise will consider site's default language as the context language.

To achieve our requirement, if the language is not found in the Query String or File Path, we will add one more check to see Language in User Languages in Request Header (Or Accept-Language request header). We can get multiple languages in the request header. So, we will give first priority to the language which is available to serve from our site.

We can get the browser's preferred languages using below code in .NET.

string[] languages = HttpContext.Current.Request.UserLanguages;

Let's see how to achieve in Sitecore

Step 1: Override LanguageResolver

Do below code to override Sitecore's default LanguageResolver:
namespace SitecoreTactics.HttpRequestPipeline
{
    public class LanguageResolver : Sitecore.Pipelines.HttpRequest.LanguageResolver
    {
        public override void Process(HttpRequestArgs args)
        {
            Assert.ArgumentNotNull((object)args, "args");
            Language languageFromRequest = this.GetLanguageFromRequest(args.Context.Request);
            if (languageFromRequest == (Language)null)
                return;
            Context.Language = languageFromRequest;
        }

        private Language ExtractLanguageFromQueryString(HttpRequest request)
        {
            string name = request.QueryString["sc_lang"];
            if (string.IsNullOrEmpty(name))
                return null;
            Language result;
            if (!Language.TryParse(name, out result))
                return null;
            else
                return result;
        }

        private Language GetLanguageFromRequest(HttpRequest request)
        {
            Language languageFromQueryString = this.ExtractLanguageFromQueryString(request);
            if (languageFromQueryString != null)
                return languageFromQueryString;
            else if (Context.Data.FilePathLanguage != null)
                return Context.Data.FilePathLanguage;
            else
            {
                return GetLanguageFromBrowser(request);
            }
        }

        // Added function to allow browser based language content negotiation
        private Language GetLanguageFromBrowser(HttpRequest request)
        {
            Language result;
            if (HttpContext.Current.Request.UserLanguages!= null && HttpContext.Current.Request.UserLanguages.Length > 0)
            {
                string[] languages = HttpContext.Current.Request.UserLanguages;
                string chosenLanguage = string.Empty;

                foreach (string language in languages)
                {
                    string[] langParts = language.Split(';');
                    if (langParts.Length > 0)
                    {
                        if (Sitecore.Context.Database.GetItem("/sitecore/system/languages/" + langParts[0]) != null)
                        {
                            chosenLanguage = langParts[0];
                            break;
                        }
                    }
                }
                if (!Language.TryParse(chosenLanguage, out result))
                    return (Language)null;
                else
                    return result;
            }
            else
                return (Language)null;
            
        }
    }
}

Step 2: Configure overridden LanguageResolver in web.config

In Web.config, replace the Sitecore's LanguageResolver and add our custom overridden LanguageResolver as below:
<httpRequestBegin>
    <!--<processor type="Sitecore.Pipelines.HttpRequest.LanguageResolver, Sitecore.Kernel" />-->
    <processor type="SitecoreTactics.HttpRequestPipeline.LanguageResolver, SitecoreTactics" />
</httpRequestBegin>

There's more

Same way, we can use many other request parameters to negotiate user experience like.
  • User-Agent: User agent of the browser, which Sitecore uses in DeviceResolver 
  • Accept: Content types the browser prefers, which we can use to provide preferable content types of media or content.
  • Accept-DateTime: It is a header to retrieve content of any specific date-time.

We can use all these locale settings to enable them to provide information in the local format.
  •  What numeric formats does the user expect?
  • How should dates and times be formatted?
  • Should measurements be metric (centimeters, kilometers, liters) or imperial (inches, miles, gallons)?
  • What is the user's time zone?
  • Does the user use Letter size paper, or A4?
  • What shoe and clothing sizing systems should be used?
  • What is the user's physical location?

Render Sitecore Content Item with name of language

One issue reported in Sitecore SDN forum regarding rendering an content item under Home which has name of a language. I mean, there is an item /sitecore/content/Home/uk. Now, Sitecore consider /uk/ as an language, so it will render the Home item with uk (Ukranian) language (even if uk language is not added to Sitecore Languages). Its expected behavior is to render /Home/uk item in default language itself.

See what's the issue

We have an item named uk under Home.




Now, see what happens when we access http://patelyogesh.in/uk

Sitecore content item with language name

Why this happens

StripLanguage processor in preprocessRequest pipeline checks the FilePath and extracts expected language name from it. If the FilePath contains a valid language, it will assign it to Context.

So, in our URL: http://patelyogesh.in/uk, it will get FilePath as uk. So, assumes the uk item as uk language and set it to Context Language and Home is set to Context item.

How to solve this

One solution came in my mind to write a custom processor in httpRequestBegin, which will over-write Context Language and Context Item both, using it it is possible to solve. But, as John West suggested in forum, this might be possible using StripLanguage. So, thought to implement this and yes, it solves and is easy too. We can solve this issue overriding StripLanguage processor in preprocessRequest.
<!--<processor type="Sitecore.Pipelines.PreprocessRequest.StripLanguage, Sitecore.Kernel"/>-->
<processor type="SitecoreTactics.StripLanguage, SitecoreTactics">
  <ignoreLanguages hint=”list”>
    <language>uk</language>
    <language>en-gb</language>
    …
  </ignoreLanguages>
</processor>
To override the StripLanguage, we need to modify only one function named ExtractLanguage, which extracts the language. Now, as per our requirement, as need to ignore uk and en-gb languages. So, we will not consider language when it is uk or en-gb.

Below code should be implemented for this, but it does not contain code to read above ignoreLanguages setting. Here I have hard-coded these languages for better understanding:
public class StripLanguage : PreprocessRequestProcessor
{

    // Other methods
    ......

    private static Language ExtractLanguage(HttpRequest request)
    {
        Language language;
        Assert.ArgumentNotNull(request, "request");
        string str = WebUtil.ExtractLanguageName(request.FilePath);

        // Our code starts here
        // If the found language is uk, then set it to Empty, 
        // so sitecore will consider this as no language
        if (str == "uk" || str == "en-gb")
            str = string.Empty;
        // Our code ends here

        if (string.IsNullOrEmpty(str))
        {
            return null;
        }
        if (!Language.TryParse(str, out language))
        {
            return null;
        }
        return language;
    }

    ....
    // Other methods
}
Now, see below screen, referring /Home/uk page now refers to uk page itself with en language instead of Home page with uk language.



Finally, StripLanguage worked, we might need some extra code when implementing Multisite environment!!

Upload Sitecore media items in selected languages

One day, one idea came in my mind to create media items in specific languages only, which is very useful thought. By default, Sitecore media item gets created in many languages, where we need actually one or two out of them. Want to know why? Then read why sitecore media item gets created in multiple languages.

NOTE: This issues was recreated in Sitecore 7.2 and older versions. Sitecore 8 and later creates media items only in context language.

Specific languages for Unversioned Media Item

How it's benefitial?
In Unversioned media item, item gets created in all languages exist under /sitecore/system/languages/. Creating its version in selected languages is benefitial:
  1. Less insert queries to DB tables, less records fetched while fetching media item details.
  2. DataCache and ItemCache will be created in selected languages only instead of all languages.
  3. Reduced Database size, where millions of media are stored.
  4. So, overall performance improvement while uploading media item and while using/fetching media item.
How to achieve it?
  1. Create your custom class inherited from the Sitecore.Resources.Media.MediaCreator one, say SitecoreTactics.Resources.Media.MediaCreator. In this class you should override CreateItem method to retrieve there the set of the languages from the web.config setting. This set should be used as the languageArray in the foreach cycle. Thus the unversioned mediaitem with certain language versions will be created.

    See how it looks:
    namespace SitecoreTactics.Resources.Media
    {
     using System.Reflection;
     public class MediaCreator : Sitecore.Resources.Media.MediaCreator
     {
      protected override Item CreateItem(string itemPath, string filePath, MediaCreatorOptions options)
      {
       // Override the CreateItem function method and write custom code to create selected language versions
      }
      }
    }
    
    
  2. Create your custom class inherited from Sitecore.Resources.Media.MediaProvider one. In the constructor of this class you should assign an instance of our custom MediaCreator class (from Step#1) to the MediaProvider "creator" private field using the Reflection. So, the MediaProvider will use our own code of MediaCreator.

    How the code looks:
     namespace SitecoreTactics.Resources.Media
     {
      using System.Reflection;
      public class MediaProvider : Sitecore.Resources.Media.MediaProvider
      {
       public MediaProvider() : base()
       {
        typeof(Sitecore.Resources.Media.MediaProvider).GetField("creator", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(this, new SitecoreTactics.Resources.Media.MediaCreator());
       }
      }
     }
    
    
  3. Change in config, register our custom mediaprovider class as below:
    <mediaLibrary>
        ...
        <mediaProvider type="SitecoreTactics.Resources.Media.CustomMediaProvider,SitecoreTactics.Resources" />
        ...
    </mediaLibrary>
    
    

Wow, it worked well!

Specific languages for Versioned Media Item

Versioned Media Item does not need an automated way to upload media in selected languages. The reason is, this kind of media does not share Media File. So, it is used only when we need to upload different files for different languages of same media item. If still someone need to achieve it, here is the way:
  1. Add a settings in the web.config file containing set of the pipe separated needed languages. For example:
    <setting name="SetOfLanguages" value="en|ru-RU|en-GB" />
    
  2. Add an extra checkbox "Versionable For Certain Languages" on the Media Folder Upload Form using this file: \Website\sitecore\shell\Applications\Media\MediaFolder\MediaFolder.xml
  3. Create a custom class inherited from Sitecore.Pipelines.Upload.UploadArgs and add here an extra "SpecialVersioned" property. This property will indicate to Sitecore.Pipelines.Upload.Save processor to execute the custom actions.
  4. Create a custom class based on Sitecore.Shell.Applications.FlashUpload.Advanced.UploadTarget one and define it in \Website\sitecore\shell\Applications\FlashUpload\Advanced\UploadTarget.aspx file instead of the default one.
  5. In this class you should assign a value to your custom UploadArgs.SpecialVersioned property similar to the UploadArgs.Versioned one.
  6. Create your custom class based on the Sitecore.Pipelines.Upload.Save one and register it in the web.config <uiUpload> pipeline instead of the default one.
  7. An example of its rewritten Process method is here: Expand Code
It's working great, isn't it?
And yes, many thanks to Yuriy Zakharov from Sitecore Support to provide such a nice solution!!

Why my Sitecore Media Item created in multiple languages?

Have you ever faced that you upload a single media file to Sitecore and it gets created in multiple languages? This is not strange but expected behavior in Sitecore. Let's see how.

Check Media.UploadAsVersionableByDefault setting in your web.config. If it is false, then uploaded media item will be created in all those languages which exist under /sitecore/system/languages. This media item will use unversioned template /sitecore/templates/System/Media/Unversioned/File template, so the Blob(Media) and its common details would be shared across all language versions. See below image.



This option is really helpful when you want to use common media files across multiple languages content pages as the media is shaerd across all languages.


Where, if you have this setting as true, then each media item will be generated in default language only. This media item will use versioned template /sitecore/templates/System/Media/Versioned/File template, so the Blob(Media) and its all details would be distinct across languages. See below image.



This option is really helpful when you want to use different media files for different language content pages.

We can choose whether we need versioned or unversioned media. While doing Upload File (Advanced), Sitecore asks to select Make Uploaded Media Items Versionable checkbox. If the checkbox is selected, it will create versioned media, otherwise will create unversioned media items. See below image:



You want to upload media items in Sitecore with your selected languages only? Refer my new post: Upload sitecore media items in selected languages.