Office365 – Multilingual content types, site columns and other site elements

As part of the new client side object model assemblies released in the SharePoint Online Client Components SDK, we now have additional capabilities for providing multilingual experiences in the Office365 SharePoint sites. This is good example of the cloud first release model, since this capability is not right now available for on-premises. It will be obviously included in future updates to the on-premises side as well, but there’s currently no public schedule for that.

What is supported

Here’s the list of labels which are currently supported labels which can be automatically translated when UI language is changed in the SharePoint site.

  • Site Title
  • Site Description
  • List Title
  • List Description
  • Content Type Name
  • Content Type Description
  • Site column Title
  • Site column Description

Notice that these are available only in the 16 versions of the client side object model assemblies. After installation of the SharePoint Online Client Components SDK, these will be available from the c:\program files\common files\Microsoft shared\web server extensions\16\ISAPI folder. These assemblies are having some dependencies on the capabilites which are currently only available from the Office365, so they might have some issues if you use them against on-premisese environment. If you only use capabilities which are in on-premises, you’ll be fine. This translation capability is currently only available in the Office365.

image

How do I enable multilingual sites in Office365?

So how do I actually get the translations to work in Offic365? There are few things which we need to configure. First thing is to enable multilingual support in the site level. This is done from Site Settings – Language Settings.

image

You will have to enable those languages which you want to be supported for this particular site.

image

Notice that currently we don’t have native way to enable language in site level using CSOM or out of the box remote pattern. This is known gap, which will be addressed sooner or later. If you have critical need to perform this, you can actually achieve it by using the controversial HTTP Post pattern, which has its own downsides.

After the previous steps, this site supports alternative languages, but to be able to see that in practice, you’ll have to select preferred language for your user profile. This can be done from OneDrive for business side or to be precise from your user profile editing pages. In following picture I’ve set Finnish as my preferred language, since Finglish was not available.

image

Now that we move back to the site where the Finnish has been enabled, we are able to see the translated content. Below pictures shows the front page in different languages.

image

image

Here’s how the list title is translated. Notice also that the title is translated properly in left navigation.

image

image

Here’s how the content type is visible when we edit document or list item properties. Notice also the site column translation.

image

image

Here’s how the site column is visible in the list with translated display text

image

image

How do I create and set the translations in code?

I’ve included here the full code for each of the examples on how the element is initially created and then how the multi-lingual support is added by using remove provisioning techniques. This would mean that we would take care of content type provisioning as part of the self service site collection creation for the collaboration sites. You can also use the same pattern against site collections which have been created from tenant admin tools, simply by running for example a console application against the just created site.

Site and list localization

Let’s start by creating the custom list which will be translated. This is pretty typical CSOM task, but added here for reference.

    1:  
    2: private static void CreateCustomList(ClientContext cc, Web web)
    3: {
    4:     ListCollection listCollection = cc.Web.Lists;
    5:     cc.Load(listCollection, lists => lists.Include(list => list.Title).
    6:                                     Where(list => list.Title == "LocalizeMe"));
    7:     cc.ExecuteQuery();
    8:     // Create the list, if it's not there...
    9:     if (listCollection.Count == 0)
   10:     {
   11:         ListCreationInformation newList = new ListCreationInformation();
   12:         newList.Title = "LocalizeMe";
   13:         newList.QuickLaunchOption = QuickLaunchOptions.On;
   14:         newList.TemplateType = (int)ListTemplateType.GenericList;
   15:         newList.Description = "LocalizeMe sample list";
   16:         List list = web.Lists.Add(newList);
   17:         cc.ExecuteQuery();
   18:     }
   19: }

Here’s the example method for site and list information localization. Notice that we simply access the UserResource objects of needed properties and we add the translation for specific language.

    1:  
    2: private static void LocalizeSiteAndList(ClientContext cc, Web web)
    3: {
    4:     // Localize site title
    5:     web.TitleResource.SetValueForUICulture("en-US", "Localize Me");
    6:     web.TitleResource.SetValueForUICulture("fi-FI", "Kielikäännä minut");
    7:     web.TitleResource.SetValueForUICulture("fr-FR", "Localize Me to French");
    8:     // Site description
    9:     web.DescriptionResource.SetValueForUICulture("en-US", 
   10:                             "Localize Me site sample");
   11:     web.DescriptionResource.SetValueForUICulture("fi-FI", 
   12:                             "Kielikäännetty saitti");
   13:     web.DescriptionResource.SetValueForUICulture("fr-FR", 
   14:                             "Localize to French in description");
   15:     web.Update();
   16:     cc.ExecuteQuery();
   17:  
   18:     // Localize custom list which was created previously
   19:     List list = cc.Web.Lists.GetByTitle("LocalizeMe");
   20:     cc.Load(list);
   21:     cc.ExecuteQuery();
   22:     list.TitleResource.SetValueForUICulture("en-US", "Localize Me");
   23:     list.TitleResource.SetValueForUICulture("fi-FI", "Kielikäännä minut");
   24:     list.TitleResource.SetValueForUICulture("fr-FR", "French text for title");
   25:     // Description
   26:     list.DescriptionResource.SetValueForUICulture("en-US", 
   27:                             "This is localization CSOM usage example list.");
   28:     list.DescriptionResource.SetValueForUICulture("fi-FI", 
   29:                 "Tämä esimerkki näyttää miten voit kielikääntää listoja.");
   30:     list.DescriptionResource.SetValueForUICulture("fr-FR", 
   31:                         "I have no idea how to translate this to French.");
   32:     list.Update();
   33:     cc.ExecuteQuery();
   34: }

Content type and site column localization

Let’s first create the site column remotely using CSOM. Again pretty typical remove operation task. Notice thought that you need to be careful on the used xml structure, since for example ID has to have also D capitalized, since otherwise a random Guid will be used as the identifier for the site column

    1:  
    2: private static void CreateSiteColumn(ClientContext cc, Web web)
    3: {
    4:     // Add site column to the content type if it's not there...
    5:     FieldCollection fields = web.Fields;
    6:     cc.Load(fields);
    7:     cc.ExecuteQuery();
    8:  
    9:     foreach (var item in fields)
   10:     {
   11:         if (item.InternalName == "ContosoString")
   12:             return;
   13:     }
   14:  
   15:     string FieldAsXML = @"<Field ID='{4F34B2ED-9CFF-4900-B091-4C0033F89944}' 
   16:                                     Name='ContosoString' 
   17:                                     DisplayName='Contoso String' 
   18:                                     Type='Text' 
   19:                                     Hidden='False' 
   20:                                     Group='Contoso Site Columns' 
   21:                                     Description='Contoso Text Field' />";
   22:     Field fld = fields.AddFieldAsXml(FieldAsXML, true, AddFieldOptions.DefaultValue);
   23:     cc.Load(fields);
   24:     cc.Load(fld);
   25:     cc.ExecuteQuery();
   26: }

Next we’ll need to create the content type. We also assign the specific ID for the site column remotely, which was one of the new things introduced lately as well.

    1:  
    2: private static void CreateContentTypeIfDoesNotExist(ClientContext cc, Web web)
    3: {
    4:     ContentTypeCollection contentTypes = web.ContentTypes;
    5:     cc.Load(contentTypes);
    6:     cc.ExecuteQuery();
    7:  
    8:     foreach (var item in contentTypes)
    9:     {
   10:         if (item.StringId == "0x0101009189AB5D3D2647B580F011DA2F356FB2")
   11:             return;
   12:     }
   13:  
   14:     // Create a Content Type Information object
   15:     ContentTypeCreationInformation newCt = new ContentTypeCreationInformation();
   16:     // Set the name for the content type
   17:     newCt.Name = "Contoso Document";
   18:     //Inherit from oob document - 0x0101 and assign 
   19:     newCt.Id = "0x0101009189AB5D3D2647B580F011DA2F356FB2";
   20:     // Set content type to be avaialble from specific group
   21:     newCt.Group = "Contoso Content Types";
   22:     // Create the content type
   23:     ContentType myContentType = contentTypes.Add(newCt);
   24:     cc.ExecuteQuery();
   25: }

And then we’ll bind the site column to the content type.

    1:  
    2: private static void AddSiteColumnToContentType(ClientContext cc, Web web)
    3:  {
    4:      ContentTypeCollection contentTypes = web.ContentTypes;
    5:      cc.Load(contentTypes);
    6:      cc.ExecuteQuery();
    7:      ContentType myContentType = contentTypes.GetById("0x0101009189AB5D3D2647B580F011DA2F356FB2");
    8:      cc.Load(myContentType);
    9:      cc.ExecuteQuery();
   10:  
   11:      FieldCollection fields = web.Fields;
   12:      Field fld = fields.GetByInternalNameOrTitle("ContosoString");
   13:      cc.Load(fields);
   14:      cc.Load(fld);
   15:      cc.ExecuteQuery();
   16:  
   17:      FieldLinkCollection refFields = myContentType.FieldLinks;
   18:      cc.Load(refFields);
   19:      cc.ExecuteQuery();
   20:  
   21:      foreach (var item in refFields)
   22:      {
   23:          if (item.Name == "ContosoString")
   24:              return;
   25:      }
   26:  
   27:      // ref does nt
   28:      FieldLinkCreationInformation link = new FieldLinkCreationInformation();
   29:      link.Field = fld;
   30:      myContentType.FieldLinks.Add(link);
   31:      myContentType.Update(true);
   32:      cc.ExecuteQuery();
   33:  }

Now the structures are created, we can then add the localization for the content type and the site column.

    1:  
    2: private static void LocalizeContentTypeAndField(ClientContext cc, Web web)
    3: {
    4:     ContentTypeCollection contentTypes = web.ContentTypes;
    5:     ContentType myContentType = contentTypes.GetById("0x0101009189AB5D3D2647B580F011DA2F356FB2");
    6:     cc.Load(contentTypes);
    7:     cc.Load(myContentType);
    8:     cc.ExecuteQuery();
    9:     // Title of the content type
   10:     myContentType.NameResource.SetValueForUICulture("en-US", 
   11:                                                     "Contoso Document");
   12:     myContentType.NameResource.SetValueForUICulture("fi-FI", 
   13:                                                     "Contoso Dokumentti");
   14:     myContentType.NameResource.SetValueForUICulture("fr-FR", 
   15:                                                     "Contoso Document (FR)");
   16:     // Description of the content type
   17:     myContentType.DescriptionResource.SetValueForUICulture("en-US", 
   18:                                     "This is the Contoso Document.");
   19:     myContentType.DescriptionResource.SetValueForUICulture("fi-FI", 
   20:                                     "Tämä on geneerinen Contoso dokumentti.");
   21:     myContentType.DescriptionResource.SetValueForUICulture("fr-FR", 
   22:                                     "French Contoso document.");
   23:     myContentType.Update(true);
   24:     cc.ExecuteQuery();
   25:  
   26:     // Do localization also for the site column
   27:     FieldCollection fields = web.Fields;
   28:     Field fld = fields.GetByInternalNameOrTitle("ContosoString");
   29:     fld.TitleResource.SetValueForUICulture("en-US", "Contoso String");
   30:     fld.TitleResource.SetValueForUICulture("fi-FI", "Contoso Teksti");
   31:     fld.TitleResource.SetValueForUICulture("fr-FR", "Contoso French String");
   32:     // Description entry
   33:     fld.DescriptionResource.SetValueForUICulture("en-US", 
   34:                             "Used to store Contoso specific metadata.");
   35:     fld.DescriptionResource.SetValueForUICulture("fi-FI", 
   36:                             "Tää on niiku Contoso metadatalle.");
   37:     fld.DescriptionResource.SetValueForUICulture("fr-FR", 
   38:                             "French Description Goes here");
   39:     fld.UpdateAndPushChanges(true);
   40:     cc.ExecuteQuery();
   41: }

Summary

MASSIVE! SUPER! AWESOME! MAGNIFICENT! GREAT! ÜBER! HIENOA! MÄKTIG!

It’s excellent to see the Office365 evolving constantly and we are seeing new capabilities introduced all the time. Not sure if the impact of these new capabilities are even truly understood, but this enables use to create portals remotely using just remote provisioning techniques, which is huge. These multilingual capabilities are definitely much needed capabilities for numerous enterprises. Personally I consider this as massive change for enabling our biggest customers to start moving to the cloud platform. This basically fulfils the initial requirements to be able to create content types and site columns using remote provisioning patterns and not to be forced to fall back on Sandbox solutions, which are hard to automate cross site collection creation.

Some reference links