html Javascript プログラミング

DeepL APIを使って、ブログの翻訳補助ツールを作ってみた

もう機械翻訳なしにはいられない

当ブログには、英語版も用意しています。

英語版の記事数はまだ少ないのですが、増やしていきたいと思っています。

翻訳には機械翻訳の力を借りています。

最近の機械翻訳は驚くほど進歩していて、かつてのような不自然さや誤訳はずいぶんと減少しました。

完全に任せるのは難しくても、ちょっと手を入れるだけできちんと通じる英語になりつす。もう、十分に実用的なレベルに達していると思います。

機械翻訳でポピュラーなのがDeepLとGoogle翻訳です。

どちらも一長一短あるようですが、個人的にはDeepLの自然な翻訳が好みです。

誤訳を防ぐには日本語に戻すのが一番

機械翻訳が進歩したと言っても、まだまだ完全とは言えません。

日本語特有の表現、特に主語の省略や複雑な修飾構造は機械翻訳泣かせだと思います。

適当な主語を与えられたり、修飾語のかかる部分が誤解されたりすることがあります。

私自身の英語力では、英文だけを見て「これは本当に適切な翻訳か?」という判断が難しいときがあります。

そんなときに役立つのが、英語の文章を再度機械翻訳を使って日本語に戻す方法です。

原文と逆翻訳した日本語を比較し、翻訳が適切であるかを確認することができます。

間違いを見つけた場合には、元の日本語か英語のどちらかを直して再度翻訳を行います。

日本語→英語→日本語の翻訳を一回の操作でやってくれると楽

しかし、この日本語→英語→日本語の翻訳をDeepLの通常の画面で行うと少々面倒です。

日本語から英語に翻訳した後で、再度、言語設定を変更してから日本語に戻す作業が必要となります。

幸い、DeepLにはAPIが提供されていて、自作のアプリケーションからDeepLの翻訳機能を利用することができます。

そこでDeepL APIを用いて日本語から英語へ、そして再び日本語へという一連の翻訳を簡単に行えるツールを作成しました。

DeepL APIはとても簡単

DeepL APIを使う場合には、通常の翻訳アカウントを持っている人でも、追加の登録が必要です。

DeepL API

私の場合は既存のアカウントにDeepL APIのプランを追加するような感じでした。

2023年6月現在、無料のDeepL API Freeプランで1か月の利用上限が500,000文字となっています。

このブログの翻訳程度なら十分そうです。

無料プランでも本人確認のためにクレジットカードの登録が必要でした。

登録が終了すると、アカウント情報の中でAPIの認証キーとAPIドメインが表示されます。

DeepLはこの認証キーだけで認証するので、これが漏れると自分のアカウントを勝手に使われてしまいます。

APIアクセスはサーバーサイドで行うか、クライアントサイドで行う場合にはローカル環境に限定するといったセキュリティの基本を守る必要があります。

うっかり認証キーを公開してしまった場合にはアカウントの画面で再発行すれば以前のキーが無効になります。

単純な翻訳をするためのコードは以下のような感じです。Jqueryのajaxで簡単に実現できます。

試す場合にはapiKeyに自分のAPIキーを設定してください。

ここでは シンプルにするために、翻訳する原文は変数に持たせて、結果はコンソールに出力しています。

<!DOCTYPE html>
<html>

<head>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>

<body>
  <script>
    const apiKey = 'xxxxx-xxxxxxx-xxxxxx-xxxxxxx'; // DeepL APIキーを設定
    const textToTranslate = 'こんにちは、世界!'; // 翻訳したいテキストを設定
    const targetLang = 'EN-US'; // この言語に翻訳する
    const endpoint = "https://api-free.deepl.com/v2/translate";

    $.ajax({
      url: endpoint,
      type: 'POST',
      data: {
        'auth_key': apiKey,
        'text': textToTranslate,
        'target_lang': targetLang
      },
      success: function (data) {
        console.log(JSON.stringify(data));
      },
      error: function (error) {
        console.error(error);
      }
    });
  </script>
</body>

</html>

結果はこんな感じのJSONが返ってきます。

{"translations":[{"detected_source_language":"JA","text":"Hello world!"}]}

文章を一段落ずつ処理できると楽

あとは自分の使いやすいような機能とUIを作り込んでいきました。

アプリの特徴としては、ボタン操作で一行(段落)ずつ翻訳する点があります。

まとめて翻訳すると日本語と英語を付き合わせての確認と修正がかえって面倒になるからです。

エディタ上でその文だけ再翻訳みたいな機能を付ければ良いのでしょうが、それはコーディングがちょっと面倒くさそうです。

画面はこのような感じです。

操作方法は以下のようになります。

1.一番上の欄に翻訳したい日本語をペーストします。

2.「load text to System」ボタンをクリックします。原文が1段落ずつ分割されて内部の配列に読み込まれます。読み込んだあとに上の欄で原文を変更した場合には、再度「load text to System」ボタンをクリックする必要があります。

3.「Take one line and translate」ボタンをクリックします。原文から一行が取り出され、それが英語に翻訳され、同時に翻訳した英語が再度日本語に逆翻訳されます。

このとき、「Speak」のチェックボックスが有効だと翻訳した英文が読み上げられます。メニューで音声が選べます。

翻訳結果に問題がなければ、「Take one line out and translate」ボタンをクリックします。

すると、さっき翻訳された英語が確定訳として「Output Text」欄に追加され、次の一文が翻訳されます。

4.翻訳に問題がある場合には、日本語の原文、翻訳した英語のどちらでも好きな方を修正できます。

修正後は「Translate to English」や「Translate back to Japannese」ボタンを使って再翻訳や逆翻訳をして確認します。

うまく翻訳ができたら「Take one line and translate」ボタンをクリックします。

これを繰り返して最後にOutput Text欄に溜まった訳文をコピペして使います。

一本道の作業が心地よい

実際にブログ記事の翻訳に使って見ると、「1文訳してまた1文」という、一本道の作業工程が快適でした。

原文、訳文のどちらもすぐに直せて、翻訳・逆翻訳できるというのは思いのほか便利です。また、訳文の読み上げも英文を凝視しないで済むので負荷軽減に有効でした。

我ながら、なかなか良いツールが出来たと思います。

コード

コードは以下のようなものです。使う場合にはAPIキーは自分のものを設定してください。

DeepL API Proの場合はAPIのURLが違うかもしれません。

変換言語設定はハードコーディングなので適宜変更してください。

日本語への再翻訳は、本ブログが"ですます調”なのでオプションで'formality': 'more'を設定しています。

公式ドキュメントによると、formalityには以下のような設定があります。任意のものに変更してください。

default (default)
more - for a more formal language
less - for a more informal language
prefer_more - for a more formal language if available, otherwise fallback to default formality
prefer_less - for a more informal language if available, otherwise fallback to default formality

今回もだいぶChatGPTに助けてもらいました。

<!DOCTYPE html>
<html>

<head>
  <title>DeepL Translation Helper</title>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
  <style>
    textarea {
      width: 100%;
      font-size: x-large;

    }

    .low {
      line-height: 180%;
      height: calc(1.8em * 3);
    }

    .high {
      line-height: 180%;
      height: calc(1.8em * 10);
    }

    button {
      margin: 10px;
    }

    #wrapper {
      width: 90%;
    }
  </style>
</head>

<body>
  <div id="wrapper">
    <h1>DeepL Translation Helper</h1>
    <p>Paste the Japanese text to be translated below</p>
    <textarea id="original_text" class="high"></textarea><br>

    <button id="loadText">load text to System</button>← Click this first<br>
    <button id="getOneParagraph">Take one line out and translate it.</button>
    <input type="checkbox" id="speak" name="speak" checked>
    <label for="speak">Speak</label>
    <select name="voiceSelect" id="voiceSelect"></select>
    <br>


    <textarea id="input" class="low"></textarea><br>

    <button id="translateToEnBtn">Translate to English</button><br>

    <textarea id="result_English" class="low"></textarea><br>

    <button id="translateBackToJaBtn">Translate back to Japanese</button>
    <button id="append">Append to Output</button><br>
    <textarea id="result_Japanese" class="low"></textarea><br>
    <p>Output Text</p>
    <textarea id="output" class="high"></textarea><br>
    <button id="copyToClipboard">Copy To Clipboard</button><br>
    <button id="resetAll">Reset All</button><br>
  </div>

  <script src="script.js"></script>
</body>

</html>
$(document).ready(function () {
  const AUTH_KEY = 'YOUR-DEEPL-API-KEY';// Replace with your actual DeepL API key
  var isFirstTime = true;
  var voices = [];
  // voiceschangedイベントハンドラを設定し、音声リストを取得して保持する
  window.speechSynthesis.onvoiceschanged = function () {
    voices = window.speechSynthesis.getVoices();
    voices = voices.filter(function (voice) {
      return voice.lang === 'en-US';
    });

    var voiceSelect = $('#voiceSelect');
    voiceSelect.empty(); // 先に既存の選択肢を消去

    voices.forEach(function (voice, index) {
      console.log(voice.voiceURI, voice.name);
      var option = $("<option>")
        .val(voice.name)
        .text(voice.name);
      voiceSelect.append(option);
    });

    //"Samantha"があればデフォルトとして選択する
    $("#voiceSelect option").each(function() {
      if ($(this).text() === 'Samantha') {
        $(this).attr('selected', 'selected');
      }
    });

    // voices.forEach(voice => {
    //   console.log(voice.voiceURI, voice.name);
    // });
  };

  var paragraphs = [];
  $('#getOneParagraph').prop('disabled', true);//ボタンを無効化


  $("#loadText").click(function () {

    var originalText = $("#original_text").val();
    if (originalText.trim() === "") return;
    paragraphs = originalText.split("\n").filter(function (line) {
      // 空行または空白行を除去
      return line.trim() !== '';
    });
    $('#getOneParagraph').prop('disabled', false);
    console.log(paragraphs);
  });

  $("#getOneParagraph").click(function () {
    $('#input').val(paragraphs.shift());
    $("#translateToEnBtn").click();
    if (!isFirstTime) {
      var textToAppend = $('#output').val() + "\n" + $('#result_English').val();
      $('#output').val(textToAppend);
    }
    isFirstTime = false;
  });

  $('#translateToEnBtn').click(function () {
    var textToTranslate = $('#input').val();
    $.ajax({
      url: 'https://api-free.deepl.com/v2/translate',
      type: 'POST',
      data: {
        'auth_key': AUTH_KEY, // Replace with your actual DeepL API key
        'text': textToTranslate,
        'target_lang': 'EN-US'
      },
      success: function (response) {
        var translatedText = response.translations[0].text;
        $('#result_English').val(translatedText);

        if (!$('#speak').prop('checked')) return;
        var utterance = new SpeechSynthesisUtterance(translatedText);
        utterance.lang = 'en-US';
        utterance.pitch = 1.5;
        utterance.rate = 0.9;

        // 保持しておいた音声リストから声を選ぶ
        // var voiceName = "Samantha"; // 使いたい音声の名前を設定
        var voiceName = $('#voiceSelect').val();
        var selectedVoice = voices.find(function (voice) {
          return voice.name === voiceName;
        });
        if (selectedVoice) {
          utterance.voice = selectedVoice;
        }

        // テキストを読み上げる
        window.speechSynthesis.speak(utterance);
        $("#translateBackToJaBtn").click();

      },
      error: function () {
        alert('Translation failed');
      }
    });
  });


  $('#translateBackToJaBtn').click(function () {
    var textToTranslate = $('#result_English').val();
    $.ajax({
      url: 'https://api-free.deepl.com/v2/translate',
      type: 'POST',
      data: {
        'auth_key': AUTH_KEY,
        'text': textToTranslate,
        'target_lang': 'JA',
        'formality': 'more'
      },
      success: function (response) {
        var translatedText = response.translations[0].text;
        $('#result_Japanese').val(translatedText);
      },
      error: function () {
        alert('Translation failed');
      }
    });
  });

  $('#append').click(function () {
    var textToAppend = $('#output').val() + "\n" + $('#result_English').val();
    $('#output').val(textToAppend);

  });


  $("#copyToClipboard").click(function () {
    console.log("clicked");
    $("#output").select();
    try {
      var successful = document.execCommand('copy');
      var msg = successful ? 'successful' : 'unsuccessful';
      console.log('Copy command was ' + msg);
    } catch (err) {
      console.error('Oops, unable to copy', err);
    }
  });

  $("#resetAll").click(function () {
    console.log("reset All");
    $('textarea').val('');
    isFirstTime = true;
    paragraphs.length = 0;
    $('#getOneParagraph').prop('disabled', true);//ボタンを無効化

  });

});

-html, Javascript, プログラミング