SDL Tridion Workflow – Translation Manager Integration

One common use case in several SDL Tridion implementations is to include Content Translation capabilities in SDL Tridion Workflow.  There are several design decisions to take in order to accomplish, I will list the main ones in this blog post.

1. Blue Print

Content Translation is about Content Localization, we have to be very careful while integrating SDL Tridion Workflow with Translation Manager since they are two different entities. Having two entities that can manipulate a Tridion Item (Component, Page, and so on) can lead to errors like “Item cannot be updated because is checked out by another user”.

Additionally we need to consider that Translation Manager can perform two types of Transactions.

1.1. Push Transactions

These Transactions are performed when the Translation is started in a Source Publication. A Source Publication acts as a source of content that is translation lower in the Blueprint in Target Publications.

When Translation Manager founds that the Translation Job was created in a Source Publication it will localize the items sent for Translation on each Target Publication using the Translation configuration (Publication Translation Tab in the Publication Properties Dialog).

In SDL Tridion Workflow terms if the Translation Job is created in a Source Publication and sent for Translation as part of a Workflow Activity we need to ensure that the items sent for Translation are not involved in any other SDL Tridion Workflow instance so that the Push Transaction can be completed successfully.

1.2. Pull Transactions

These Transactions are performed when the Translation is started in a Target Publication. As mentioned above we have to be careful in order to avoid locking items sent for Translation, having said that, Pull Transactions cannot be started within an SDL Tridion Workflow simply because we cannot localize an item that is checked out by another user or process.

In order to integrate Pull Transactions with SDL Tridion Workflow we need to translate first and then start a SDL Tridion Workflow instance. This operation can be automatically done by creating a Translation Manager Plug In which will start a Workflow instance when the Pull Transaction is completed.

2. Establishing connectivity between SDL Tridion Workflow and Translation Manager

Translation Manager doesn’t have an API that is compatible with the SDL Tridion Workflow API (Based on Core Services) for that reason it is recommended to create a Translation Manger Façade by implementing a WCF service that acts as glue between SDL Tridion Workflow (Core Services) and Translation Manager (COM+)

Translation Manager Architecture

Description

  • Allows to start Translation Jobs from SDL Tridion Workflow code
  • Translation Manager does not have an API that is compatible with SDL Tridion Workflow (Core Services). In that sense a Translation Manager Façade (WCF Service) to be implemented in order to provide a compatible integration point

Design

Translation Manager Facade

using System;

using System.Collections.Generic;

using System.Linq;

using System.Runtime.Serialization;

using System.ServiceModel;

using System.ServiceModel.Web;

using System.Text;

using Tridion.TranslationManager.DomainModel.Api;

 

namespace SDL.Extensions.Workflow.Translation

{

    public class TranslationManagerFacade : ITranslation

    {

        private readonly string UserId = "[Content Manager Admin]";

 

        public string Translate(string title, string publicationId, IEnumerable<string> items)

        {

            TranslationJobManager manager = new TranslationJobManager(UserId);

            TranslationJob job = manager.CreateJob(title, publicationId, TranslationJobType.PushJob);

            job.RequiredDate = DateTime.Now;

            job.Priority = TranslationJobPriority.High;

 

            TranslationConfiguration configuration =

manager.GetTranslationConfiguration(

new Tridion.TranslationManager.DomainModel.Api.TcmUri(publicationId));

 

  foreach (TranslationConfiguration targetconfiguration in configuration.TargetConfigurations)

            {

                job.TargetPublicationUris.Add(targetconfiguration.ConfiguredItemUri);

            }

 

            foreach (string item in items)

            {

                job.AddedItems.Add(new AddedItem(item, TranslationOptions.TranslateSubItems));

            }

 

            job.IncludeItemsAlreadyTranslated = true;

            job.State = TranslationJobState.ReadyForTranslation;

            job.Save();

 

            return job.Id.ToString();

        }

    }

}

 

3. Workflow Process Definition for Push Transactions

In this Blog Post I cover Push Transactions; I will provide more details on Pull Transactions in another Blog Post. The diagram below shows a basic Tridion Workflow Process Definition that includes an Automatic Activity called Translate which will create a Translation Job and will start a Push Transaction.

3.1.Translate Activity

Translate Activity

Description

  • Starts a Translation Job for the Bundle assigned to the current Workflow Process
  • Suspends the activity until the Translation Job is completed and translated content is returned to Tridion from SDL World Server
  • Starts a Pull Transaction
  • Content is sent to a Translation System (World Server, BeGlobal, TMS)

Type

Automatic

Approval Status

Staging

Assignee

Workflow Agent Identity

Due Date

Not Specified

Script Type

External Activity

Script

AssemblyTbbId = "[Workflow Assembly Id]"

Type = "SDL.Extensions.Workflow.ExternalActivities.Translate.TranslateActivity"

Implementation Approach

using SDL.Extensions.Workflow.ExternalActivities.Base;

using SDL.Extensions.Workflow.Utils;

using System;

using System.Collections.Generic;

using System.Linq;

using System.ServiceModel;

using System.ServiceModel.Channels;

using System.Text;

using System.Threading.Tasks;

 

namespace SDL.Extensions.Workflow.ExternalActivities.Translate {

    public class TranslateActivity : AbstractActivity {

        protected override void Initialize() {

            base.Initialize();

            NextAssignee = GetLastManualActivityPerformer();

        }

 

        protected override void Execute() {

            Initialize();

 

            string targetTranslationPublication = GetCurrentPublication();

            Binding binding = GetTranslationManagerFacadeBinding();

            EndpointAddress url = GetTranslationManagerEndpointUrl();

 

            TranslationManagerFacade channel = new TranslationManagerFacade(binding, url);

            IEnumerable<string> items = GetBundleItemsIdentifiersForTranslation();

            string translationId = channel.Translate(Bundle.Title, targetTranslationPublication, items.ToArray());

 

            SuspendedActivity = CoreServiceClient.SuspendActivity(ActivityInstance.Id, PublishConstants.TranslateAwarePublish, null, PublishConstants.TranslateAwarePublish, ReadOptions);

 

            if (ProcessInstance.Variables.ContainsKey(PublishConstants.TranslateAwarePublish)) {

                ProcessInstance.Variables[PublishConstants.TranslateAwarePublish] = translationId;

            }

            else {

                ProcessInstance.Variables.Add(PublishConstants.TranslateAwarePublish, translationId);

            }

        }

 

        private EndpointAddress GetTranslationManagerEndpointUrl() {

            //TODO: Get Translation Façade Endpoint

        }

 

        protected override void Resume(string bookmark) {

            if (bookmark == PublishConstants.TranslateAwarePublish) {

                base.Resume(bookmark);

                FinishActivity();

            }

        }

 

        private string GetCurrentPublication() {

            //TODO: Get Current Publication

        }

 

        private IEnumerable<string> GetBundleItemsIdentifiersForTranslation() {

            //TODO: Get Item Ids in the Bundle

        }

 

        private Binding GetTranslationManagerFacadeBinding() {

            //TODO: Get Translation Façade WCF Binding

        }

    }

}

4.  Resuming the Workflow Instance after the Translation is completed

Based on the SDL Tridion Workflow process definition above the Translate activity is the first one to be executed then the results are reviewed and published to the different targets.

Translation transactions may take several minutes, hours or even days (depending on how complicated is the Translation process in the Translation system or if it is manual or automatic). It is necessary to suspend the Workflow Instance and re-activate (resume) it only when the Translation Transaction is completed. In order to do that we need to implement a Translation Manager Plug In.

Translation Manager Plug In

Description

  • It is executed after the Translation is completed for a given Translation Job
  • Resumes the Translated Activity that was suspended in order to continue with the workflow processing

Implementation Approach

using System.IO;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

using Tridion.TranslationManager.DomainModel.Api;

using Tridion.ContentManager.CoreService.Client;

using System.ServiceModel;

 

namespace SDL.Extensions.Workflow.Translation.PlugIns

{

    [TranslationManagerPlugIn]

    public class ResumeActivityUponTranslation

    {

        private readonly string UserId = "[Content Manager Name]";

 

        public ResumeActivityUponTranslation()

        {

            TranslationJobManager.TranslationJobCreated += TranslationJobManagerInitiated;

            TranslationJobManager.TranslationJobLoaded += TranslationJobManagerInitiated;

        }

 

        public void TranslationJobManagerInitiated(object sender, TranslationJobEventArgs e)

        {

            e.TranslationJob.StateChanged += OnStateChanged;

        }

 

        private void OnStateChanged(object sender, TranslationJobStateChangeEventArgs e)

        {

            TranslationJob job = (TranslationJob)sender;

            if (job.State == TranslationJobState.Completed)

            {

                string jobId = job.Id.ToString();

                SessionAwareCoreServiceClient channel = new SessionAwareCoreServiceClient("netTcp_2012");

                try

                {

                    channel.Impersonate(UserId);

 

                    ActivityInstancesFilterData activitiesFilter = new ActivityInstancesFilterData()

                    {

                        ForAllUsers = true,

                        ActivityState = ActivityState.Suspended

                    };

 

                    IEnumerable<ActivityInstanceData> activities =

                        channel.GetSystemWideList(activitiesFilter).Cast<ActivityInstanceData>().Where(w => w.SuspendOrFailReason == " TranslateAwarePublish");

 

                    KeyValuePair<string, string> identifier = new KeyValuePair<string, string>("TranslationId", jobId);

                    ActivityInstanceData activity = activities.FirstOrDefault(a =>

                        ((ProcessInstanceData)channel.Read(a.Process.IdRef, new ReadOptions())).Variables.Contains(identifier));

 

                    if (activity != null)

                    {

                        channel.ResumeActivity(activity.Id, new ReadOptions());

                    }

 

                    channel.Close();

                }

                catch (Exception ex)

                {

                    channel.Abort();

                    throw ex;

                }  

            }

        }

    }

}

Anonymous
  • @Eric, Thanks for the article.

    A quick question, is there a reason you are using the same handler function "TranslationJobManagerInitiated" for Job created and loaded events of the job?

    public ResumeActivityUponTranslation()

           {

               TranslationJobManager.TranslationJobCreated += TranslationJobManagerInitiated;

               TranslationJobManager.TranslationJobLoaded += TranslationJobManagerInitiated;

           }

  • @Eric thanks a lot for this helpful article..