Google Apps Script の LanguageApp を使う

　`LanguageApp` は Google Apps Script に用意されている小さなクラスです。簡単な翻訳くらいしかできることはありませんが、どんな使い道があるものかな、と思ったのでまとめてみました。Google Cloud Translation API とかなり機能が似ている……というか無料版の制限があるのを除けば `LanguageApp` の上位互換なので、なんとか機能面で `LanguageApp` の存在意義を見つけたいところ。

- [Class LanguageApp | Google Developers](https://developers.google.com/apps-script/reference/language/language-app)
- [Cloud Translation API | Google Cloud](https://cloud.google.com/translate/docs/translating-text?hl=ja)

# 基本的な機能
　`LanguageApp` に用意されている唯一のメソッドが `translate(text, sourceLanguage, targetLanguage[, advancedArgs])` です。言語名は [Google Translate で指定されている略語](https://cloud.google.com/translate/docs/languages) の文字列で指定します。翻訳元言語に空文字列を指定すると、自動判別が効きます。自動判別の際に推測された言語名も取得できるといいんですけど、どうやらそれは無理そう。

```javascript
const txt = LanguageApp.translate("ねこです。", "ja", "en");
Logger.log(txt); // >>> It is a cat.

const txt = LanguageApp.translate("ねこです。", "", "en");
Logger.log(txt); // >>> It is a cat.
```

　`advanceArgs` とは大仰ですが、入力データがプレーンテキスト (txt) か HTML かを選択できるだけです（デフォルトでは txt）。HTML 指定をするとタグを省いたテキスト部分だけ翻訳され、タグは可能な範囲で対応する箇所に残ります。html のままで処理できるのは便利ですね。

```javascript
const html = "これは<strong>ねこ</strong>です。";
const sl = "";
const tl = "en";
const translatedHtml = LanguageApp.translate(html, sl, tl, {contentType: 'html'});
Logger.log(translatedHtml); // >>> This is a <strong>cat</strong>
```

　なお HTML の `translate` 属性に対応しているので、`translate="no"` と指定された部分は翻訳対象から除外されます。

- [HTML5/グローバル属性/translate属性 翻訳対象にするかどうかを指定する - TAG index](https://www.tagindex.com/html5/attribute/translate.html)
- [「翻訳しないをする」GAS LanguageApp.translate | 四海のほとり](https://sikaitoka.blogspot.com/2018/12/gas-languageapptranslate.html)

## 複数入力
　配列を入力した場合は、カンマで結合してひとつの文字列にしたうえで、翻訳にかけられるようです。

```javascript
const input1 = ["I am a cat", "and a dog."];
const output1 = LanguageApp.translate(input1, "", "ja"); // 私は猫であり、犬です。
const input2 = "I am a cat, and a dog.";
const output2 = LanguageApp.translate(input2, "", "ja"); // 私は猫であり、犬です。
const input3 = "I am a cat and a dog.";
const output3 = LanguageApp.translate(input3, "", "ja"); // 私は猫と犬です。
```

　配列として出力したい場合は `map` したりループを回したりしましょう。ただ `LanguageApp` を何度も呼びたくない場合は、改行を入れて結合したり、何らかの置換を噛ませたりすることも考えられます（改行をはさむと翻訳上は別単位と扱われるため）。

```javascript
const inputList = ["cat","dog","squirrel"];
const outputList = inputList.map(x => LanguageApp.translate(x,"","ja"));
// OR:
const outputList = LanguageApp.translate(inputList.join("\n"),"","ja").split("\n");
// outputList = [ネコ, 犬, リス]

```

参考

- [LanguageApp.translateで英文中の単語ごとに翻訳したい | Google Apps Script試行錯誤Blog](https://www.pre-practice.net/2020/01/languageapptranslate.html)

# 応用的な使い道
　以上の機能しかないので、これをメインに据えた使い道というのは限られます。

## 言語処理の API にする
　URL クエリで文字列を渡して、`ContentService.textOutput` でテキストとして返すシンプルな API を作ってみます[^simple]。

[^simple]: 実装では q が未指定の場合の挙動もきちんとする。


```javascript
const doGet = (e) => {
  const q = e.parameter.q;           // 翻訳したい文字列は q で渡す
  const sl = e.parameter.sl || "";   // 翻訳元言語は sl で指定、デフォルトで自動判別
  const tl = e.parameter.tl || "ja"; // 翻訳先言語は tl で指定、デフォルトで日本語
  const r = LanguageApp.translate(q, sl, tl);
  return ContentService.createTextOutput(r);
}
// .../exec?q='This is a cat.' とすると "これは猫です。" が返ってくる
```

　これだけでは何の役にも立たないので、選択範囲を翻訳してアラート表示するブックマークレットを作ってみましょう。上のコードを Web アプリケーションとして導入し[^authorization]、以下をブックマークに登録すればＯＫです。

[^authorization]: API のように使うには、導入の際、権限を「Anyone, even anonymous」に設定するようにしてください。「Only myself」のままだと CORS に引っかかってうまく機能しません。

```javascript:※改行を除いてブックマークレットにする
javascript:void((()=>{
  const txt = window.getSelection().toString();
  const url = 'https://script.google.com/macros/s/___scriptId___/exec?q='+txt;
  fetch(url)
    .then((response)=>response.text())
    .then((text)=>alert(text))
    .catch((error)=>alert(error))
})())
```

参考

- [Bookmarkletを作ろう(準備編）| Qiita](https://qiita.com/kanaxx/items/63debe502aacd73c3cb8)
- [今から10分ではじめる Google Apps Script(GAS) で Web API公開 | Qiita](https://qiita.com/riversun/items/c924cfe70e16ee3fe3ba#%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%81%8B%E3%82%89%E3%81%93%E3%81%AEapi%E3%82%92%E3%81%9F%E3%81%9F%E3%81%8D%E3%81%9F%E3%81%84)
- [GASのExecution APIを使ってGASを外部からぶっ叩く | Qiita](https://qiita.com/soundTricker/items/1bcfc5c9e80d29a7ae4b)

## テキストエディタの翻訳プラグインとして使う
　やはり Google Docs との連携は楽です。Docs のアプリからは文書全体の翻訳しかできませんが、GAS を書くことで部分翻訳を含めた様々なことができるようになります。たとえば、カスタムメニューに選択範囲を翻訳する関数をつけてみましょう。以下のスクリプトを Docs ファイル紐付きの GAS として書き、`onOpen` を開始時トリガーに設定しましょう。
　Google Docs はドキュメントを様々な独自オブジェクトのカタマリとして扱っているので、それとは無関係に設定される「ユーザーの選択範囲」を扱うのは直観よりはるかに面倒くさいですが、そこだけ別口で関数をつくってしまえば簡単です。

```javascript:code.gs
const doc = DocumentApp.getActiveDocument();

const onOpen = () => {
  const ui = DocumentApp.getUi();
  ui.createMenu('カスタムメニュー')
    .addItem('選択部分を日本語に翻訳する（アラート）', 'translateToJaByAlert')
    .addItem('選択部分を日本語に翻訳する（挿入）', 'translateToJaByInsertion')
    .addToUi();
}

const translateToJaByAlert = () => {
  const range = doc.getSelection();
  const originalText = getText(range);
  if(originalText){
    const translatedText = LanguageApp.translate(originalText, "", "Ja");
    DocumentApp.getUi().alert(`BEFORE「${originalText}」\n AFTER「${translatedText}」`);
  }else{
    DocumentApp.getUi().alert("選択範囲がありません。");
  }
}

const translateToJaByInsertion = () => {
  const range = doc.getSelection();
  const originalText = getText(range);
  const positionForInsertion = getEndPosOfRange(range);
  if(originalText && positionForInsertion){
    const translatedText = LanguageApp.translate(originalText, "", "Ja");
    positionForInsertion.insertText(translatedText);
  }else{
    DocumentApp.getUi().alert("選択範囲がありません。");
  }
}

const getText = (range) => {
  if(!range) return false;
  const elements = range.getRangeElements();
  let text = [];
  for(let i=0; i<elements.length; i++){
    if(!elements[i].getElement().editAsText()) continue;
    if(elements[i].isPartial()){
      const element = elements[i].getElement().asText();
      const startIndex = elements[i].getStartOffset();
      const endIndex = elements[i].getEndOffsetInclusive() + 1;
      text.push(element.getText().substring(startIndex, endIndex));
    }else{
      text.push(elements[i].getElement().asText().getText());
    }
  }
  return text.length!==0 ? text.join("") : ""; // 区切り文字等は好みによる
}

const getEndPosOfRange = (range) => {
  if(!range) return false;
  const elements = range.getRangeElements();
  let lastElement, endOfLastElement;
  for(let i=elements.length-1; i>=0; i--){
    if(!elements[i].getElement().editAsText()) continue;
    endOfLastElement = elements[i].isPartial()
      ? elements[i].getEndOffsetInclusive() + 1
      : elements[i].getElement().asText().getText().length;
    lastElement = elements[i];
    break;
  }
  return doc.newPosition(lastElement.getElement().editAsText(), endOfLastElement);
}
```

　選択範囲を `range` オブジェクトとして取得するのは簡単ですが、`range` オブジェクトから直接 text を取得することはできないので、いったん各種 `element` を経由して取得する必要があります。選択範囲が `element` の境界と一致しない場合は `getStartOffset` と `getEndOffsetInclusive` で選択範囲の境界を取得して、それを利用して text を抽出します。

- [[Google Apps Script]選択文字列を翻訳する | 初心者備忘録](https://www.ka-net.org/blog/?p=4277)

　それ以外のテキストエディタでも同様に、翻訳プラグインとして利用して、読書／執筆作業を効率化することができます。既にいくつも作成されています。

- [Vimの翻訳プラグイン作りました | ゴリラの技術ブログ](https://gorilla.netlify.app/articles/20190512-vim-translate.html#%E6%A9%9F%E8%83%BD)
- [HmGoogleLangTranslatorCS](http://xn--pckzexbx21r8q9b.net/?page=nobu_tool_hm_google_lang_translator_cs)（秀丸エディタのマクロ）

　VBA でも HTTPS 通信できるようですが、`System.Net.Http` は Windows10 非対応っぽい？　VBA はよくわかりません。

- [エクセルVBAでHTTPリクエストをする最も簡単なプログラム | いつも隣にITのお仕事](https://tonari-it.com/excel-vba-http-request/)
- [【ExcelVBA】HTTP/HTTPS通信でWebページを取得する | Qiita](https://qiita.com/nkojima/items/c11f6369f4c32b06c90b)

## 折り返し翻訳やってみる
　かつて「[エキサイト翻訳](https://dic.nicovideo.jp/a/%E3%82%A8%E3%82%AD%E3%82%B5%E3%82%A4%E3%83%88%E7%BF%BB%E8%A8%B3)]というものが流行りましたが、機械翻訳技術の発達により、再翻訳はいまや笑い以外の幸せを人間にもたらすようになりました。日本語→英語→日本語と多重翻訳を繰り返しても、ちょっとした言葉遣いの違いが蓄積していくだけで、あまり文意を損なうことがなくなりました。これを用いて文章の推敲をやってみましょう。先ほどの続きで、以下のコードを Google Docs に仕込んで試してみます。

```javascript
const reviseViaRecursiveTranslation = () => {
  const range = doc.getSelection();
  const originalText = getText(range);
  if (originalText){
    const mediumText = LanguageApp.translate(originalText, "Ja", "En");
    const revisedText = LanguageApp.translate(mediumText, "En", "Ja");
    DocumentApp.getUi().alert(`BEFORE「${originalText}」\n AFTER「${revisedText}」`);
  }else{
    DocumentApp.getUi().alert("選択範囲がありません。");
  }
}
```

　宮沢賢治「セロ弾きのゴーシュ」の冒頭の一文をコレにかけてみると、こんな感じ。英語の単純過去形を習慣相に訳せていないのはしょうがないとして、poor と楽長以外はほぼ語彙の間違いもなく、良さげです。単純なフレーズ翻訳ではないので、文の構造からがらりと変わっているのも可能性を感じます。
![qiita_gosh.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526487/72581a8b-ba42-9a03-6977-c18d91b4cd86.png)
　安部公房「砂の女」はどうでしょう。翻訳システムの学習に使われているデータの都合でしょうか、語彙選択が多少ナウでヤングな感じになっていますが、まあ内容は外してないかな……。
![qiita_suna.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526487/4cfb6d44-9f81-9a56-ed2a-e258d26126a6.png)
　……推敲に使えるかは微妙ですが、見ていると面白いですね。これだけで単独の記事にできそう。

# その他
　Google Spreadsheet の組み込み関数 `googletranslate` はなぜか本記事で扱った `LanguageApp` とは異なる（より品質の低い）翻訳を返すようです。

- [Google Spreadsheet の googletranslate 関数の代わりに LanguageApp を使うワークシート関数を作ってイケてる翻訳ができるようにする | Qiita](https://qiita.com/kazinoue/items/82779a11af8a2dea0d21)
- [GASでspreadsheetの組み込み関数を使う方法 | Qiita](https://qiita.com/nyantera/items/fdf1170a964ffaac0dc8)

　近頃はあちこちで翻訳 API が公開されていますので、翻訳結果を並べて比較するのも良いかもしれません。

- [Google Translateやその他の翻訳API | Rakuten RapidAPI](https://api.rakuten.net/collection/google-translate-api)

　単に全文自動翻訳にかけるのではなく、何らかの処理を噛ませることで、ウェブアプリの日本語化をより高精度に行なう手法はありそうです（形態素解析を組み合わせたりとか）。ただそうなるともう `Cloud Translation API` に用語集噛ませるのが強いので、GAS で頑張ることではない気がします。

# まとめ
- やっぱり機能が貧弱すぎる。
- GAS は API を作りやすいので、翻訳をサポートとして使う道はたくさんあるはず。