Google Apps Script クイックスタート：選択したテキストを他の言語に変換するアドオンを５分で作成する

（[同じ内容で画面キャプチャ付きの記事はこちらにあります。](http://grgrjnjn.blogspot.jp/2017/01/gas-quickstart-docs-add-on-translate.html)）


Google翻訳を利用して選択したテキストを他の言語に変換するGoogle Docsのアドオンを作成します。

上図のようにサイドバーとしてテキストを翻訳する機能を追加します。

##準備

1.
新しいGoogleドキュメントを作成します。

2.
新しい文書の中から、メニュー項目の[ツール]> [スクリプトエディタ]を選択します。ようこそ画面が表示されたら、[空のプロジェクト]をクリックします。

3.
スクリプトエディタでコードを削除し、下のコードに貼り付けます。

```JavaScript
/**
 * @OnlyCurrentDoc
 *
 */
 
/**
 * ドキュメントを開いたときに、GoogleドキュメントのUIにメニューを作成します。
 * このメソッドは通常のアドオンでのみ使用され、モバイルでは呼び出されません。
 *
 * @param {object} e 単純な onOpen トリガーのイベントパラメーターです。
 *     トリガーが実行されている認証モード（ScriptApp.AuthMode）を確認するには、
 *     e.authModeを調べます。
 */
function onOpen(e) {
  DocumentApp.getUi().createAddonMenu()
      .addItem('Start', 'showSidebar')
      .addToUi();
}

/**
 * アドオンがインストールされているときに実行されます。
 * モバイルでは動きません。
 *
 * @param {object} e 単純なonInstallトリガのイベントパラメータです。
 *     トリガーが実行されている認証モード（ScriptApp.AuthMode）を
 *     確認するには、e.authModeを調べます。（実際には、onInstall
 *     トリガーは常にAuthMode.FULLで実行されますが、onOpenトリガーは
 *     AuthMode.LIMITEDまたはAuthMode.NONEです。）
 */
function onInstall(e) {
  onOpen(e);
}

/**
 * サイドバーを開きます。
 * モバイルでは動きません。
 */
function showSidebar() {
  var ui = HtmlService.createHtmlOutputFromFile('Sidebar')
      .setTitle('Translate');
  DocumentApp.getUi().showSidebar(ui);
}

/**
 * ユーザーが選択したテキストを取得します。
 * 選択がない場合は、エラーメッセージを表示します。
 *
 * @return {Array.} 選択されたテキスト
 */
function getSelectedText() {
  var selection = DocumentApp.getActiveDocument().getSelection();
  if (selection) {
    var text = [];
    var elements = selection.getSelectedElements();
    for (var i = 0; i < elements.length; i++) {
      if (elements[i].isPartial()) {
        var element = elements[i].getElement().asText();
        var startIndex = elements[i].getStartOffset();
        var endIndex = elements[i].getEndOffsetInclusive();

        text.push(element.getText().substring(startIndex, endIndex + 1));
      } else {
        var element = elements[i].getElement();
        // テキストとして編集できる要素のみを翻訳します。
        // 画像やその他のテキスト以外の要素をスキップします。
        if (element.editAsText) {
          var elementText = element.asText().getText();
          // このチェックは、画像を除外して空のテキスト要素を返すために必要です。
          if (elementText != '') {
            text.push(elementText);
          }
        }
      }
    }
    if (text.length == 0) {
      throw 'Please select some text.';
    }
    return text;
  } else {
    throw 'Please select some text.';
  }
}

/**
 * 設定されているオリジナルの言語と目的の言語（翻訳後）を取得します。
 * モバイルでは動きません。
 *
 * @return {Object} オリジナルの言語と目的の言語
 */
function getPreferences() {
  var userProperties = PropertiesService.getUserProperties();
  var languagePrefs = {
    originLang: userProperties.getProperty('originLang'),
    destLang: userProperties.getProperty('destLang')
  };
  return languagePrefs;
}

/**
 * ユーザーが選択したテキストを取得し、オリジナルの言語から目的の言語
 * （翻訳後）に変換します。言語は2文字の短い形式で表記されています。
 * たとえば、英語は「en」、スペイン語は「es」です。
 * オリジナルの言語は、Google Translateが自動的にその言語を検出する
 * ことを示す空の文字列として指定することができます。
 *
 * @param {string} origin オリジナルの言語の２文字の短縮形
 * @param {string} dest 目的の言語の２文字の短縮形
 * @param {boolean} savePrefs オリジナルの言語と目的の言語を保存するかどうか
 * @return {Object} 元のテキストと翻訳後のテキストを含むオブジェクト
 */
function getTextAndTranslation(origin, dest, savePrefs) {
  var result = {};
  var text = getSelectedText();
  result['text'] = text.join('\n');

  if (savePrefs == true) {
    var userProperties = PropertiesService.getUserProperties();
    userProperties.setProperty('originLang', origin);
    userProperties.setProperty('destLang', dest);
  }

  result['translation'] = translateText(result['text'], origin, dest);

  return result;
}

/**
 * 現在の選択範囲のテキストを指定されたテキストに置き換えるか、
 * 現在のカーソル位置にテキストを挿入します。
 * 複数の要素が選択されている場合は、テキストを含むことができる
 * 最初の要素に翻訳されたテキストを挿入し、他の要素を削除します。
 *
 * @param {string} newText 現在の選択を置き換えるテキスト
 */
function insertText(newText) {
  var selection = DocumentApp.getActiveDocument().getSelection();
  if (selection) {
    var replaced = false;
    var elements = selection.getSelectedElements();
    if (elements.length == 1 &&
        elements[0].getElement().getType() ==
        DocumentApp.ElementType.INLINE_IMAGE) {
      throw "Can't insert text into an image.";
    }
    for (var i = 0; i < elements.length; i++) {
      if (elements[i].isPartial()) {
        var element = elements[i].getElement().asText();
        var startIndex = elements[i].getStartOffset();
        var endIndex = elements[i].getEndOffsetInclusive();

        var remainingText = element.getText().substring(endIndex + 1);
        element.deleteText(startIndex, endIndex);
        if (!replaced) {
          element.insertText(startIndex, newText);
          replaced = true;
        } else {
          // このブロックは、部分的な要素で終わる選択を処理します。
          // この部分テキストを前の要素にコピーして、最後の部分的な
          // 部分の前に改行がないようにします。
          var parent = element.getParent();
          parent.getPreviousSibling().asText().appendText(remainingText);
          // ドキュメントの最後の段落は削除できません。
          // この場合は、代わりに最後の段落内のテキストを
          // 削除してください。
          if (parent.getNextSibling()) {
            parent.removeFromParent();
          } else {
            element.removeFromParent();
          }
        }
      } else {
        var element = elements[i].getElement();
        if (!replaced && element.editAsText) {
          // テキストとして編集できる要素のみを翻訳し、
          // 他の要素は削除しないでください。
          element.clear();
          element.asText().setText(newText);
          replaced = true;
        } else {
          // ドキュメントの最後の段落は削除できません。
          // このような場合は、要素をクリアしてください。
          if (element.getNextSibling()) {
            element.removeFromParent();
          } else {
            element.clear();
          }
        }
      }
    }
  } else {
    var cursor = DocumentApp.getActiveDocument().getCursor();
    var surroundingText = cursor.getSurroundingText().getText();
    var surroundingTextOffset = cursor.getSurroundingTextOffset();

    // カーソルがスペース以外の文字の前または後にある場合は、
    // 文字と翻訳の間にスペースを挿入します。
    // それ以外の場合は、翻訳を挿入するだけです。
    if (surroundingTextOffset > 0) {
      if (surroundingText.charAt(surroundingTextOffset - 1) != ' ') {
        newText = ' ' + newText;
      }
    }
    if (surroundingTextOffset < surroundingText.length) {
      if (surroundingText.charAt(surroundingTextOffset) != ' ') {
        newText += ' ';
      }
    }
    cursor.insertText(newText);
  }
}


/**
 * Google翻訳を使って翻訳をします。
 * 言語は2文字の短い形式で表記されています。
 * たとえば、英語は「en」、スペイン語は「es」です。
 * オリジナル言語は、Google Translateが自動的にその言語を検出することを示す
 * 空の文字列として指定することができます。
 *
 * @param {string} text 翻訳するテキスト
 * @param {string} origin オリジナルの言語の２文字の短縮形
 * @param {string} dest 目的の言語の２文字の短縮形
 * @return {string} 翻訳の結果
 */
function translateText(text, origin, dest) {
  if (origin === dest) {
    return text;
  }
  return LanguageApp.translate(text, origin, dest);
}
```

4.
メニューから[ファイル] > [新規作成] > [HTMLファイル] 選択して新しいファイルを作成します。ファイルに「Sidebar」という名前を付けます。

5.
新しいエディタタブでコードを削除し、下のコードに貼り付けます。

```html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
    <!-- The CSS package above applies Google styling to buttons and other elements. -->

    <style>
    .branding-below {
      bottom: 56px;
      top: 0;
    }

    .branding-text {
      left: 7px;
      position: relative;
      top: 3px;
    }

    .col-contain {
      overflow: hidden;
    }

    .col-one {
      float: left;
      width: 50%;
    }

    .logo {
      vertical-align: middle;
    }

    .radio-spacer {
      height: 20px;
    }

    .width-100 {
      width: 100%;
    }
    </style>
  </head>
  <body>
    <div class="sidebar branding-below">
      <form>
        <div class="block col-contain">
          <div class="col-one">
            <b>Selected text</b>
            <div>
              <input type="radio" name="origin" id="radio-origin-auto" value="" checked="checked">
              <label for="radio-origin-auto">Auto-detect</label>
            </div>
            <div>
              <input type="radio" name="origin" id="radio-origin-en" value="en">
              <label for="radio-origin-en">English</label>
            </div>
            <div>
              <input type="radio" name="origin" id="radio-origin-fr" value="fr">
              <label for="radio-origin-fr">French</label>
            </div>
            <div>
              <input type="radio" name="origin" id="radio-origin-de" value="de">
              <label for="radio-origin-de">German</label>
            </div>
            <div>
              <input type="radio" name="origin" id="radio-origin-ja" value="ja">
              <label for="radio-origin-ja">Japanese</label>
            </div>
            <div>
              <input type="radio" name="origin" id="radio-origin-es" value="es">
              <label for="radio-origin-es">Spanish</label>
            </div>
          </div>
          <div>
            <b>Translate into</b>
            <div class="radio-spacer">
            </div>
            <div>
              <input type="radio" name="dest" id="radio-dest-en" value="en">
              <label for="radio-dest-en">English</label>
            </div>
            <div>
              <input type="radio" name="dest" id="radio-dest-fr" value="fr">
              <label for="radio-dest-fr">French</label>
            </div>
            <div>
              <input type="radio" name="dest" id="radio-dest-de" value="de">
              <label for="radio-dest-de">German</label>
            </div>
            <div>
              <input type="radio" name="dest" id="radio-dest-ja" value="ja" checked="checked">
              <label for="radio-dest-ja">Japanese</label>
            </div>
            <div>
              <input type="radio" name="dest" id="radio-dest-es" value="es">
              <label for="radio-dest-es">Spanish</label>
            </div>
          </div>
        </div>

        <div class="block form-group">
          <label for="translated-text"><b>Translation</b></label>
          <textarea class="width-100" id="translated-text" rows="10"></textarea>
        </div>

        <div class="block">
          <input type="checkbox" id="save-prefs">
          <label for="save-prefs">Use these languages by default</label>
        </div>

       <div class="block" id="button-bar">
          <button class="blue" id="run-translation">Translate</button>
          <button id="insert-text">Insert</button>
        </div>
      </form>
    </div>

    <div class="sidebar bottom">
      <img alt="Add-on logo" class="logo" src="https://www.gstatic.com/images/branding/product/1x/translate_48dp.png" width="27" height="27">
      <span class="gray branding-text">Translate sample by Google</span>
    </div>

    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js">
    </script>
    <script>
      /**
       * ドキュメントの読み込み時に、各ボタンにクリックハンドラを割り当てます。
       * 設定されている場合は、オリジナルの言語と目的の言語（翻訳後）の
       * 設定を読み込みます。
       */
      $(function() {
        $('#run-translation').click(runTranslation);
        $('#insert-text').click(insertText);
        google.script.run.withSuccessHandler(loadPreferences)
            .withFailureHandler(showError).getPreferences();
      });

      /**
       * サーバーから渡されたオリジナルの言語と目的の言語を設定するための
       * コールバック関数。
       *
       * @param {Object} languagePrefs 保存されたオリジナルの言語と目的の言語
       */
      function loadPreferences(languagePrefs) {
        $('input:radio[name="origin"]')
            .filter('[value=' + languagePrefs.originLang + ']')
            .attr('checked', true);
        $('input:radio[name="dest"]')
            .filter('[value=' + languagePrefs.destLang + ']')
            .attr('checked', true);
      }

      /**
       * サーバーサイドで翻訳を実行して、サイドバーの結果を更新します。
       */
      function runTranslation() {
        this.disabled = true;
        $('#error').remove();
        var origin = $('input[name=origin]:checked').val();
        var dest = $('input[name=dest]:checked').val();
        var savePrefs = $('#save-prefs').is(':checked');
        google.script.run
            .withSuccessHandler(
              function(textAndTranslation, element) {
                $('#translated-text').val(textAndTranslation.translation);
                element.disabled = false;
              })
            .withFailureHandler(
              function(msg, element) {
                showError(msg, $('#button-bar'));
                element.disabled = false;
              })
            .withUserObject(this)
            .getTextAndTranslation(origin, dest, savePrefs);
      }

      /**
       * サーバー側の機能を実行して、翻訳されたテキストをユーザーのカーソルに
       * 挿入または選択したテキストを置き換えます。
       */
      function insertText() {
        this.disabled = true;
        $('#error').remove();
        google.script.run
            .withSuccessHandler(
              function(returnSuccess, element) {
                element.disabled = false;
              })
            .withFailureHandler(
              function(msg, element) {
                showError(msg, $('#button-bar'));
                element.disabled = false;
              })
            .withUserObject(this)
            .insertText($('#translated-text').val());
      }

      /**
       * 指定された要素（element）にエラーメッセージを含むdivタグを挿入します。
       *
       * @param msg 表示するエラーメッセージ
       * @param element エラーを表示する要素
       */
      function showError(msg, element) {
        var div = $('<div id="error" class="error">' + msg + '</div>');
        $(element).after(div);
      }
    </script>
  </body>
</html>
```

6. 
メニューの [ファイル] > [すべてを保存] を選択します。新しいプロジェクト名を「Translate Quickstart」とし、「OK」をクリックします（プロジェクトの名前は、承認ダイアログを含むいくつかの場所のエンドユーザーに表示されます）。



##実行

1.
ドキュメントに戻ってページをリロードしてください。アドオンが読み込まれるまで数秒待ちます。

2.
メニュー [アドオン] > [Translate Quickstart] > [Start] を選択します。（スクリプトに別の名前を選択した場合は、代わりにその名前が表示されます）

３.
承認が必要となるので[継続]ボタンを選択してください。


４.
[許可]ボタンを選択してください。


５.
サイドバーが表示されます。
ドキュメントにテキストを入力し、それを選択して青の翻訳ボタンをクリックします。ドキュメント内のテキストを置き換えるには、[Insert]ボタンをクリックします。