当サイトを最適な状態で閲覧していただくにはブラウザのJavaScriptを有効にしてご利用下さい。
JavaScriptを無効のままご覧いただいた場合には一部機能がご利用頂けない場合や正しい情報を取得できない場合がございます。
承知しました
本サイトではWebサイトのエクスペリエンスを向上させるために、Cookieを使用しています。Cookieはブラウザの設定から無効にできます。本サイトで使用するCookieについては、プライバシーポリシーをご確認ください。

Blog

ブログ

開発者向け

Webhookを使用してコンテンツを自動的に翻訳する方法

By Eric Dugre  

多言語サイトでは、コンテンツをすべての言語に翻訳し続けることが常に課題でした。最近のwebhookの更新は、その課題を自動的に解決し、代わりにコーヒーブレイクを可能にするのにどのように役立ちますか?

Webhook 101

物事が自動的に機能するのはいいですね。確かに、その新しいKontentの記事を自分でスペイン語に翻訳することはできますが、それができるのであれば、なぜ時間を無駄にするのでしょうか。そこで、強力なWebhook機能が登場します。コンテンツアイテムを変更すると、システムは提供されたURLに通知を送信します。これは通常、AzureまたはAmazonでホストされているサーバーレス機能、または変換を実行するサードパーティのエンドポイントです。この例では、 MicrosoftのTranslator Text CognitiveServiceを使用して翻訳します。

Kentico Kontent

プロジェクトの設定

この例では、 Javascript SDKExpressJSを使用して、Webhookを使用するアプリケーションを作成します。 ここで説明するように、ジェネレーターを使用して基本的なExpressアプリケーションを作成することから始めましょう:

 mkdir webhook-appcd webhook-appnpx express-generatornpm i

`npm start`を使用してアプリケーションを実行する場合は、 http:// localhost:3000 /からアクセスできるはずです。次に、KontentがWebhookをPOSTするエンドポイントを設定する必要があります。 app.jsで、新しい '/ webhook'ルートを追加し、単に200の応答を返します。

 app.post('/webhook', (req, res) => { res.status(200).send('Success');});

注:このGitHubリポジトリにある上記のすべてのコードファイルにアクセスすることもできます。

インターネットからローカルサーバーにアクセスする

コードを追加する前に、/ webhookルートをテストして、Kontentがエンドポイントに到達できることを確認する必要があります。ローカルアプリケーションを一般公開するために、 ngrokを使用できます。登録は無料です。 次に、ここの手順に従って、PCで実行します。手順4で、ローカルアプリケーションが実行されているポートを使用します。

 ./ngrok http 3000

成功すると、パブリックURLを含む情報を表示するメッセージが表示されます。

Kentico Kontent

KontentのWebhook構成でこのURLを使用します。

まず、ワークフローを設定する必要があります。特定のワークフローステップに到達したときに言語バリアントを自動的に翻訳したいので、翻訳ステップを追加しましょう。

Kentico Kontent

次に、[プロジェクト設定]> [Webhook]に移動して、新しいWebhookを作成し、「自動翻訳」と呼びます。既存のトリガーを削除し、「監視するコンテンツアイテムのワークフローステップ」の下のドロップダウンメニューから翻訳ステップを選択します。 ngrokからURLをコピーし、最後に/ webookルートを付けてURLアドレスフィールドに追加します。

Kentico Kontent

テストする準備ができました! Expressアプリケーションとngrokの両方が実行されていることを確認し、コンテンツアイテムをレビューステップから翻訳に移動します。数秒以内に、ngrokが/ webhookへのPOSTの成功を報告し、KontentのWebhookの横にあるドットが緑色に変わり、機能していることを示します。

Kentico Kontent

Webhookの検証

アプリケーションでPOSTを正しく受信したので、署名を確認して何かを実行します。すべてのWebhook通知には、リクエストがKontentから送信されていることを確認するための署名があります。署名は、Webhookシークレットとリクエストの本文のハッシュです。署名の詳細についてはこちらをご覧ください。Javascriptで署名を検証する例についてはこちらをご覧ください。

サンプルコードに記載されているように、 body-parserを使用して生のJSONデータを解析する必要があります。 body-parserをインストールします。

 npm i body-parser

次に、app.jsでJSONデータを解析するときに使用するようにExpressに指示します。

 const bodyParser = require('body-parser');//app.use(express.json()); // original code added automatically to Express- comment or remove this lineapp.use(bodyParser.raw({type:'application/json'})) // add this line

これで、POSTリクエストのヘッダーから署名を取得し、署名が有効かどうかを確認できます。 Kontentで見つかったWebhookシークレットをconstとしてapp.jsファイルに保存するか、 環境変数を使用する必要があります。署名を検証するための新しい関数を作成します。

 const crypto = require('crypto');const webhookSecret = 'EKAPGD8JrgjtqrAXqK2C1hTT0/JnHDf/5bziRnHtstM=';const hasValidSignature = (body, signature) => { const computedSignature = crypto.createHmac('sha256', webhookSecret) .update(body) .digest(); return crypto.timingSafeEqual(Buffer.from(signature, 'base64'), computedSignature);}

/ webhookルートで、req.headersオブジェクトの本体と署名を渡してhasValidSignature関数を呼び出します。

 app.post('/webhook', (req, res) => { if(hasValidSignature(req.body, req.headers['x-kc-signature'])) { res.status(200).send('Success'); } else { res.status(403).send('Invalid signature'); }});

Webhookを再度テストして、200応答が正常に受信されることを確認します。 Webhookが適切に検証されたら、KontentがWebhook通知の本文で送信したデータの読み取りに進むことができます。

Webhookの処理

Javascript SDKのコンテンツ管理APIを使用して、Kontentからデータを取得し、翻訳された言語バリアントを更新します。このセクションを開始する前に、CM APIクライアントをインストールし、app.jsファイルでクライアントをインスタンス化するための手順に従ってください。

 const cmClient = new ContentManagementClient({ projectId: 'your-project-ID', apiKey: 'your-CM-API-key'});

Webhook通知で提供された情報を見て、バリアントを翻訳するために何が必要かを判断しましょう。 ワークフローWebhook通知の本文は、以前の非ワークフローWebhookとは異なります。次のようになります。

 'data': { 'items': [ { 'item': { 'id': '65f05e0f-40c3-436b-a641-e2d4cae16e46' }, 'language': { 'id': '00000000-0000-0000-0000-000000000000' }, 'transition_from': { 'id': 'eee6db3b-545a-4785-8e86-e3772c8756f9' }, 'transition_to': { 'id': '03b6ebd3-2f49-4621-92fd-4977b33681d1' } } ]}

アイテムは、翻訳ワークフローステップに移動した言語バリアントのIDです。このWebhookはそのワークフローステップに対してのみトリガーされるため、 transition_fromまたはtransition_toの値について心配する必要はありません。この言語バリアントを他の言語に完全に翻訳するには、いくつかの異なる情報が必要です。

  • 翻訳する必要のあるコンテンツタイプの要素
  • 更新された言語バリアントからのこれらの要素のテキスト
  • 翻訳する必要のある言語

これは4つのステップに分けることができます。

  1. Webhook通知のIDを使用して言語バリアントを取得します。
  2. 手順1のIDを使用して、言語バリアントのコンテンツアイテムを取得します。
  3. 手順2のIDを使用して、コンテンツアイテムのコンテンツタイプを取得します。
  4. コンテンツを翻訳するプロジェクト言語を入手してください。

このプロセスを開始するには、app.jsに新しい関数を作成してロジックを含め、/ webhookルートから呼び出します。

 app.post('/webhook', (req, res) => { if(hasValidSignature(req.body, req.headers['x-kc-signature'])) { processWebhook(JSON.parse(req.body)); //...const processWebhook = (body) => {}

プロセスのステップ1で行うContentManagement API呼び出しで使用するには、JSONデータから更新されたバリアントのIDが必要です。英語のバリアント(この例ではデフォルトの言語)のみを他の言語に翻訳したいので、言語IDも取得しましょう。

 const processWebhook = (body) => { const updatedVariantLangID = body.data.items[0].language.id const updatedVariantItemID = body.data.items[0].item.id; // Only translate variants when an English variant was updated if(updatedVariantLangID !== '00000000-0000-0000-0000-000000000000') return;}

app.jsファイルで、CMAPI呼び出しの結果を格納するいくつかのグローバル変数を宣言します。

 let updatedVariant, contentItem, contentType;

rxjsをインストールし、mergeMap()関数をロードします。この関数を使用して、後で使用するmap()とともに、4つのリクエストに対して単一のObservableを作成します。

 npm i rxjs
 const { mergeMap, map } = require('rxjs/operators');


ステップ1-Webhook通知のIDを使用して言語バリアントを取得します

これで、4ステップのプロセスで必要なデータを取得する準備が整いました。この時点ではIDしかないため、最初のステップはviewLanguageVariant()を使用して完全な言語バリアントを取得することです。

 const getLanguageVariant = cmClient .viewLanguageVariant() .byItemId(updatedVariantItemID) .byLanguageId(updatedVariantLangID) .toObservable();

これにより、バリアントの要素IDとその値、およびバリアント自体に関するいくつかのシステムデータが提供されます。どの要素を翻訳するかを決定するには、コンテンツアイテムが必要です。

ステップ2-言語バリアントのコンテンツアイテムを取得する

最初のObservableの結果を使用して、コンテンツアイテムのIDを取得し、 viewContentItem()を使用してコンテンツアイテムをリクエストできます。

 const getContentItem = (result) => { updatedVariant = result.data; return cmClient .viewContentItem() .byItemId(updatedVariant.item.id) .toObservable(); };

ステップ3-コンテンツアイテムのコンテンツタイプを取得する

コンテンツアイテムの定義を使用して、コンテンツタイプをリクエストできるようになりました。これにより、 updatedVariantオブジェクトのどの要素がtextまたはrich_text要素であるかを識別し、それらの値を変換できます。このObservableの結果を保存してから、 viewContentType()を呼び出します。

 const getContentType = (result) => { contentItem = result.data; return cmClient .viewContentType() .byTypeId(contentItem.type.id) .toObservable(); };

ステップ4-コンテンツを翻訳するプロジェクト言語を取得する

これで、プロジェクト言語を除くすべてのデータが得られました。上記のObservableの結果を保存してから、 listLanguages()を使用して言語を取得できます。

 const getLanguages = (result) => { contentType = result.data; return cmClient .listLanguages() .toObservable(); }

これで、これら4つのオブザーバブルを1つにまとめることができます。

 const obs = getLanguageVariant.pipe(mergeMap(getContentItem)).pipe(mergeMap(getContentType)).pipe(mergeMap(getLanguages));const sub = obs.subscribe(result => { sub.unsubscribe();

この最終的なObservableの結果は、言語のリストになります。言語に関する多くの情報が含まれていますが、コードネームを外部サービスに送信するだけです。プロジェクトで「en-us」などの標準コードネームを使用していない場合は、今すぐ変更してください。これにより、4文字の形式を想定している外部サービスにコードネームを簡単に送信できます。 rxjsのmap()関数を使用して、コードネームの配列を作成します。

 const sub = obs.subscribe(result => { sub.unsubscribe(); const projectLanguages = result.data.languages.map(l => l.codename);

4回目のCMAPI呼び出しの結果により、コンテンツを翻訳するために必要なすべての情報が得られました。

コンテンツの翻訳

次の行動計画は、コンテンツタイプのすべての翻訳可能な要素を取得し、言語コード名をループして、英語のバリアントから外部サービスに値を送信して翻訳することです。 filter()を使用してtext要素とrich_text要素を検索し、map()関数を使用してそれらのIDの配列を作成します。

 const sub = obs.subscribe(result => { sub.unsubscribe(); const projectLanguages = result.data.languages.map(l => l.codename); const textElementIDs = type.elements.filter(e => e.type === 'text' || e.type === 'rich_text').map(e => e.id);

翻訳が必要な要素のこのリストを使用して、すべての言語コード名をループし、次に作成する関数を呼び出して、実際の翻訳を実行し、バリアントを更新できます。

 const sub = obs.subscribe(result => { sub.unsubscribe(); const projectLanguages = result.data.languages.map(l => l.codename); const textElementIDs = type.elements.filter(e => e.type === 'text' || e.type === 'rich_text').map(e => e.id); projectLanguages.forEach(targetLangCode => { if(targetLangCode !== 'en-us') upsertLanguageVariant(targetLangCode, textElementIDs); }); });

私たちのコードは、英語以外のカルチャごとにupsertLanguageVariant()を呼び出し、翻訳する必要のある要素IDの配列を渡します。 rxjsのzip() を使用して、MicrosoftのTranslator Text APIに複数のリクエストを送信します(要素ごとに1つのリクエスト)。新しいupsertLanguageVariant関数とObservablesを保持する配列を作成し、zip()関数を登録します。

 const { zip } = require('rxjs');const upsertLanguageVariant = (targetLangCode, textElementIDs) => { const translateObservables = [];}

次に、更新された英語のバリアントのすべての要素をループします。要素のIDが翻訳可能なIDのリストにある場合、翻訳用のObservableを作成し、それを配列に追加します。

 const upsertLanguageVariant = (targetLangCode, textElementIDs) => { const translateObservables = []; updatedVariant.elements.forEach(e => { if(textElementIDs.includes(e.element.id)) { translateObservables.push( // Create Observable ); } });

MicrosoftへのRESTリクエストのObservableを作成するには、 axios-observableを使用します。インストールしてからapp.jsに登録してください

const Axios = require('axios-observable').Axios;

また、サービスを使用するには、Microsoftのキーが必要になります。 TranslatorTextサービスのAzureCognitive Servicesアカウントを作成し、その後、[キー]タブの「キー1」を環境変数またはconstとしてapp.jsに保存します。

Kentico Kontent
 const translationKey = '4e888811x03f4bd2732321683175d56b';

個々の要素の値を変換するRESTリクエストを作成する準備が整いました。 MicrosoftのAPIリファレンスによると、リッチテキスト要素がHTMLを格納するため、ソース言語のfromパラメーター、ターゲット言語のtoパラメーター、およびtextTypeを提供する必要があります。キーは「Ocp-Apim-Subscription-Key」ヘッダーで送信され、本文はモデルに準拠します。これまでの関数は次のとおりです。

 const upsertLanguageVariant = (targetLangCode, textElementIDs) => {const translateObservables = [];updatedVariant.elements.forEach(e => {if(textElementIDs.includes(e.element.id)) {translateObservables.push(Axios.request({ method: 'POST', params: { from: 'en-us', to: targetLangCode, textType: 'html' }, url: 'https://api.cognitive.microsofttranslator.com/translate?api-version=3.0', headers: { 'Ocp-Apim-Subscription-Key': translationKey, 'Content-type': 'application/json' }, data: [{ 'text': e.value }] }) .pipe(map(result => [e.element.id, result.data[0].translations[0].text])));}});}

1つのリクエストですべての要素を技術的に翻訳することはできますが、Microsoftの応答方法により、どのテキストがどの要素に属しているかを判断する方法はありません。これは、リクエストが作成された後のpipe(map())関数の目的です。個々のリクエストの結果は、次のように、要素IDと翻訳されたテキストを含む配列として保存されます。['4e9acd7a-2db8-4c33- a13a-0c368ec2f108 '、' Ahoj ']。これらの結果はすべて、zip形式のObservableによって返される単一のオブジェクトに保存されます。

Spread演算子を使用してこのObservablesの配列をzip()して、結果を取得しましょう。その結果、英語のバリアントの各要素をループして、一致する要素IDがあるかどうかを確認します。その場合、updatedVariantの値を新しい変換された値に設定できます。

 const sub = zip(...translateObservables).subscribe(result => { sub.unsubscribe(); // Set new values updatedVariant.elements.forEach(e => { const match = result.filter(arr => arr[0] == e.element.id); if(match.length > 0) { let text = match[0][1]; if(match.length > 0) e.value = text.replace(/
/g, '
'); } }); });

小さなreplace()を実行する必要があることに気付くでしょう。 Microsoftのサービスは、Kontent APIが必要とする改行を閉じないため、修正する必要があります。 updateVariants.data.elements配列には、デフォルトの英語値といくつかの翻訳された値が入力され、これらの要素を使用して新しい言語バリアントをアップサートできます。

これを実現するには、createNewVersionOfLanguageVariant()を使用して、ターゲットバリアントの新しいバージョンを作成します。その後、upsertLanguageVariant()を呼び出して、コンテンツアイテムのID、ターゲット言語、および更新した要素配列を渡します。

// Create new version then upsert data- only works for published variants!cmClient.createNewVersionOfLanguageVariant() .byItemId(contentItem.id) .byLanguageCodename(targetLangCode) .toObservable() .subscribe(result => { cmClient.upsertLanguageVariant() .byItemId(contentItem.id) .byLanguageCodename(targetLangCode) .withElements(updatedVariant.elements) .toObservable() .subscribe(result => { console.log(`language ${result.data.language.id}: ${result.debug.response.status}`); });});

この自動翻訳は、公開されているバリアントに対してのみ機能することに注意してください。これがすべてのバリアントで機能するためには、バリアントが公開されているかどうかを確認し、公開されていない場合は、バリアントをドラフトステップに移動する必要があります。この記事の最後にあるリンクを使用して、GitHubでapp.jsファイルの完全なソースコードを表示できます。

私たちが学んだこと

ふぅ!必要なデータを取得するには、いくつかのCM API呼び出しが必要でしたが、rxjsの助けを借りて、コードが整理され、効率的になりました。これは、Webhookがプロジェクトに提供する自由のほんの一例です。これらを使用して、外部統合を最新の状態に保ち、コンテンツで自動機能を実行するなど、さまざまなことができます。

Webhookの使用の他の例をご覧になりたい場合は、このすばらしい記事「Webhookを使用した廃止されたキャッシュエントリのクリア」をご覧ください。機械で自動翻訳を実行するサンプルアプリケーションを取得する手順を含む、GitHubでの自動翻訳の実装の完全なコードを表示できます。

Headless CMSの導入をお考えでしょうか?

クラウドとマルチデバイスに最適化されたKentico Kontentをお試しください