One of the great things about working on MOSS projects here in Europe is the prevalance of multilanguage requirements. You just don't encounter multilanguage requirements (at least on intranets & extranets) nearly as often in the US. I've had a lot of deep, hands-on experience with custom and variations-based multilanguage sites this year, and wanted to provide a heads-up regarding a problem and a novel solution when using variations and search.

The Problem

This is a pretty straightforward scenario you'd encounter if you are using variations to implement a multilanguage site. Assuming you've got the default shared scopes configured and are using a master page like default.master that uses the SmallSearchInputBox delegate control, here's the repro:

  1. Create a new site collection in language X (English in this example).
  2. Configure the site collection to use custom scopes (Site Actions > Modify All Site Settings > Site Settings > Search Settings > Use Custom Scopes).
  3. Set the variation home ("/" in this example).
  4. Create a variation label for language X (English in this example)
  5. Create a variation label for language Y (Dutch in this example)
  6. Create the variation hierarchies
  7. Navigate to the welcome page for variation site in language X
  8. Select the search dropdown control - "This Site:...", "All Sites", "People" appear in the list (these labels are different if language X is not English).
  9. Navigate to the welcome page for variation site in language Y
  10. Select the search dropdown control - "Deze Site:..." appears in the list (this label is different if language Y is not Dutch).

You'll find "All Sites" and "People" (and any other shared scopes) are missing for any variation site where the language is different from the root site. So where did they go?

The key question is, "why do items appear in the search dropdown control?" The dependency in question is a Search Display Group (Site Actions > Modify All Site Settings > Site Settings > Search Scopes). In single-language site collections, display groups and search scopes all use the same language, so no special configuration is necessary.

In Multilanguage site collections – such as those that use variations – there is an important difference in behaviors. As with single-language site collections, the search scopes and display groups are created in the top-level site’s language. However, some of the subsites are created in different languages. On each site, the search control will look for the scopes in the Search Dropdown display group (which is named differently for each language) to determine which scopes to show on the search control.

On the sites where the language is the SAME as the top-level site, all the scopes in the Search Dropdown display group will render – this is because the names of the Search Dropdown display group in the search settings and the search control match. For example, if the top-level site is English, the Search Dropdown display group is named “Search Dropdown”, and the search control on English sites will be looking for scopes in a display group named “Search Dropdown”.

On the sites where the language is DIFFERENT from the top level site, none of the scopes in the Search Dropdown display group will render – this is because the names of the Search Dropdown display group in the search settings and the search control do not match. For example, if the top-level site is English, the Search Dropdown display group is named “Search Dropdown”, and the search control on Dutch sites will be looking for scopes in a display group named “Vervolgkeuzelijst voor zoeken”, which does not exist.

The Design

Our solution was based on our requirements, which is to support the set of five languages required by our customer. The solution components are a single site collection-scoped feature with a feature receiver, which are manually activated as needed by site collection owners for sites that use variations. When the feature is activated, the feature receiver performs the following tasks:

  • Retrieves the list of five language-specific Search Dropdown display group names from the feature properties
  • Identifies the default scope in the existing Search Dropdown display group
  • Creates any of the Search Dropdown display group that do not exist
  • Ensures that all compiled shared search scopes are added to each search dropdown display group

The feature does NOT delete any of these display groups or their associated scopes upon deactivation, as it is not possible to determine which scopes were set manually. If additional language packs would be installed, we'd need to include the additional language-specific Search Dropdown display group names. Extending the feature to support additional languages simply requires adding a new property to the feature.xml <Properties/> element.

The Code

There are only two pieces to the solution - the feature manifest and the feature receiver. The example below just shows properties for two languages to keep things simple (and avoid problems handling unicode).

Feature.xml:

<?xml version="1.0" encoding="utf-8"?>

<Feature
Id="12345678-1234-1234-1234-1234567890AB"
Title="ScopeDisplayGroups"
Description="This feature is used to create the search scope display groups needed to enable enhanced search on multilanguage sites."
Version="1.0.0.0"
Scope="Site"
Hidden="false"
ReceiverAssembly="ScopeDisplayGroups, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1234123412341234"
ReceiverClass="ScopeDisplayGroups.FeatureReceiver" xmlns="http://schemas.microsoft.com/sharepoint/">

<Properties>
<
Property Key="en" Value="Search Dropdown"/>
<
Property Key="nl" Value="Vervolgkeuzelijst voor zoeken"/>
</Properties>

</Feature>

Feature receiver:

using System;

using System.Web;

using System.Web.UI;

using System.Web.UI.WebControls;

using System.Web.UI.WebControls.WebParts;

using Microsoft.SharePoint;

using Microsoft.SharePoint.WebControls;

using Microsoft.SharePoint.WebPartPages;

using Microsoft.SharePoint.Navigation;

using Microsoft.SharePoint.Administration;

using Microsoft.Office.Server.Search.Administration;

namespace ScopeDisplayGroups {

public class FeatureReceiver : SPFeatureReceiver {

public override void FeatureActivated(SPFeatureReceiverProperties properties) {

 

SearchContext searchContext;

string currentGroupName = string.Empty;

using (SPSite currentSite = (SPSite)properties.Feature.Parent)

{

//retrieve current scope display groups from this site

searchContext = SearchContext.GetContext(currentSite);

Scopes scopes = new Scopes(searchContext);

ScopeDisplayGroupCollection scopeDisplayGroups = scopes.AllDisplayGroups;

Uri siteUri = new Uri(currentSite.Url);

ScopeDisplayGroup displayGroup = null;

Scope defaultScope = null;

 

//retrieve new scope display group labels from feature properties

//first, iterate through the scope display group labels to find the one that exists

//so we can identify the default scope

foreach (SPFeatureProperty featureProperty in properties.Feature.Properties)

{

currentGroupName = featureProperty.Value;

try

{

displayGroup = scopes.GetDisplayGroup(siteUri, currentGroupName);

defaultScope = displayGroup.Default;

}

catch (InvalidCastException ex)

{

//do nothing if group is not found

}

}

//retrieve new scope display group labels from feature properties

foreach (SPFeatureProperty featureProperty in properties.Feature.Properties)

{

currentGroupName = featureProperty.Value;

try

{

displayGroup = scopes.GetDisplayGroup(siteUri, currentGroupName);

//if display group exists, add any missing compiled shared scopes

//this is a "bonus feature", since API doesn't allow us to determine which scopes are OOTB

//normally only All Sites and People are added automatically to the search dropdown feature

foreach (Scope sharedScope in scopes.GetSharedScopes())

{

//only add shared scopes that are currently compiled AND are not part of display group

if (!displayGroup.Contains(sharedScope) &&

(sharedScope.CompilationState.Equals(ScopeCompilationState.Compiled)))

displayGroup.Add(sharedScope);

}

displayGroup.Update();

}

catch (InvalidCastException ex)

{

//if display group does not exist, create the new scope display group

ScopeDisplayGroup newDisplayGroup = scopeDisplayGroups.Create(currentGroupName, string.Empty, siteUri, true);

foreach (Scope sharedScope in scopes.GetSharedScopes())

{

//only add shared scopes that are currently compiled

if (sharedScope.CompilationState.Equals(ScopeCompilationState.Compiled))

newDisplayGroup.Add(sharedScope);

}

//if one was found, set the default scope to the same value for all new display groups

//AND move it to the first choice on the list

if (defaultScope != null)

{

newDisplayGroup.Remove(defaultScope);

newDisplayGroup.Insert(0, defaultScope);

newDisplayGroup.Default = defaultScope;

}

newDisplayGroup.Update();

}

scopes.Update();

}

}

 

}

public override void FeatureDeactivating(SPFeatureReceiverProperties properties) {

/* no op */

//do nothing - removing search scope display groups could be destructive

}

public override void FeatureInstalled(SPFeatureReceiverProperties properties) {

/* no op */

}

public override void FeatureUninstalling(SPFeatureReceiverProperties properties) {

/* no op */

}

}

}