MUI Refinements panel in SharePoint 2013

Just as it was the case in SharePoint 2010, in SharePoint 2013 the refinement panel only shows in the default language, even if the translations were added in the term store manager.

There was a solution for SharePoint 2010 (Thanks to Timmy Gilissen) but I could not find a lot of resources for a 2013 solution. With the help of Didier Danse, Timmy Gilissen and some online blogs (I learned a lot from the blog of Elio Struyf), I managed to reach a solution.

This solution is still under development and can be improved so I am open for suggestions. Following improvements are still lacking:

  • Obtain the translation purely by GUID and that way, avoid searching for the GUID of a term first (i had not found this yet)
  • Rewrite the scripts so that no DIV output is done until all translations are loaded (no idea if this is possible)
  • Following to the above, outputting the translated value by calling the outputFilter function and not modifying the innerHTML of the DIV

In SharePoint 2013 we can no longer use the solution by creating a farm solution and inheriting from the WebPart class as it was the case in 2010. We should now modify the refinements behavior through javascripts and by using JSOM in the Filter Display Templates.

So let’s start by opening SharePoint Designer:

1) Add the ScriptLinks to your master page. We are going to use sp.js, sp.runtime.js and sp.taxonomy.js (optionally to provide user feedback, we can add sp.ui.dialog.js and sp.init.js) . I put them in the master page because sometimes I have issues with them not being loaded before I want to use them. As my Enterprise Search Center only exists out of a page or two, I assume this can’t be a problem nor generates extra load.

	<SharePoint:ScriptLink ID="customScriptLink1" Name="sp.js" runat="server" OnDemand="false" LoadAfterUI="true" Localizable="false" />
	<SharePoint:ScriptLink ID="customScriptLink2" Name="sp.runtime.js" runat="server" OnDemand="false" LoadAfterUI="true" Localizable="false" />
	<SharePoint:ScriptLink ID="customScriptLink3" Name="sp.taxonomy.js" runat="server" OnDemand="false" LoadAfterUI="true" Localizable="false" />
	<SharePoint:ScriptLink ID="customScriptLink4" Name="sp.init.js" runat="server" OnDemand="false" LoadAfterUI="true" Localizable="false" />
	<SharePoint:ScriptLink ID="customScriptLink5" Name="sp.ui.dialog.js" runat="server" OnDemand="false" LoadAfterUI="true" Localizable="false" />

2) Copy the default filter HTML found in

_catalogs > masterpage > Display Templates > Filters

and paste it. Rename it to something like “Filter_MultiLang.html”. Don’t mind the .js-file, it will be auto-generated by SharePoint.

3) Rename the title in the Filter_MultiLang.html

<title>Multilanguage Refinement Item</title>

This will be the title that will be shown in the refiners configuration of the Refinement webpart

4) First we define some global variables we will use. I created ‘termstorename’ which is a hardcoded string containing the name of the Managed Metadata Service. You are supposed to modify this. Next we have ‘client_language’ which will hold the current browser language of the user (LCID, such as 1033, 1036,…). Next we have ‘context’ which will hold the ClientContext object, ‘taxSession’ which will hold the Taxonomy Session Object and a variable which will hold the TermStore, called ‘termStore’.

var termstorename = "Omptranet Managed Metadata Service";
var client_language = _spPageContextInfo.currentLanguage;
var context = SP.ClientContext.get_current();
var taxSession;
var termStore;

The function that will initialize the Taxonomy Session Object is the following:

function initTax(termstorename){
taxSession = SP.Taxonomy.TaxonomySession.getTaxonomySession(context);
var termStores = taxSession.get_termStores();
termStore = termStores.getByName(termstorename);
}

Notice that we just create the Term Store object and we will query this to obtain our translations. We could also use a TermSet object for this but I will explain in the next step why I choosed to use the global Term Store.

5) Now we create a function that returns the refinement GUID given the refinementName. I did not yet found a way to directly retrieve the GUID from the standard code. I thought it was the refinementToken object but that does not resemble a correct GUID so that’s why I will lookup the GUID by passing the default value of the refinement. So we are searching for strings, which is never a good idea but for now I did not find a proper solution yet.

function getGUID(refinername, termstorename, success, error){
initTax(termstorename);

var matchInfo = new SP.Taxonomy.LabelMatchInformation(context);
matchInfo.set_termLabel(refinername);
matchInfo.set_trimUnavailable(true);
var termMatches = termStore.getTerms(matchInfo);
context.load(termMatches);

context.executeQueryAsync(
function() {
if(termMatches.get_data().length &gt; 0){
success(termMatches.get_data()[0].get_id());
}else{
error("getGUID FAILED for " + refinername);
}
},
function(){
error("getGUID FAILED");
}
);
}

We use the getTerms(matchInfo) function of the termStore object to retrieve our data. Querying against the Term Store will search in all TermSets of the MM Service. We could also query against a separate TermSet but this would imply that each time we want to search, we should create a corresponding TermSet object. That’s why I just use the global Term Store.

Notice that when there is data found, I just take the first result as the GUID. It is possible that a result of the query actually shows a value from another Term Set. I did not see this as a problem because eventually it are the same words, so they should have the same translation.

6)After that, we create a function that searches the refinement name in a language we can specify. As refinerguid parameter we give the GUID that our previous function returns. For the language we can use our global variable which returns the language LCID (1033, 1036, etc…). We still pass it as a parameter to the function; this is actually not needed.

function getTranslation(refinerguid, language, termstorename, success, error){
initTax(termstorename);

var translated_term = "";
translated_term = termStore.getTerm(refinerguid).getAllLabels(language);
context.load(translated_term);
context.executeQueryAsync(
function() {
if(translated_term.get_count() &gt; 0){
success(translated_term.itemAt(0).get_value());
}else{
error("No translations found for " + refinerguid);
}
},
function(){
error("Failed");
}
);
}

7)Now we adjust the default outputFilter function :

<!--#_
            function outputFilter(refinementName, refinementCount, refiners, method, aClass, showCounts, refinementToken) {
                var aOnClick = "$getClientControl(this)." + method + "('" + $scriptEncode(Sys.Serialization.JavaScriptSerializer.serialize(refiners)) + "');";
                var nameClass = "ms-ref-name " + (showCounts ? "ms-displayInline" : "ms-displayInlineBlock ms-ref-ellipsis");

		if(!(method == "updateRefinersJSON")){
		  var guid = refinementToken.match(/"+([^"]*)/g)[0].replace('"','').substring(2);

		        getGUID(refinementName, termstorename,
		         function(refinerGUID){
		         	 console.log("GUID found for " + refinementName + " -> " + refinerGUID);
					 getTranslation(refinerGUID, client_language, termstorename,
				         function(translation){
			 				document.getElementById("RefinementName" + guid).innerHTML = ">" + translation;
						 },
				         function(fail){
				            console.log(fail);
						 });
				 },
		         function(fail){
		            console.log(fail);
				 });
	}
_#-->
            <div id='Value' name='Item'>
                <a id='FilterLink' class='_#= $htmlEncode(aClass) =#_' onclick="_#= aOnClick =#_" href='javascript:{}' title='_#= $htmlEncode(String.format(Srch.U.loadResource("rf_RefineBy"), refinementName)) =#_'>
                    <div id='RefinementName_#= guid =#_' class='_#= nameClass =#_'> _#= $htmlEncode(refinementName) =#_ </div>
<!--#_
                if (showCounts) {
_#-->
                    <div id='RefinementCount' class='ms-ref-count ms-textSmall'> (_#= $htmlEncode(Srch.U.toFormattedNumber(refinementCount)) =#_) </div>
<!--#_
                }
_#-->
                </a>
            </div>
<!--#_

            };

Notice that I pass the refinementToken parameters as an extra (because I thought It would equals the refinement GUID) but It may be deleted.

Notice that we call our two custom functions to retrieve the translation. We show the translation on the page by calling

document.getElementById("RefinementName" + guid).innerHTML = ">" + translation;

This might not be the right way to do it. Ideally, we would pass the translated term to the outputFilter function but by using our async calls, the divs are already displayed on the page. If there is a way to let the div-output wait the finishing of the async calls, that would be great but for the moment I have not found the solution.

That is also why we concatenate the (incorrect) guid to the div’s ID so that we can get the div-object in the DOM and modify the innerHTML.

Also the if-statement that checks the method not to be “updateRefinersJSON” is there for a reason. We don’t want to force translations on the “All Items”-link (that shows after you have refined). There is a chance that otherwise the “All items”-link is overwritten by a translation. And what’s even better, that link will be translated OOTB.

7) Another thing is that when we have to modify the show / hide refiners function. By default some things are (re-)loaded and it might interact with our translations that were added by our javascripts. That’s why I modified the default code to:

</div>
                <a id='unselToggle' class='ms-ref-unsel-toggle ms-commandLink' onclick='if(this.parentNode.querySelector("#unselLongList").style.display == "none"){this.parentNode.querySelector("#unselLongList").style.display="block";for(i=0;i &#60; 5;i++){this.parentNode.querySelector("#unselLongList").querySelectorAll("#Value")[i].style.display="none";}}else{this.parentNode.querySelector("#unselLongList").style.display="none";};'>
                    <div class="ms-displayInlineBlock">_#= $htmlEncode(Srch.U.loadResource("rf_RefinementLabel_More")) =#_</div>
                </a>
<!--#_
            }
_#-->

            </div>

Actually it’s just a javascript toggle.

8) To see your code in action on the search refinements, edit your refinement panel, click Refinements… and modify the Filter Display Template per section. You should be able to select your custom Filter there (as the title you gave your custom HTML-page).

9) Result: In the screenshot, my browser language is French. Where my code found a translation, the refinement is prefixed with a >

10) as an extra, we could add some notifications to alert the user that there is some extra translation code going on. Attention: you have to add the ScriptLinks to sp.init.js and sp.ui.Dialog.js as described in step 1

Add the following function to the page:

	function showFeedback(message){
		try{
//the round loading gif is = PROGRESS-CIRCLE-24.GIF (i suggest to add some resizing attributes eg. width='12' in the img-tag)
		SP.UI.Notify.addNotification("<img src='" + _spPageContextInfo.siteAbsoluteUrl + "/" + _spPageContextInfo.layoutsUrl + "/images/progress.gif'>&nbsp;&nbsp;" + message, false);
		}catch(Err){
			//none
		}
	}

Next, place the following code

showFeedback("Translating " + refinerCatTitle);

beneath this code:

var refinerCatTitle = Srch.Refinement.getRefinementTitle(ctx.RefinementControl);

The result will be a notification that refinements are translated (per block)

Now, for different reasons it can occur that terms are not translated. For example, the content type names still aren’t displayed multilingual. Or non-managed metadata that is used as refinements. Thanks to the search against the global termstore, we can fix these lacking translations by just adding a new termset eg. “UntranslatedContent”, and add the terms there (with the translation of course)

Full source: Download Filter_MultiLang.html

Tags : , ,

3 thoughts on “MUI Refinements panel in SharePoint 2013”

  1. Great solution! Helped me a lot.

    By the way, you can get the default term store of a site with this:
    TermStore termStore = taxonomySession.GetDefaultSiteCollectionTermStore();

    But for this you have to select the option “Default term store for term sets…” in the properties of the managed metadata service application.

    1. Verthosa says:

      Hey tnx for your reply. As this article is already a bit old i would suggest to modify the async calls of the JSOM to use javascript promises, as well to reform the code in a more object-oriented way to ensure the loading of the scripts in SharePoint. This is something i learned after i wrote this solution. I might post some examples when i find the time ;-)

  2. However, you’ll find significant risks involved when you fimd youdself working
    with private funding sources because oof thee fact thyat they may moderate your business very quickly.
    The initial thing yyou have to do is build a business blog that relates specifically to the products or services that youu
    will be marketing. Study businsses that possess a proven tract record for generating target income streams and revenue.

Leave a Reply to Verthosa Cancel reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>