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 |
|
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 |
|
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 |
|
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; } } } } } |