AWS Amplify Gen2とAmazon Translateを使ってリアルタイム翻訳機能を作ってみた

# はじめに

こんにちは、ほっそーです。
この度、ANGEL Calendarのアドベントカレンダー企画に参加しております！
今日は、7日目の記事になります！下記のOrganizationアカウントから、記事一覧をご覧になってください！

https://qiita.com/organizations/2024-angel-dojo

# 作成した成果物はこちらです！

<iframe width="560" height="315" src="https://www.youtube.com/embed/lBHaUbtwmwE?si=-GZDXA_nPSA6QZnP" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

今回は、「 **AWS Amplify Gen2とAmazon Translateをさわってみたい!!** 」ということで、リアルタイム翻訳機能を持つReactアプリケーションを作成してみました！「言語を選択して、テキストを入力し翻訳テキストを表示する」という、一般的な翻訳サービスと同じようなものになります！

# AWS Amplify Gen2 とは？

ANGEL Calendar 6日目の下記の記事で、完璧に解説いただいているので、そちらを是非、読んでいただければと思います！Amplify Gen2をさわってみたいという方にとっては、最高の記事だと思います!!

https://qiita.com/hohe096/items/94caf06a39ba80db2a3e

# Amzon Translateとは？

>　Amazon Translate は、多様なグローバルユーザー向けにコンテンツをローカライズし、大量のテキストを翻訳および解析して、ユーザー間のクロスリンガルなコミュニケーションを活性化させることができます。

https://aws.amazon.com/jp/translate/

サービス名から連想できるかと思いますが、翻訳サービスですね。文章やウェブサイトを瞬時に他の言語に翻訳してくれます。2024年9月現在75の言語間の翻訳をサポートしているようです。詳細は下記リンクをご覧ください。

https://docs.aws.amazon.com/translate/latest/dg/what-is.html#language-pairs

### AWS Amplify Gen2とAmzon Translateは統合できるの？

実が結構簡単にできるようです！AmplifyはAWSが提供するAI/MLのクラウドサービスをサポートしており、アプリケーションに統合することができます。
Amazon Translate、Amazon Polly、Amazon Transcribe、Amazon Rekognition、Amazon Textract等のサービスを使って、下記のユースケース等を実装することが可能です。

1. テキストを音声に変換する
1. 音声をテキストに書き起こす
1. 言語を翻訳する
1. テキストを識別する
1. 画像からエンティティを識別する
1. 画像内のオブジェクトにラベルをつける
1. テキストを解釈する

今回はこの中にある、「 **言語を翻訳する** 」に挑戦してみました！

https://docs.amplify.aws/react/build-a-backend/add-aws-services/predictions/translate/


# 開発環境
- node.js: v22.7.0
- npm: 10.8.2
- AWS CLI: 2.17.18 (AWSの認証情報はプロファイルに設定済み)

※ Amplify gen2のサンドボックス機能を使用して開発を進めます。

# 実装手順
1. Reactプロジェクトを作成する
1. Backendを実装する
    1. Predictionsを設定して翻訳機能(Amazon Translate)を追加する
1. Frontendを実装する
    1. Backendで定義したPredictionsを読み込む
    1. 翻訳機能を呼び出すカスタムフックを実装する
    1. 翻訳画面を実装する
1. サンドボックス機能で動作確認する


## Reactプロジェクトを作成する

まずは、プロジェクトのセットアップを行います。
今回はAWSが提供している下記サンプルレポジトリをクローンして、その環境に翻訳機能を実装する方針で進めます。サンプルレポジトリにはToDo機能がすでに実装されています。

https://github.com/aws-samples/amplify-vite-react-template

```
git clone git@github.com:aws-samples/amplify-vite-react-template.git
cd amplify-vite-react-template && npm install
```

次にサンドボックス環境を起動してみます。

```
npx ampx sandbox
```

別タブでReactアプリケーションを起動します。
```
npm run dev
```

ToDoアプリの画面にアクセスできることが確認できました！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639705/a41e5ef8-c09b-5b96-6d97-8d89b1e3fadf.png)


## Backendを実装する

Backendでは、既存ディレクトリにある、`amplify/backend.ts`に変更を加えていきます！

### Predictionsを設定して翻訳機能(Amazon Translate)を追加する

`amplify/backend.ts`は、ファイル名の通りBackendリソースを定義するファイルです。

実装することは下記の通りです。
- 認証されたユーザーにAmazon Translateの使用権限を付与する
- フロントエンドへの連携としてカスタムリソースを定義する

```typescript:amplify/backend.ts
import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
import { data } from './data/resource';
// ----------　下記追加する ----------
import { Stack } from "aws-cdk-lib"; 
import { PolicyStatement } from "aws-cdk-lib/aws-iam";
// ----------　ここまで ----------


const backend = defineBackend({ // defineBackend関数を使用してバックエンドを定義する
  auth,
  data,
});

// ----------　下記を追加する ----------

// 認証されたユーザーにAmazon Translateの使用権限を付与する
backend.auth.resources.authenticatedUserIamRole.addToPrincipalPolicy( 
  new PolicyStatement({
    actions: [
      "translate:TranslateText",
    ],
    resources: ["*"],
  })
);

// フロントエンドへの連携としてカスタムリソースを定義する
// amplify_outputs.jsonにLambda関数名（物理名）を出力することでフロントからAmazon TranslateのAPIを呼び出せる

backend.addOutput({
  custom: {
    Predictions: {
      convert: {
        translateText: {
          defaults: {
            sourceLanguage: "ja",
            targetLanguage: "en",
          },
          proxy: false,
          region: Stack.of(backend.auth.resources.authenticatedUserIamRole)
            .region,
        },
      },
    },
  },
});
// ----------　ここまで ----------
```

参考

https://docs.amplify.aws/react/build-a-backend/add-aws-services/predictions/set-up-predictions/

## Frontendを実装する

次にFrontendを実装していきます！
普段の業務ではReact/TypeScriptといったフロントエンドを担当しているわけではございませんので、最適化できていない実装箇所も多くあるかと思いますが、ご了承ください:bow_tone3:

既存の構成をベースにしつつ、下記のディレクトリ構成で実装を進めていきます。

```
src
├── App.css
├── App.tsx
├── assets
│   └── react.svg
├── components //追加する
│   ├── LanguageSelector.tsx 
│   └── TranslateComponent.tsx　
├── hooks //追加する
│   └── translate.tsx 
├── index.css
├── main.tsx
├── utils　//追加する
│   └── language-option.ts
└── vite-env.d.ts
```

### Backendで定義したPredictionsを読み込む

実装したいことは下記の通りです。
- カスタムリソースであるPredictionsカテゴリを指定して、フロント側で翻訳機能を参照できるようにする

```typescript: src/main.ts(抜粋)
Amplify.configure(outputs);
// ----------　下記を追加する ----------
Amplify.configure({
  ...Amplify.getConfig(),
  Predictions: outputs.custom.Predictions,
});
// ----------　ここまで ----------
ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
```

### 翻訳機能を呼び出すカスタムフックを実装する

実装を進めるため、Amplify提供のPredictions用のライブラリを追加します。
```
npm add --save-dev @aws-amplify/predictions
```

次に、今回のメインであるリアルタイム翻訳機能のカスタムフック実装をします。
カスタムフックとして実装することで、他のコンポーネントでも再利用可能にします。

実現したいことは下記の通りです。
- 翻訳結果のステートを管理する
- 入力テキストを検証する
- Amazon Translateに翻訳リクエストを実行する
- エラーハンドリングを作成する

```typescript: src/hooks/translate.tsx
import { useCallback, useState } from "react";
import { Predictions } from '@aws-amplify/predictions';

// カスタムフックを定義
const useTranslateLanguage = () => {
  // 翻訳結果を保持するためのステートを定義
  const [translatedText, setTranslatedText] = useState("");

  // 翻訳を実行する関数を定義
  const translateLanguage = useCallback(
    async (
      sourceText: string, // 翻訳元のテキスト
      sourceLanguage: string, // 翻訳元の言語
      targetLanguage: string // 翻訳先の言語
    ) => {
      // 入力テキストを検証
      // 空白のみの場合、翻訳結果をクリアして終了する
      if (!sourceText.trim().replace(/\r?\n/g, "")) {
        setTranslatedText("");
        return;
      }
      try {
        // AWS Amplify Predictionsを使用してテキストを翻訳
        const result = await Predictions.convert({
          translateText: {
            source: {
              text: sourceText, 
              language: sourceLanguage,
            },
            targetLanguage: targetLanguage,
          },
        });
        // 翻訳結果をステートに設定
        setTranslatedText(result.text);
      } catch (error) {
        console.error(error);
      }
    },
    [] 
  );

  // 翻訳関数と翻訳結果を返す
  return { translateLanguage, translatedText };
};

export default useTranslateLanguage;

```

参考

https://zenn.dev/zuma_lab/articles/nextjs-amplify-text-to-translate


### リアルタイム翻訳画面を実装する

ここからは実際にブラウザに表示させる画面を実装していきます。

App.tsxはメインページとなります。ページルーティングは実装しないため、ルートパスでアクセスした場合に表示される画面です。

このファイルで実装したいことは下記の通りです。
- 認証機能を持つ画面を表示する
- 認証後にメインコンテンツ(翻訳機能)を表示する
    - 翻訳コンポーネントを表示する
    - クリック時にサインアウトを実行するサインアウトボタンを表示する

※既存ファイルを修正します。

```typescript: App.tsx
import { Authenticator } from '@aws-amplify/ui-react'
import '@aws-amplify/ui-react/styles.css'
import TranslateComponent from './components/TranslateComponent';

function App() {

  return (
    // Authenticatorコンポーネントでアプリ全体をラップし、認証を提供
    <Authenticator>
      {({ signOut }) => (
    <main>
      <div>
      {/* 翻訳コンポーネントを表示 */}
        <TranslateComponent />
      </div>
      {/* サインアウトボタンを表示し、クリック時にサインアウトを実行 */}
      <button onClick={signOut}>Sign out</button>
    </main>   
      )}
      </Authenticator>
  );
}

export default App;
```

次に`TranslateComponent.txt`ファイルを作成します。
このファイルで実装したいことは下記の通りです。

- UIコンポーネントを作成する
    - ソース言語・ターゲット言語の選択コンポーネントを表示する
    - テキスト入力フィールドを表示する(AmplifyのUIライブラリを活用する)
- ステートを管理する
    - コンポーネント表示に必要な値の状態を管理する
- ハンドラーを実装する
    - 入力テキストやターゲット言語の変更を検知し、翻訳機能のカスタムフックを実行する
- 翻訳結果を表示
    - 翻訳機能によって返されたテキスト結果を表示する

```typescript:src/components/TranslateComponent.tsx
import React, { useState, useEffect } from 'react';
import { TextAreaField } from '@aws-amplify/ui-react';
import LanguageSelector from './LanguageSelector';
import useTranslateLanguage from '../hooks/translate';

// ステートを管理する
const TranslateComponent: React.FC = () => {
  const [inputText, setInputText] = useState('');
  const [sourceLanguage, setSourceLanguage] = useState('en');
  const [targetLanguage, setTargetLanguage] = useState('es');
  // カスタムフックから翻訳関数と翻訳結果を取得
  const { translateLanguage, translatedText } = useTranslateLanguage();

  // 入力テキストが変更されたときのハンドラー
  const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    const text = e.target.value;
    setInputText(text);
    translateLanguage(text, sourceLanguage, targetLanguage);
  };

  // ターゲット言語が変更されたときのハンドラー
  const handleTargetLanguageChange = (newTargetLanguage: string) => {
    setTargetLanguage(newTargetLanguage);
    translateLanguage(inputText, sourceLanguage, newTargetLanguage);
  };

  return (
    <div>
      <h2>リアルタイム翻訳ツール</h2>
      <div style={{ display: 'flex', gap: '1rem', marginBottom: '1rem' }}>
        {/* ソース言語の選択コンポーネント */}
        <LanguageSelector
          selectedLanguage={sourceLanguage}
          onLanguageChange={setSourceLanguage}
          label="ソース言語"
        />
        {/* ターゲット言語の選択コンポーネント */}
        <LanguageSelector
          selectedLanguage={targetLanguage}
          onLanguageChange={handleTargetLanguageChange}
          label="ターゲット言語"
        />
      </div>
      <div style={{ display: 'flex', gap: '1rem', marginBottom: '1rem' }}>
        {/* 入力テキスト用のテキストエリア */}
        <TextAreaField
          value={inputText}
          onChange={handleInputChange}
          placeholder="テキストを入力"
          label="入力テキスト"
          rows={10}
          style={{ flex: 1 }}
        />
        {/* 翻訳結果用のテキストエリア */}
        <TextAreaField
          value={translatedText}
          readOnly
          placeholder="翻訳結果"
          label="翻訳結果"
          rows={10}
          style={{ flex: 1 }}
        />
      </div>
    </div>
  );
};

export default TranslateComponent;
```

次に`LanguageSelector`ファイルを作成します。
このファイルで実装したいことは下記の通りです。
- UIコンポーネントを作成する
    - 言語を選択できる
    - 言語オプションファイルを参照し、選択肢を表示する
- 言語変更時にコールバック関数を実行する

```TypeScript:src/components/LanguageSelector.tsx
import React from 'react';
import { SelectField } from '@aws-amplify/ui-react';
import { languageOptions } from '../utils/language-option';

// LanguageSelectorコンポーネントのプロパティの型を定義
interface LanguageSelectorProps {
  selectedLanguage: string; // 現在選択されている言語コード
  onLanguageChange: (language: string) => void; // 言語変更時に呼び出される関数
  label: string; // セレクトフィールドのラベル
}

// LanguageSelectorコンポーネントを定義
const LanguageSelector: React.FC<LanguageSelectorProps> = ({ selectedLanguage, onLanguageChange, label }) => {
  return (
    <div style={{ flex: 1 }}>
      {/* 言語を選択するためのセレクトフィールド */}
      <SelectField
        value={selectedLanguage} // 現在選択されている言語コードを設定
        onChange={(e) => onLanguageChange(e.target.value)} // 言語が変更されたときに呼び出される関数を設定
        label={label} // セレクトフィールドのラベルを設定
      >
        {/* 言語オプションを動的に生成 */}
        {languageOptions.map((language) => (
          <option key={language.code} value={language.code}>
            {language.label} {/* 言語のラベルを表示 */}
          </option>
        ))}
      </SelectField>
    </div>
  );
};

export default LanguageSelector;

```

`language-option.ts`ファイルを作成します。
Amazon Translateでサポートされている言語の中からアプリに必要な言語をこちらのファイルで管理します。

``` typeScript: src/utils/language-option.ts
export const languageOptions = [
    { label: "中国語", code: "zh" },
    { label: "English", code: "en" },
    { label: "French", code: "fr" },
    { label: "German", code: "de" },
    { label: "Hindi", code: "hi" },
    { label: "Italian", code: "it" },
    { label: "日本語", code: "ja" },
    { label: "韓国語", code: "ko" },
    { label: "Spanish", code: "es" },
  ];
```

### エラー(global is not defined)の解消
これまでの実装を進めたのち、下記エラーで画面が表示されませんでした。

```
Uncaught ReferenceError: global is not defined
```
Node.js上で実行しようとしたところ”global”オブジェクトがないというのが原因でしたので、`vite.config.ts`ファイルに下記を追記することで解消されました。

```typescript: vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  // ----------　下記を追加する ----------
  define: {
    global: {},
  }
  // ----------　ここまで ----------
})
```

参考:

https://almonta2021blog.com/react-vite-global-error/#google_vignette



## サンドボックス機能を使って動作確認する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639705/b80c51e1-e97b-2567-02e0-894cbfcbbc57.png)
認証画面が最初に表示されていますね！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639705/269b6030-a909-cf2c-b9ad-84a8e29be19d.png)

ログイン後、メインコンテンツが表示されていますね！
リアルタイム翻訳も想定通り機能しており、ターゲット言語を変更時も反映されること確認できました！


# 最後に

今回、AWS Amplify Gen2とAmazon Translateを使ってリアルタイム翻訳機能を作ってみました。思った以上に、AmplifyとAmazon Translateを統合が簡単にできたので、驚きました！(どちらかというとReact/TypeScriptのキャッチアアップの方が大変でした・・:joy:)
普段業務でさわっていない技術でしたが、手を動かして作ってみるとやっぱり楽しいなと感じました。
ご興味のある方に、参考になれば幸いです！！