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 .
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 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 .
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 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.
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
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
After you've created webhook please copy Signing Secret and paste it into SVIX_SECRET
env variable
You can read more about webhooks here
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 payloadsvix
- for verification of webhook payload (read more here )@uniformdev/tms-sdk
- sdk for preparing Uniform content for translationsFollowing code examples are based on NextJS API handlers
Payload verification# 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;
}
...
}
CopyCopied! Prepare Smartling SDK# 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);
...
}
CopyCopied! Get effected entity# 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;
}
...
}
...
}
const resolveUniformEntityFromWebhook = async (
payload: WorkflowTransitionPayload
): Promise<{
translationEntityType: TranslationPayload['metadata']['entityType'];
entity?: RootComponentInstance | EntryData;
}> => {
if (payload.entity.type === 'component') {
const canvasClient = new CanvasClient({
apiKey: uniformApiKey,
projectId: payload.project.id,
apiHost: uniformApiHost,
bypassCache: true,
});
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 ContentClient({
apiKey: uniformApiKey,
projectId: payload.project.id,
apiHost: uniformApiHost,
bypassCache: true,
});
// 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' };
}
};
CopyCopied! Build translation payload for each target locale# 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}`);
}
}
...
}
CopyCopied! Send to Smartling for translation# We are using Smartling Batch V2 flow
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',
},
};
CopyCopied! Make sure your Smartling webhook is workflow aware
You can find it inside ensureWorkflowStage
function here
It is resposible for automated transition from Ready for translation to Translated