# Smartling automation based on Uniform Workflow

> This tutorial describes automating sending Uniform content for translation and receiving translations from Smartling using Uniform Workflow.

Source: https://docs.uniform.app/docs/learn/tutorials/dev/tms-automation/smartling-workflow-automation

Uniform enables you to set up an automated flow for content editors to manage translations.

> **Do You Need a Workflow to Manage Smartling Translations?:**
>
> Automation via Uniform Workflow gives you much better control over when exactly the translation is sent to Smartling, locking content while translating, limiting permissions, adding review steps, etc.
>
> However, it is not required, as you can use a simple on-demand flow, which you can find [here](https://docs.uniform.app/docs/integrations/translation-management/smartling/sending-for-translation).

If you've determined that you do, in fact, need workflow automation, this tutorial will guide you through the process of building one.

## Prerequisites

You need to go through [the Smartling Integration documentation](https://docs.uniform.app/docs/integrations/translation-management/smartling) and follow both the integration installation and configuring webhook sections.

Our workflow automation code will include another webhook handler similar to the Smartling Webhook handler.

You can find the final code in [the examples repository](https://github.com/uniformdev/examples/blob/main/examples/smartling-webhooks/src/pages/api/uniform-workflow-transition-webhook.ts).

Please repeat the locale mapping you've configured on the Smartling Integration settings page inside your webhook handler app. The Uniform source locale code should go into `UNIFORM_SOURCE_LOCALE`, and `UNIFORM_TO_SMARTLING_LOCALE_MAPPING` should be a JSON with target Uniform => Smartling locale codes mapping.

## Configuring Workflow

You can read about workflow [here](https://docs.uniform.app/docs/guides/composition/workflows) and set up any kind of flow that suits you. In this tutorial, the workflow will focus on translations, but you can reuse concepts and code from here.

We will create a simple 3-stage workflow with the following stages:

- **Editing** - Initial stage which is open for Uniform content editors and is basically the only stage where content can be manually changed.
- **Ready for translation** - Second stage where content is still not published but it is locked from editing and should automatically be sent to Smartling for translation.
- **Translated** - Last stage where Uniform has received translations from Smartling and it is automatically published.

![Workflow setup for translations automation.](https://docs.uniform.app/images/learn/tutorials/tms-automation/smartling/workflow-setup.png)

You need to copy these three Public IDs into your webhook handler `.env` file:

- **Workflow Public ID** into `WORKFLOW_ID`
- **Ready for translation** stage Public ID into `WORKFLOW_LOCKED_FOR_TRANSLATION_STAGE_ID`
- **Translated** stage Public ID into `WORKFLOW_TRANSLATED_STAGE_ID`

## Configuring Uniform Workflow Webhook

Now we to send Uniform content that was moved to **Ready for translation** stage to smartling automatically. For this purpose we will use Uniform Webhook event

![Creation Uniform workflow webhook](https://docs.uniform.app/images/learn/tutorials/tms-automation/smartling/create-workflow-webhook.png)

After you've created webhook please copy **Signing Secret** and paste it into `SVIX_SECRET` env variable

You can read more about webhooks [here](https://docs.uniform.app/docs/guides/webhooks)

## Uniform webhook handler

### Prepare npm dependencies

It is recommended to keep Uniform webhook handler together with Smartling webhook handler, as they share some code and some env variables.

Please install required dependencies: `npm i smartling-api-sdk-nodejs @uniformdev/canvas @uniformdev/webhooks uuid svix @uniformdev/tms-sdk`

- `smartling-api-sdk-nodejs` - Smartling SDK to create job/files and authorize for translation
- `@uniformdev/canvas` - for fetching uniform content that was moved to **Ready for translation** stage
- `@uniformdev/webhooks` (optional) contains TS types for handling webhook payload
- `svix` - for verification of webhook payload (read more [here](https://docs.svix.com/receiving/verifying-payloads/how#nodejs-nextjs-13-app-router))
- `@uniformdev/tms-sdk` - sdk for preparing Uniform content for translations

> **Minimum SDK version:**
>
> The examples on this page use the [Content API clients](https://docs.uniform.app/docs/sdk/content-api-clients) and require `@uniformdev/canvas` and `@uniformdev/tms-sdk` **version 20.74.7 or later**. Older versions of `@uniformdev/tms-sdk` do not accept these clients.

Following code examples are based on NextJS API handlers

### Payload verification

```ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { Webhook } from "svix";

const uniformSvixSecret = process.env.SVIX_SECRET;

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const headers = req.headers as Record<string, string>;
  const wh = new Webhook(uniformSvixSecret);
  try {
    wh.verify(JSON.stringify(req.body), headers);
    console.log('payload verified')
  } catch (err) {
    console.error(err);
    res
      .status(400)
      .send(
        JSON.stringify({ reason: "webhook payload was not verified", err })
      );
    return;
  }

  ...
}
```

### Prepare Smartling SDK

```ts
import {
  SmartlingApiClientBuilder,
  ...
} from 'smartling-api-sdk-nodejs';

const smartlingUserId = process.env.SMARTLING_USER_ID;
const smartlingUserSecret = process.env.SMARTLING_USER_SECRET;
const uniformToSmartlingLanguageMap = process.env.UNIFORM_TO_SMARTLING_LOCALE_MAPPING;

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  ...

  const uniformMappedLocales = JSON.parse(uniformToSmartlingLanguageMap) as Record<string, string>;
  const apiBuilder = new SmartlingApiClientBuilder()
    .setBaseSmartlingApiUrl('https://api.smartling.com')
    .authWithUserIdAndUserSecret(smartlingUserId, smartlingUserSecret);

  ...
}

```

### Get effected entity

```ts
import { WorkflowTransitionPayload } from '@uniformdev/webhooks';

const smartlingUserId = process.env.SMARTLING_USER_ID;
const smartlingUserSecret = process.env.SMARTLING_USER_SECRET;

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  ...

      // You can find full payload structure under Uniform Project -> Settings -> Webhooks => Event Catalog
  const payloadObject: WorkflowTransitionPayload = req.body;

  if (payloadObject.newStage.stageId === uniformWorkflowStageIdReadyForTranslations) {
    const { entity, translationEntityType } = await resolveUniformEntityFromWebhook(payloadObject);

    if (!entity || !translationEntityType) {
      console.log(`skip: can not find Uniform entity: ${translationEntityType} (${payloadObject.entity.id})`);

      res.status(400).json('error: can not find Uniform entity');
      return;
    }

    ...
  }

  ...
}
```

_For the US region:_

```ts
import { CompositionManagementClient, EntryManagementClient } from '@uniformdev/canvas';

const resolveUniformEntityFromWebhook = async (
  payload: WorkflowTransitionPayload
): Promise<{
  translationEntityType: TranslationPayload['metadata']['entityType'];
  entity?: RootComponentInstance | EntryData;
}> => {
  if (payload.entity.type === 'component') {
    const canvasClient = new CompositionManagementClient({
      apiKey: uniformApiKey,
      projectId: payload.project.id,
    });

    const compositionOrPattern = await getCompositionForTranslation({
      canvasClient,
      compositionId: payload.entity.id,
      releaseId: payload.entity.releaseId,
      state: CANVAS_DRAFT_STATE,
    });

    if (compositionOrPattern) {
      return {
        entity: compositionOrPattern.composition,
        translationEntityType: compositionOrPattern.pattern ? 'componentPattern' : 'composition',
      };
    }

    return { translationEntityType: 'composition' };
  } else {
    const contentClient = new EntryManagementClient({
      apiKey: uniformApiKey,
      projectId: payload.project.id,
    });

    // NOTE: entityType === "entry" for both entry and entry pattern
    const entry = await getEntryForTranslation({
      contentClient,
      entryId: payload.entity.id,
      releaseId: payload.entity.releaseId,
      state: CANVAS_DRAFT_STATE,
      pattern: false,
    });

    if (entry) {
      return {
        entity: entry.entry,
        translationEntityType: 'entry',
      };
    }

    const entryPattern = await getEntryForTranslation({
      contentClient,
      entryId: payload.entity.id,
      releaseId: payload.entity.releaseId,
      state: CANVAS_DRAFT_STATE,
      pattern: true,
    });

    if (entryPattern) {
      return {
        entity: entryPattern.entry,
        translationEntityType: 'entryPattern',
      };
    }

    return { translationEntityType: 'entry' };
  }
};
```

_For the EU region:_

```ts
import { CompositionManagementClient, EntryManagementClient } from '@uniformdev/canvas';

const resolveUniformEntityFromWebhook = async (
  payload: WorkflowTransitionPayload
): Promise<{
  translationEntityType: TranslationPayload['metadata']['entityType'];
  entity?: RootComponentInstance | EntryData;
}> => {
  if (payload.entity.type === 'component') {
    const canvasClient = new CompositionManagementClient({
      apiHost: 'https://eu.uniform.app',
      apiKey: uniformApiKey,
      projectId: payload.project.id,
    });

    const compositionOrPattern = await getCompositionForTranslation({
      canvasClient,
      compositionId: payload.entity.id,
      releaseId: payload.entity.releaseId,
      state: CANVAS_DRAFT_STATE,
    });

    if (compositionOrPattern) {
      return {
        entity: compositionOrPattern.composition,
        translationEntityType: compositionOrPattern.pattern ? 'componentPattern' : 'composition',
      };
    }

    return { translationEntityType: 'composition' };
  } else {
    const contentClient = new EntryManagementClient({
      apiHost: 'https://eu.uniform.app',
      apiKey: uniformApiKey,
      projectId: payload.project.id,
    });

    // NOTE: entityType === "entry" for both entry and entry pattern
    const entry = await getEntryForTranslation({
      contentClient,
      entryId: payload.entity.id,
      releaseId: payload.entity.releaseId,
      state: CANVAS_DRAFT_STATE,
      pattern: false,
    });

    if (entry) {
      return {
        entity: entry.entry,
        translationEntityType: 'entry',
      };
    }

    const entryPattern = await getEntryForTranslation({
      contentClient,
      entryId: payload.entity.id,
      releaseId: payload.entity.releaseId,
      state: CANVAS_DRAFT_STATE,
      pattern: true,
    });

    if (entryPattern) {
      return {
        entity: entryPattern.entry,
        translationEntityType: 'entryPattern',
      };
    }

    return { translationEntityType: 'entry' };
  }
};
```

### Build translation payload for each target locale

```ts
import { WorkflowTransitionPayload } from '@uniformdev/webhooks';

const uniformSourceLocale = process.env.UNIFORM_SOURCE_LOCALE

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  ...

  const translationPayloads: { payload: TranslationPayload; fileUri: string; targetLocale: string }[] = [];
  for (const uniformLanguage of Object.keys(uniformMappedLocales)) {
    if (uniformLanguage === uniformSourceLocale) {
      continue;
    }

    const {
      payload: translationPayload,
      errorKind,
      errorText,
    } = collectTranslationPayload({
      uniformProjectId: payloadObject.project.id,
      uniformSourceLocale: uniformSourceLocale,
      uniformTargetLocale: uniformLanguage,
      uniformReleaseId: payloadObject.entity.releaseId,
      targetLang: uniformMappedLocales[uniformLanguage],
      entity: entity,
      entityType: translationEntityType,
    });

    if (translationPayload) {
      const hasContentToTranslate = Object.keys(translationPayload.components).length > 0;

      if (hasContentToTranslate) {
        console.log(`translation for ${uniformLanguage} is ready`);
        translationPayloads.push({
          payload: translationPayload,
          fileUri: `${getJobNamePrefix({
            entityId: payloadObject.entity.id,
            entityType: payloadObject.entity.type,
            slug: payloadObject.entity.name,
            projectId: payloadObject.project.id,
          })}__${uniformLanguage}__${new Date().toISOString()}.json`,
          targetLocale: uniformMappedLocales[uniformLanguage],
        });
      } else {
        console.warn(`nothing to translate for ${uniformMappedLocales[uniformLanguage]} locale`);
      }
    } else {
      console.error(`error: ${errorKind} - ${errorText}`);
    }
  }

  ...
}
```

### Send to Smartling for translation

We are using [Smartling Batch V2 flow](https://help.smartling.com/hc/en-us/articles/360007812713-API-Flow-Example#job-batches-v2)

```ts
import {
  SmartlingJobsApi,
  SmartlingJobBatchesApi,
  CreateJobParameters,
  CreateBatchParameters,
  UploadBatchFileParameters,
} from 'smartling-api-sdk-nodejs';

// This be Smarting webhook url, that you've configured in Smartling Integration Settings before
const uniformSmartlingWebhookCallbackUrl = process.env.SMARTLING_WEBHOOK_URL;

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  ...

  const jobClient = apiBuilder.build(SmartlingJobsApi);

  const params = new CreateJobParameters({
    jobName: `${getJobNamePrefix({
      entityId: payloadObject.entity.id,
      entityType: payloadObject.entity.type,
      slug: payloadObject.entity.name,
      projectId: payloadObject.project.id,
    })}__${new Date().toISOString()}`,
    targetLocaleIds: Object.values(uniformMappedLocales),
    callbackUrl: uniformSmartlingWebhookCallbackUrl,
    callbackMethod: 'GET',
  });

  const createdJob = await jobClient.createJob(smartlingProjectId, params);

  console.log(`Translation job was created: ${createdJob.translationJobUid}`);

  if (!createdJob) {
    throw 'Failed to create job';
  }

  const jobBatchesClient = apiBuilder.build(SmartlingJobBatchesApi);
  const batchParams = new CreateBatchParameters({
    translationJobUid: createdJob.translationJobUid,
    authorize: 'true',
  });
  for (const { fileUri } of translationPayloads) {
    batchParams.addFileUri(fileUri);
  }
  const batch = await jobBatchesClient.createBatch(smartlingProjectId, batchParams);
  console.log(`Batch was created: ${batch.batchUid}`);
  if (!batch) {
    throw 'Failed to create batch';
  }

  const filesUploadClient = apiBuilder.build(SmartlingJobBatchesApi);

  for (const { payload, fileUri, targetLocale } of translationPayloads) {
    const filePath = path.join(os.tmpdir(), `translation-${v4()}.json`);

    fs.writeFileSync(filePath, JSON.stringify({ ...smartlingJsonPayloadSettings, ...payload }));

    const fileUploadParameters = new UploadBatchFileParameters({
      fileUri,
      fileType: 'json',
      file: fs.createReadStream(filePath),
      authorize: 'true',
      localeIdsToAuthorize: [targetLocale],
    });

    const success = await filesUploadClient.uploadBatchFile(
      smartlingProjectId,
      batch.batchUid,
      fileUploadParameters
    );

    if (success) {
      console.log(`File ${fileUri} was uploaded`);
    }
  }

  res.status(200).json('ok');
}

// If you want to see status of jobs in Smartling Integration UI, you have to keep this job name prefix format
export const getJobNamePrefix = ({
  entityId,
  entityType,
  slug,
  projectId,
}: {
  entityId: string;
  entityType: string;
  slug: string;
  projectId: string;
}) => {
  return `${slug}-${entityType}-${entityId}-${projectId}`;
};

const smartlingJsonPayloadSettings = {
  smartling: {
    translate_paths: [
      {
        path: '*/locales/target',
        key: '{*}/locales/target',
      },
      {
        path: '*/locales/target/xml',
        key: '{*}/locales/target/xml',
      },
    ],
    placeholder_format_custom: ['\\$\\{[^\\}]+\\}'],
    variants_enabled: 'true',
  },
};

```

> **Make sure your Smartling webhook is workflow aware:**
>
> You can find it inside `ensureWorkflowStage` function [here](https://github.com/uniformdev/examples/blob/main/examples/smartling-webhooks/src/pages/api/smartling-job-completed-webhook.ts#L158)
>
> It is resposible for automated transition from **Ready for translation** to **Translated**
