Google Slides を翻訳する Google App Script

# はじめに

こちらの記事に触発されて自分でもいろいろと試してみました。

- [【GoogleAppsScript/Google Slide】DeepL を使ってプレゼンを一括で翻訳する - Qiita](https://qiita.com/Qiitadelsol/items/a688455b215eae958ccc)
- [【GAS】Google Apps Script 活用事例　Google Slides 全ページ英訳スクリプトの完全版。表の中のテキストも翻訳出来るようになりましたッ！！｜nepia_infinity｜note](https://note.com/nepia_infinity/n/n4b84abd91fdf)

# やりたいこと

Google 翻訳も DeepL も以下のように直接 Google Slides ファイルには対応していないため、ファイル形式を一回変換して翻訳後にまた戻すという作業が必要です。

<u>Google Slides のまま</u>、翻訳する方法を模索した結果、先人の偉業にたどり着きました。

![image-20220311235317114](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20220311235317114.png)

![image-20220311235241200](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20220311235241200.png)

# カスタマイズした点

参考サイトにあった Google App Script の流れを汲んで以下のように対応しています。

- Google 翻訳と DeepL の両方に対応
  - DeepL Free Plan は `500,000 character limit / month` まで OK
- スピーカーノートに翻訳前テキストを追加
- 表内のテキストに対応（結合セル非対応）
- 全スライド翻訳と、特定ページ翻訳の機能として整理
- `Exception: Object is not of type Shape` を回避
- `GROUP` オブジェクトタイプ（グループ化されたオブジェクト）に2階層まで対応

# 使い方

- DeepL が必要な場合は、DeepL API Key を取得 https://www.deepl.com/docs-api
  - 以下のコード内 `YOUR_DEEPL_API_KEY` を取得したもので置換
- [Google App Script コード](#google-app-script-コード) をスクリプトエディタから貼り付けて保存し、「Run」
  その後、「翻訳機能」のメニューから利用する
  ![image-20220312000816959](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20220312000816959.png)
  ![image-20220312001340383](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20220312001340383.png)
  ![image-20220312001431607](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20220312001431607.png)
  ![image-20220312001517771](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20220312001517771.png)

# Google App Script コード

冗長ですが、ご活用ください。

DeepL の方が翻訳に時間はかかりますが、クオリリティは高いというのが感想です。

```javascript:Code.gas
function onOpen() {
SlidesApp.getUi()
.createMenu('翻訳機能') // メニューの追加、（セルを結合した表が一つでもあると、処理が行われません。）
.addItem('[全スライド] DeepL 翻訳（英語-->日本語）（テーブル対応）', 'translateAllDeepL')
.addItem('[特定ページ] DeepL 翻訳（英語-->日本語）（テーブル対応）','specifiedPageTranslateDeepL')
.addItem('[全スライド] Google翻訳（英語-->日本語）（テーブル対応）', 'translateAll')
.addItem('[特定ページ] Google翻訳（英語-->日本語）（テーブル対応）','specifiedPageTranslate')
.addToUi();
}

const apiKey = 'YOUR_DEEPL_API_KEY';
const apiUrl = 'https://api-free.deepl.com/v2/translate';

function getLatestGlossaryId() {
  const getheader = {
    "Authorization":`DeepL-Auth-Key ${apiKey}`,
  } 
 
  const parameters = {
    "method": "get",
    "headers": getheader
  }

  let response = UrlFetchApp.fetch('https://api-free.deepl.com/v2/glossaries', parameters);
  let response_code = response.getResponseCode().toString();
  if (response_code != 200) return `DeepL:HTTP Error(${response_code})`
 
  let json = JSON.parse(response.getContentText('UTF-8'));// JSONからテキストを取り出す
  if (!json.glossaries.length) {
    return ""
  }
  console.log(json.glossaries.slice(-1)[0].glossary_id)
  return json.glossaries.slice(-1)[0].glossary_id;
}

const gid = getLatestGlossaryId()
console.log(gid)

function deepltranslate(text, src, tgt) {
  let t = text.replace( '&', 'and' ).toString();
  let content = encodeURI(`auth_key=${apiKey}&text=${t}&source_lang=${src}&target_lang=${tgt}&glossary_id=${gid}`);

  const postheader = {
    "accept":"gzip, */*",
    "timeout":"20000",
    "Content-Type":"application/x-www-form-urlencoded"
  }  

  const parameters = {
    "method": "post",
    "headers": postheader,
    'payload': content
  }

  /*try {
    let response = UrlFetchApp.fetch(apiUrl, parameters);
  }
  catch (e) {
    Logger.log(e.toString());return 'DeepL:Exception';
  }*/
  let response = UrlFetchApp.fetch(apiUrl, parameters);
  let response_code = response.getResponseCode().toString();
  if (response_code != 200) return `DeepL:HTTP Error(${response_code})`

  let json = JSON.parse(response.getContentText('UTF-8'));// JSONからテキストを取り出す
  return json.translations[0].text;
}

function translateAllDeepL() {
const presentation  = SlidesApp.getActivePresentation();
const slides        = presentation.getSlides();

console.log(presentation.getName());
console.log(slides);
console.log('スライドの枚数: %s',slides.length);

for(let i = 0; i < slides.length; i++){
  for(let j = 0; j < slides[i].getPageElements().length; j++){
    /*console.log(slides[i].getPageElements()[j].getPageElementType().toString())*/
    if (slides[i].getPageElements()[j].getPageElementType().toString() == 'SHAPE') {

      const contents = slides[i].getPageElements()[j].asShape().getText().asString();
    
      /*スピーカーノートの取得 翻訳前のテキストをスピーカノートに入れる*/
      const notes    = slides[i].getNotesPage();
      notes.getSpeakerNotesShape().getText().appendText("\n" + contents);
    
      /*翻訳後のテキストを貼り付け*/
      const results  = deepltranslate(contents, 'en', 'ja');
      slides[i].getPageElements()[j].asShape().getText().setText(results);
    } 
    else if (slides[i].getPageElements()[j].getPageElementType().toString() == 'GROUP'){
      const elemsInGroup = slides[i].getPageElements()[j].asGroup().getChildren()
      for(const elem of elemsInGroup) {
        /*console.log(elem.getPageElementType().toString());*/
        if (elem.getPageElementType().toString() === 'SHAPE') {
          const contents = elem.asShape().getText().asString();
    
          /*スピーカーノートの取得 翻訳前のテキストをスピーカノートに入れる*/
          const notes    = slides[i].getNotesPage();
          notes.getSpeakerNotesShape().getText().appendText("\n" + contents);
    
          /*翻訳後のテキストを貼り付け*/
          const results  = deepltranslate(contents, 'en', 'ja');
          elem.asShape().getText().setText(results);
        }
        else if (elem.getPageElementType().toString() === 'GROUP') {
          const elemsInGroup2 = elem.asGroup().getChildren()
          for(const elem2 of elemsInGroup2) {
            console.log(elem2.getPageElementType().toString());
            if (elem2.getPageElementType().toString() === 'SHAPE') {
              const contents = elem2.asShape().getText().asString();
    
              /*スピーカーノートの取得 翻訳前のテキストをスピーカノートに入れる*/
              const notes    = slides[i].getNotesPage();
              notes.getSpeakerNotesShape().getText().appendText("\n" + contents);
    
              /*翻訳後のテキストを貼り付け*/
              const results  = deepltranslate(contents, 'en', 'ja');
              elem2.asShape().getText().setText(results);
            }
          }
        }
      }
    }
  }//for_j
}//for_i
/*テーブル内も変換出来るようにする*//*全スライドの取得*/
for(let i = 0; i < slides.length; i++){
  for(let j = 0; j < slides[i].getTables().length; j++){
    
    console.log('スライド　%s　ページ目には、表　%sがあります。',i+1,slides[i].getTables());
    
    const rowIndex    = slides[i].getTables()[j].getNumRows();
    const columnIndex = slides[i].getTables()[j].getNumColumns();
    
    console.log('行数：　%s',rowIndex);
    console.log('列数：　%s',columnIndex);
    
    for(let k = 0; k < rowIndex; k++){
      for(let l = 0; l < columnIndex; l++){
        
        /**/
        const innertable  = slides[i].getTables()[j].getCell(k, l).getText().asString();
        const results     = deepltranslate(innertable, 'en', 'ja');
        
        console.log('getCell(%s, %s)　元のテキスト：　%S',k,l,innertable);
        console.log('getCell(%s, %s)　翻訳後のテキスト：　%S',k,l,results);
        
        
        if(innertable === ''){continue}
        slides[i].getTables()[j].getCell(k, l).getText().setText(results);
      }//for_l
    }//for_k
  }//for_j
}//for_i
}

function specifiedPageTranslateDeepL() {
const presentation  = SlidesApp.getActivePresentation();
const slides        = presentation.getSlides();

/*画面の操作を変化させるための記述*/
const ui            = SlidesApp.getUi();
const response      = ui.prompt(
  '翻訳こんにゃく',
  '翻訳したいスライドのページ番号を入力してください。',
  ui.ButtonSet.OK_CANCEL
);

//スライドページは配列で取得されるため
let page = response.getResponseText();
page -= 1;

switch(response.getSelectedButton()){
  case ui.Button.OK:
    console.log('ページ番号は、%s です。', page);
    break;
 
  case ui.Button.CANCEL:
    console.log('キャンセルが押されたため、処理を中断しました。');
    break;
    
  case ui.Button.CLOSE:
    console.log('閉じるボタンが押されました。');
}

for(let j = 0; j < slides[page].getPageElements().length; j++){
  /*console.log(slides[page].getPageElements()[j].getPageElementType().toString())*/
  if (slides[page].getPageElements()[j].getPageElementType().toString() == 'SHAPE') {

    const contents = slides[page].getPageElements()[j].asShape().getText().asString();
  
    /*スピーカーノートの取得 翻訳前のテキストをスピーカノートに入れる*/
    const notes    = slides[page].getNotesPage();
    notes.getSpeakerNotesShape().getText().appendText("\n" + contents);
  
    /*翻訳後のテキストを貼り付け*/
    const results  = deepltranslate(contents, 'en', 'ja');
    slides[page].getPageElements()[j].asShape().getText().setText(results);
  } 
  else if (slides[page].getPageElements()[j].getPageElementType().toString() == 'GROUP'){
    const elemsInGroup = slides[page].getPageElements()[j].asGroup().getChildren()
    for(const elem of elemsInGroup) {
      /*console.log(elem.getPageElementType().toString());*/
      if (elem.getPageElementType().toString() === 'SHAPE') {
        const contents = elem.asShape().getText().asString();
  
        /*スピーカーノートの取得 翻訳前のテキストをスピーカノートに入れる*/
        const notes    = slides[page].getNotesPage();
        notes.getSpeakerNotesShape().getText().appendText("\n" + contents);
  
        /*翻訳後のテキストを貼り付け*/
        const results  = deepltranslate(contents, 'en', 'ja');
        elem.asShape().getText().setText(results);
      }
      else if (elem.getPageElementType().toString() === 'GROUP') {
        const elemsInGroup2 = elem.asGroup().getChildren()
        for(const elem2 of elemsInGroup2) {
          console.log(elem2.getPageElementType().toString());
          if (elem2.getPageElementType().toString() === 'SHAPE') {
            const contents = elem2.asShape().getText().asString();
  
            /*スピーカーノートの取得 翻訳前のテキストをスピーカノートに入れる*/
            const notes    = slides[page].getNotesPage();
            notes.getSpeakerNotesShape().getText().appendText("\n" + contents);
  
            /*翻訳後のテキストを貼り付け*/
            const results  = deepltranslate(contents, 'en', 'ja');
            elem2.asShape().getText().setText(results);
          }
        }
      }
    }
  }
}//for_j

/*特定のページ番号を取得*/
for(let i = 0; i < slides[page].getTables().length; i++){
  
  const rowIndex    = slides[page].getTables()[i].getNumRows();
  const columnIndex = slides[page].getTables()[i].getNumColumns();
  
  console.log('行数：　%s',rowIndex);
  console.log('列数：　%s',columnIndex);
  
  for(let j = 0; j < rowIndex; j++){
    for(let k = 0; k < columnIndex; k++){
      
      /**/
      const innertable  = slides[page].getTables()[i].getCell(j, k).getText().asString();
      const results     = deepltranslate(innertable, 'en', 'ja');
      
      console.log('getCell(%s, %s)　元のテキスト：　%S',j,k,innertable);
      console.log('getCell(%s, %s)　翻訳後のテキスト：　%S',j,k,results);
      
      if(innertable === ''){continue}
      slides[page].getTables()[i].getCell(j, k).getText().setText(results);
    }//for_k
  }//for_j
}//for_i
}

/*スライド内容全体を英語に翻訳する。表や図形（シェイプ）の上に書かれた文言については翻訳されない。*/
function translateAll() {
const presentation  = SlidesApp.getActivePresentation();
const slides        = presentation.getSlides();

console.log(presentation.getName());
console.log(slides);
console.log('スライドの枚数: %s',slides.length);

for(let i = 0; i < slides.length; i++){
  for(let j = 0; j < slides[i].getPageElements().length; j++){
    /*console.log(slides[i].getPageElements()[j].getPageElementType().toString())*/
    if (slides[i].getPageElements()[j].getPageElementType().toString() == 'SHAPE') {

      const contents = slides[i].getPageElements()[j].asShape().getText().asString();
    
      /*スピーカーノートの取得 翻訳前のテキストをスピーカノートに入れる*/
      const notes    = slides[i].getNotesPage();
      notes.getSpeakerNotesShape().getText().appendText("\n" + contents);
    
      /*翻訳後のテキストを貼り付け*/
      const results  = LanguageApp.translate(contents,'en','ja');
      slides[i].getPageElements()[j].asShape().getText().setText(results);
    } 
    else if (slides[i].getPageElements()[j].getPageElementType().toString() == 'GROUP'){
      const elemsInGroup = slides[i].getPageElements()[j].asGroup().getChildren()
      for(const elem of elemsInGroup) {
        /*console.log(elem.getPageElementType().toString());*/
        if (elem.getPageElementType().toString() === 'SHAPE') {
          const contents = elem.asShape().getText().asString();
    
          /*スピーカーノートの取得 翻訳前のテキストをスピーカノートに入れる*/
          const notes    = slides[i].getNotesPage();
          notes.getSpeakerNotesShape().getText().appendText("\n" + contents);
    
          /*翻訳後のテキストを貼り付け*/
          const results  = LanguageApp.translate(contents,'en','ja');
          elem.asShape().getText().setText(results);
        }
        else if (elem.getPageElementType().toString() === 'GROUP') {
          const elemsInGroup2 = elem.asGroup().getChildren()
          for(const elem2 of elemsInGroup2) {
            console.log(elem2.getPageElementType().toString());
            if (elem2.getPageElementType().toString() === 'SHAPE') {
              const contents = elem2.asShape().getText().asString();
    
              /*スピーカーノートの取得 翻訳前のテキストをスピーカノートに入れる*/
              const notes    = slides[i].getNotesPage();
              notes.getSpeakerNotesShape().getText().appendText("\n" + contents);
    
              /*翻訳後のテキストを貼り付け*/
              const results  = LanguageApp.translate(contents,'en','ja');
              elem2.asShape().getText().setText(results);
            }
          }
        }
      }
    }
  }//for_j
}//for_i
/*テーブル内も変換出来るようにする*//*全スライドの取得*/
for(let i = 0; i < slides.length; i++){
  for(let j = 0; j < slides[i].getTables().length; j++){
    
    console.log('スライド　%s　ページ目には、表　%sがあります。',i+1,slides[i].getTables());
    
    const rowIndex    = slides[i].getTables()[j].getNumRows();
    const columnIndex = slides[i].getTables()[j].getNumColumns();
    
    console.log('行数：　%s',rowIndex);
    console.log('列数：　%s',columnIndex);
    
    for(let k = 0; k < rowIndex; k++){
      for(let l = 0; l < columnIndex; l++){
        
        /**/
        const innertable  = slides[i].getTables()[j].getCell(k, l).getText().asString();
        const results     = LanguageApp.translate(innertable,'en','ja');
        
        console.log('getCell(%s, %s)　元のテキスト：　%S',k,l,innertable);
        console.log('getCell(%s, %s)　翻訳後のテキスト：　%S',k,l,results);
        
        
        if(innertable === ''){continue}
        slides[i].getTables()[j].getCell(k, l).getText().setText(results);
      }//for_l
    }//for_k
  }//for_j
}//for_i
}//end


/*特定のページに存在する表内のテキストを翻訳する*/
function specifiedPageTranslate() {
const presentation  = SlidesApp.getActivePresentation();
const slides        = presentation.getSlides();

/*画面の操作を変化させるための記述*/
const ui            = SlidesApp.getUi();
const response      = ui.prompt(
  '翻訳こんにゃく',
  '翻訳したいスライドのページ番号を入力してください。',
  ui.ButtonSet.OK_CANCEL
);

//スライドページは配列で取得されるため
let page = response.getResponseText();
page -= 1;

switch(response.getSelectedButton()){
  case ui.Button.OK:
    console.log('ページ番号は、%s です。', page);
    break;
 
  case ui.Button.CANCEL:
    console.log('キャンセルが押されたため、処理を中断しました。');
    break;
    
  case ui.Button.CLOSE:
    console.log('閉じるボタンが押されました。');
}

for(let j = 0; j < slides[page].getPageElements().length; j++){
  /*console.log(slides[page].getPageElements()[j].getPageElementType().toString())*/
  if (slides[page].getPageElements()[j].getPageElementType().toString() == 'SHAPE') {

    const contents = slides[page].getPageElements()[j].asShape().getText().asString();
  
    /*スピーカーノートの取得 翻訳前のテキストをスピーカノートに入れる*/
    const notes    = slides[page].getNotesPage();
    notes.getSpeakerNotesShape().getText().appendText("\n" + contents);
  
    /*翻訳後のテキストを貼り付け*/
    const results  = LanguageApp.translate(contents,'en','ja');
    slides[page].getPageElements()[j].asShape().getText().setText(results);
  } 
  else if (slides[page].getPageElements()[j].getPageElementType().toString() == 'GROUP'){
    const elemsInGroup = slides[page].getPageElements()[j].asGroup().getChildren()
    for(const elem of elemsInGroup) {
      /*console.log(elem.getPageElementType().toString());*/
      if (elem.getPageElementType().toString() === 'SHAPE') {
        const contents = elem.asShape().getText().asString();
  
        /*スピーカーノートの取得 翻訳前のテキストをスピーカノートに入れる*/
        const notes    = slides[page].getNotesPage();
        notes.getSpeakerNotesShape().getText().appendText("\n" + contents);
  
        /*翻訳後のテキストを貼り付け*/
        const results  = LanguageApp.translate(contents,'en','ja');
        elem.asShape().getText().setText(results);
      }
      else if (elem.getPageElementType().toString() === 'GROUP') {
        const elemsInGroup2 = elem.asGroup().getChildren()
        for(const elem2 of elemsInGroup2) {
          console.log(elem2.getPageElementType().toString());
          if (elem2.getPageElementType().toString() === 'SHAPE') {
            const contents = elem2.asShape().getText().asString();
  
            /*スピーカーノートの取得 翻訳前のテキストをスピーカノートに入れる*/
            const notes    = slides[page].getNotesPage();
            notes.getSpeakerNotesShape().getText().appendText("\n" + contents);
  
            /*翻訳後のテキストを貼り付け*/
            const results  = LanguageApp.translate(contents,'en','ja');
            elem2.asShape().getText().setText(results);
          }
        }
      }
    }
  }
}//for_j

/*特定のページ番号を取得*/
for(let i = 0; i < slides[page].getTables().length; i++){
  
  const rowIndex    = slides[page].getTables()[i].getNumRows();
  const columnIndex = slides[page].getTables()[i].getNumColumns();
  
  console.log('行数：　%s',rowIndex);
  console.log('列数：　%s',columnIndex);
  
  for(let j = 0; j < rowIndex; j++){
    for(let k = 0; k < columnIndex; k++){
      
      /**/
      const innertable  = slides[page].getTables()[i].getCell(j, k).getText().asString();
      const results     = LanguageApp.translate(innertable,'en','ja');
      
      console.log('getCell(%s, %s)　元のテキスト：　%S',j,k,innertable);
      console.log('getCell(%s, %s)　翻訳後のテキスト：　%S',j,k,results);
      
      if(innertable === ''){continue}
      slides[page].getTables()[i].getCell(j, k).getText().setText(results);
    }//for_k
  }//for_j
}//for_i
}//end
```

以上。