Wednesday, January 22, 2014

Developing Translation Manager Plug Ins

In this post I want to talk about some of the cool but sadly no often used features available with SDL Tridion, Translation Manager plug in system. This plug in system allow us to customize the default Translation Jobs processing behavior. Before I start with the Translation Manager Plug In system I will provide some background about Translation Manager and how it is integrated with a Translation System like TMS or World Server.

The following picture will explain how the integration is done.


Just to provide a little more context. The translation is started in Tridion via Translation Manager and is sent to the Translation System via the Translation Manager Service, then after the translation is completed in the Translation system it is returned back to Tridion and the changes are applied by the Translation Manger Service and visualized by SDL Tridion and Translation Manager.

So the behavior above is the default one, but what happens if we want to customize it for instance we want to add or update items that are sent for translation or we want to execute some operation after the translation is completed (like notifications or workflow integrations). Here is when the Translation Manager Plug In system comes into the picture.

Developing a Translation Manager Plug In

Step 1

We should start identifying the API and the libraries that will help us to develop a Plug In.

Translation Manager API (Tridion.TranslationManager.DomainModel.dll)
Tridion Core Services (Tridion.ContentManager.CoreService.Client.dll) – I prefer using Core Services instead of TOM .Net API when I connect to Tridion from a Plug In, because I don’t need to start a new Session object and I don’t have to deal with session objects initialization and disposal.

Once you have identified the libraries that will help us to develop a plug in, you will need to reference them to your project in Visual Studio (Class Library project).

Step 2

The next step is to create the plug in class skeleton as you can see in the following code.

[TranslationManagerPlugIn]
public class MyTranslationPlugIn
{
    public MyTranslationPlugIn()
    {
        TranslationJobManager.TranslationJobCreated += TranslationJobManagerInitiated;
        TranslationJobManager.TranslationJobLoaded += TranslationJobManagerInitiated;
    }

    public void TranslationJobManagerInitiated(object sender, TranslationJobEventArgs e)
    {
            
    }
}

As you might noticed in the source code above we need to decorate the class with the TranslationManagerPlugIn attribute.

Step 3

Subscribe to a Translation Event that should be extended to meet your requirement. The events that can be used are actually Translation Jobs events. Below are the list and a some description of them.

Deleted: Executed when a Translation Job is deleted. A translation job can be deleted when it is completed or canceled from a Translation System.
Deleting: This event happens while the Translation Job is being deleted but the translation is not completed yet.
Saved: Executed when a Translation Job is saved. It is a good event to add / remove more items for Translation.
Saving: This event happens while the Translation Job is being saved but the transaction is not completed yet.
Resolved: Executed when all the items are resolved.
Resolving: This is the last stage where the resolved items collection can be modified and items can be added to the items sent for Translation. Items added at this stage are not visible in a Translation Job.
StateChanged: Executed when the Translation job changes its state. It can be used to execute logic when the Translation is completed.
StateChanging: This event happens when the Translation Job is changing its state but the transaction is not completed yet.

The following sample will add linked components to the Translation Job using the Save Event.

[TranslationManagerPlugIn]
public class AddLinkedComponents
{
    private readonly string UserId = "Administrator";

    public AddLinkedComponents()
    {
        TranslationJobManager.TranslationJobCreated += OnTranslationJobInitiated;
        TranslationJobManager.TranslationJobLoaded += OnTranslationJobInitiated;
    }

    public void OnTranslationJobInitiated(object sender, TranslationJobEventArgs e)
    {
        e.TranslationJob.Saving += OnSaving;
    }

    public void OnSaving(object sender, System.ComponentModel.CancelEventArgs e)
    {
        TranslationJob job = (TranslationJob)sender;
        if (job.State == TranslationJobState.Definition)
        {
            SessionAwareCoreServiceClient channel = new SessionAwareCoreServiceClient("netTcp_2012");
            try
            {
                channel.Impersonate(UserId);

                IEnumerable<XElement> links = null;
                XNamespace xLinkNS = XNamespace.Get("http://www.w3.org/1999/xlink");

                foreach (AddedItem addedItem in job.AddedItems)
                {
                    string addedItemId = addedItem.TcmUri.ToString();
                    RepositoryLocalObjectData item = (RepositoryLocalObjectData)channel.Read(addedItemId, new ReadOptions());
                    if (item is ComponentData) {
                        ComponentData component = (ComponentData)item;
                        if (!string.IsNullOrEmpty(component.Content)) {
                            XElement xContent = XElement.Parse(component.Content);
                            XNamespace xNS = XNamespace.Get(xContent.FirstAttribute.Value);

                            links = xContent.Descendants().Where(w => w.Attributes(xLinkNS + "href").Count() > 0);
                        }
                    }
                }

                if (links != null && links.Count() > 0)
                {
                    foreach (XElement link in links)
                    {
                        AddedItem newItem = new AddedItem(link.Attribute(xLinkNS + "href").Value, TranslationOptions.TranslateSubItems);
                        if (!job.AddedItems.Contains(newItem))
                        {
                            job.AddedItems.Add(newItem);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                channel.Abort();
                throw ex;
            }
        }
    }
}

Step 4

In this sample I am using Core Services to access Tridion Objects so you will need to configure the endpoints, the easiest is to configure the Core Services endpoints and bindings in the app.config files for your class library.

Step 5

Once you have finished developing your plug in then you need to register it. You can do in but updating the TranslationManager.xml file available at %TRIDION_HOME%/config

Here a sample:

  <PlugInAssemblies>
    <Assembly fullPath="D:\Tridion\Translation Manager\Plugins\Tridion.TranslationManager.PlugIn.dll"/>
  </PlugInAssemblies>