12
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Google Apps Script の LanguageApp を使う

Last updated at Posted at 2020-05-30

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

基本的な機能

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

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 のままで処理できるのは便利ですね。

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" と指定された部分は翻訳対象から除外されます。

複数入力

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

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 を何度も呼びたくない場合は、改行を入れて結合したり、何らかの置換を噛ませたりすることも考えられます(改行をはさむと翻訳上は別単位と扱われるため)。

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 = [ネコ, 犬, リス]

参考

応用的な使い道

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

言語処理の API にする

 URL クエリで文字列を渡して、ContentService.textOutput でテキストとして返すシンプルな API を作ってみます1

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 アプリケーションとして導入し2、以下をブックマークに登録すればOKです。

※改行を除いてブックマークレットにする
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))
})())

参考

テキストエディタの翻訳プラグインとして使う

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

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 の境界と一致しない場合は getStartOffsetgetEndOffsetInclusive で選択範囲の境界を取得して、それを利用して text を抽出します。

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

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

折り返し翻訳やってみる

 かつて「エキサイト翻訳]というものが流行りましたが、機械翻訳技術の発達により、再翻訳はいまや笑い以外の幸せを人間にもたらすようになりました。日本語→英語→日本語と多重翻訳を繰り返しても、ちょっとした言葉遣いの違いが蓄積していくだけで、あまり文意を損なうことがなくなりました。これを用いて文章の推敲をやってみましょう。先ほどの続きで、以下のコードを Google Docs に仕込んで試してみます。

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
 安部公房「砂の女」はどうでしょう。翻訳システムの学習に使われているデータの都合でしょうか、語彙選択が多少ナウでヤングな感じになっていますが、まあ内容は外してないかな……。
qiita_suna.png
 ……推敲に使えるかは微妙ですが、見ていると面白いですね。これだけで単独の記事にできそう。

その他

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

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

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

まとめ

  • やっぱり機能が貧弱すぎる。
  • GAS は API を作りやすいので、翻訳をサポートとして使う道はたくさんあるはず。
  1. 実装では q が未指定の場合の挙動もきちんとする。

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

12
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?