Monday, December 15, 2014

Localize content type names and field titles in Sharepoint sandbox solutions

If you work with Sharepoint starting with pre-2013 version most probably you know how to localize strings (content type names, field types, feature names, web part titles, etc.) in farm solutions. In order to do it we need to use provisioning resources with special syntax: $Resources:{resource file name without resx extension},{string identifier}, e.g. “$Resources:MyResource,Foo”. It assumes that specified resx file is provisioned with farm solution to 14 or 15/Resources folder (in VS solution you may create mapped folder for that). But when we work with farm solutions situation is different: files are not saved to the local file system. Because of that we can’t use the same approach for localizing strings for various artifacts which are provisioned with sandbox solution. In this post I will show one approach which may be used for localizing content types names and fields titles which is most probably is most often needed requirement.

First of all I need to say that tried many ways before found really working approach. Also need to say that this approach uses codebehind which is as you probably know is deprecated in sandbox solutions by MS. If it is important for you to not use codebehind in sandbox solutions, you may rewrite the code e.g. via javascript client object model which and use the same idea for running it first time for provisioned site (see below). Also example below is shown for scenario when in the same site collection there may be several different languages. When you have single language per site collection approach will be simpler: instead of fixing content types inheritors in all bound lists it will be enough to fix only site content type and propagate changes to all lists which use this content type.

Ok, lets check the approach itself. Site columns (fields) and content types are provisioned with hardcoded strings (I use English names, but you may use any language for default string values depending on your language):

   1: <Field ID="..."
   2:     Name="MyField"
   3:     SourceID="http://schemas.microsoft.com/sharepoint/v3"
   4:     StaticName="MyField"
   5:     Group="Custom"
   6:     Type="Text"
   7:     DisplayName="My Field"/>

content type:

   1: <ContentType ID="..."
   2:              Name="My Content type"
   3:              Group="Custom"
   4:              Inherits="TRUE"
   5:              Version="0">
   6:   <FieldRefs>
   7:     <FieldRef ID="..." Name="MyField" />
   8:     ...
   9:   </FieldRefs>
  10: </ContentType>

After we will provision these elements to the Sharepoint site they will have names/titles which are hardcoded in xml shown above. Also in all lists where this content type is bound to the same string values will be used. The next step is to run code which will localize these strings depending on the specified language.

As I already mentioned above we will consider the scenario when there may be sub sites (SPWeb) on different languages in the same site collection. In this case we will need to get reference on the list content type which inherits site content type and update strings there. If we would update site content type itself and would propagate changes to all lists which use this content type, all sub sites would have titles on the same language which was last used which is of course not what we want.

Translated strings should be stored in resx files which are embedded to your sandbox solution’s assembly. After you will add them to VS solution you will also need to add additional assemblies to your package manifest (one assembly per each translation):

image

In this example we added Finnish and Russian resources.

After that we are ready to make actual localization. Here is the sandbox code which will localize list content type and its fields:

   1: public static void LocalizeCTFields(SPWeb web)
   2: {
   3:     var list = web.Lists.Cast<SPList>().FirstOrDefault(l =>
   4:         l.RootFolder.Url == "MyList");
   5:     if (list == null)
   6:     {
   7:         return;
   8:     }
   9:  
  10:     var ci = new CultureInfo((int)web.Language);
  11:     web.AllowUnsafeUpdates = true;
  12:     var fieldsList = list.Fields.Cast<SPField>().ToList();
  13:     var ct = list.ContentTypes.Cast<SPContentType>().FirstOrDefault(c =>
  14:         c.Id.IsChildOf(new SPContentTypeId(MY_CONTENT_TYPE_ID)));
  15:     if (ct != null)
  16:     {
  17:         ct.Name = Properties.Resources.ResourceManager.
  18:             GetString("MyContentType_Title", ci);
  19:         ct.Update();
  20:     }
  21:  
  22:     localizeField(fieldsList, new Guid(FIELD1_ID),
  23:         Properties.Resources.ResourceManager.GetString("Field1_Title", ci));
  24:     localizeField(fieldsList, new Guid(FIELD2_ID),
  25:         Properties.Resources.ResourceManager.GetString("Field2_Title", ci));
  26:     ...
  27: }
  28:  
  29: private static void localizeField(List<SPField> fields, Guid fieldId,
  30:     string title)
  31: {
  32:     if (fields.IsNullOrEmpty() || string.IsNullOrEmpty(title))
  33:     {
  34:         return;
  35:     }
  36:     var field = fields.FirstOrDefault(f => f.Id == fieldId);
  37:     if (field == null)
  38:     {
  39:         return;
  40:     }
  41:     field.Title = title;
  42:     field.Update(true);
  43: }

It gets the reference to the SPLIst (lines 3-8), then find needed content type in the list (lines 13-15) and updates content type name (lins 15-20) and all fields in the list which correspond to appropriate content type (lines 22-26) with strings from resource file for the language of the current site (line 10). Note that we get reference on fields collection from SPList.Fields, not from SPContentType.Fields or FieldLinks.

The remaining question is where to put this code and in what moment it should be executed. I tried to use WebProvisioned handler, but it didn’t work: there were no any errors, just changes were not applied to the content types and fields, and they remain with default English texts. The only working approach I found is to put hidden web part with initialization code to the site’s front page. Once site is created user is redirected to the front page of this site. In this moment we may execute necessary post initialization logic. We need to ensure that it will be executed once. In the farm env it will be tricky to ensure that code is running in single thread once, so I will omit these details here:

   1: public class InitializationWebPart : WebPart
   2: {
   3:     protected override void OnLoad(EventArgs e)
   4:     {
   5:         var web = SPContext.Current.Web;
   6:         web.AllowUnsafeUpdates = true;
   7:  
   8:         // site should be initialized once
   9:         if (this.isInitialized(web))
  10:         {
  11:             return;
  12:         }
  13:  
  14:         try
  15:         {
  16:             LocalizeCTFields(web);
  17:         }
  18:         finally
  19:         {
  20:             this.setInitialized(web);
  21:         }
  22:     }
  23:  
  24:     private bool isInitialized(SPWeb web)
  25:     {
  26:         if (!web.AllProperties.ContainsKey("IsInitialized"))
  27:         {
  28:             return false;
  29:         }
  30:         return bool.Parse(web.AllProperties["IsInitialized"] as string);
  31:     }
  32:  
  33:     private void setInitialized(SPWeb web)
  34:     {
  35:         web.SetProperty("IsInitialized", true);
  36:         web.Update();
  37:     }
  38: }

As shown in this example when web part runs the code once, it will update property bag setting and won’t run twice.

This post shows that although in sandbox solution simple operations like localization of content types and fields are more complicated they are still possible and you may use it in your projects.

1 comment:

  1. Alexey, if you need a localization tool to help you manage the translation of software strings, a nice one with a friendly and collaborative interface is https://poeditor.com/
    The API and Translation Memory are particularly useful.

    ReplyDelete