Configuring webhook
Setup webhook to automatically import Phrase translations back into Uniform.
You would need Phrase integration to be installed and configured.
You can find a full example of implementation in this examples repo
Setup#
Lets create a webhook that can handle composition or entry translations coming back from Phrase TMS.
First we need Phrase and Uniform credentials:
# Authentication to Phrase # https://cloud.memsource.com or https://us.cloud.memsource.com PHRASE_API_HOST= PHRASE_PROJECT_UID= PHRASE_USER_NAME= PHRASE_USER_PASSWORD= PHRASE_WEBHOOK_SECRET_TOKEN= # Authentication to Uniform UNIFORM_API_KEY=
Uniform API Keys
Here you can find how to create Uniform API Keys. Phrase integration requires at least Create and Update permissions for Compositions / Entries / Assets
We would also need Uniform dependencies
npm i @uniformdev/canvas @uniformdev/tms-sdk @uniformdev/tms-phraseLets validate Phrase webhook
const token = req.headers['x-memsource-token']; if (token !== process.env.PHRASE_WEBHOOK_SECRET_TOKEN) { // eslint-disable-next-line no-console console.log('Secret token is invalid'); res.status(403).json({ message: 'Secret token is invalid' }); return; } const payload = req.body as PhraseWebhookPayload; if (payload.event !== 'JOB_STATUS_CHANGED') { // eslint-disable-next-line no-console console.log(`skip: event !== 'JOB_STATUS_CHANGED'`); res.status(200).json({ updated: false }); return; } const job = payload.jobParts.at(0); if (!job || !shouldProcessJob(job)) { res.status(200).json({ updated: false }); return; } const shouldProcessJob = (job: PhraseWebhookPayload['jobParts'][number]): boolean => { if (job.status !== 'COMPLETED_BY_LINGUIST') { // eslint-disable-next-line no-console console.log('skip: job status !== COMPLETED_BY_LINGUIST'); return false; } const phraseProjectUid = job.project.uid; if (!process.env.PHRASE_PROJECT_UID || phraseProjectUid !== process.env.PHRASE_PROJECT_UID) { if (!process.env.PHRASE_PROJECT_UID) { // eslint-disable-next-line no-console console.log(`skip: missing 'PHRASE_PROJECT_UID' env`); } else { // eslint-disable-next-line no-console console.log( `skip: Phrase project id mismatch (expected: ${process.env.PHRASE_PROJECT_UID}, current: ${phraseProjectUid})` ); } return false; } return true; };Lets retrieve target file from the job
const phraseClient = new PhraseTmsClient({ apiHost: process.env.PHRASE_API_HOST || assert('missing PHRASE_API_HOST'), userName: process.env.PHRASE_USER_NAME || assert('missing PHRASE_USER_NAME'), password: process.env.PHRASE_USER_PASSWORD || assert('missing PHRASE_USER_PASSWORD'), }); const translationPayload = job.uid ? await phraseClient.downloadTargetFile<TranslationPayload>({ projectUid: job.project.uid, jobUid: job.uid, }) : null; if (!translationPayload) { // eslint-disable-next-line no-console console.log('skip: no translation payload'); res.status(200).json({ updated: false }); return; } function assert(msg: string): never { throw new Error(msg); }Now lets merge translation into Uniform entities
const uniformProjectId = translationPayload.metadata.uniformProjectId; const uniformReleaseId = translationPayload.metadata.uniformReleaseId; const uniformEntityType = translationPayload.metadata.entityType; const uniformEntityId = translationPayload.metadata.entity.id; const canvasClient = new CanvasClient({ projectId: uniformProjectId, apiKey: process.env.UNIFORM_API_KEY || assert('missing UNIFORM_API_KEY'), bypassCache: true, }); const contentClient = new ContentClient({ projectId: uniformProjectId, apiKey: process.env.UNIFORM_API_KEY || assert('missing UNIFORM_API_KEY'), bypassCache: true, }); const { translationMerged } = await mergeTranslationToUniform({ canvasClient, contentClient, translationPayload, updateComposition: async ({ canvasClient, composition }) => { // eslint-disable-next-line no-console console.log('update composition: start'); await canvasClient.updateComposition(composition); // eslint-disable-next-line no-console console.log('update composition: done'); return true; }, updateEntry: async ({ contentClient, entry }) => { // eslint-disable-next-line no-console console.log('update entry: start'); await contentClient.upsertEntry(entry); // eslint-disable-next-line no-console console.log('update entry: done'); return true; }, onNotFound: ({ translationPayload }) => { const entityType = translationPayload.metadata.entityType; const entityId = translationPayload.metadata.entity.id; // eslint-disable-next-line no-console console.log(`skip: can not find ${entityType} (${entityId})`); }, onNotTranslatedResult: ({ updated, errorKind, errorText }) => { if (errorKind !== undefined) { // eslint-disable-next-line no-console console.warn(errorText || 'Unknown error'); } else if (!updated) { // eslint-disable-next-line no-console console.log('Translation has no updates'); } }, });If everything goes fine - lets mark Phrase job as delivered:
if (translationMerged) { // eslint-disable-next-line no-console console.log('update job status: start'); await phraseClient.setJobStatus({ projectUid: job.project.uid, jobUid: job.uid, jobStatus: 'DELIVERED', }); // eslint-disable-next-line no-console console.log('update job status: done'); }
Register Webhook in Phrase TMS Dashboard#
- Open Phrase TMS dashdoard
- Go to Settings and click WebhooksPhrase webhook settings location.
- Click Add webhook
- Define webjook's name
- Provide publicly avaiable url where Phrase is going to push job status notifications
- Define webhook's secret token to validate income requests
- Select Job status changed eventCreate Phrase webhook.