見出し画像

NotionにDeepL APIで自分専用の単語帳を作ろう

最近使い始めたnotionというデータ管理ツールに自分専用の単語帳を作ってみたので、その作り方を記録しておく。

世界のどこかのNotionユーザーの参考になれば、かなり嬉しい。


概要

作るのは、Notionのデータベースに単語を入力すると、翻訳ツールのDeepLが和訳してくれて、それを自動でデータベースに反映することで出来上がる自分専用の単語帳。

図1.1:単語帳のイメージ

Google Apps ScriptでNotionとDeepLのAPIを使って作る。毎回手動でコードを実行するのは面倒なので、一定の間隔(1分とか1時間とか)で勝手に実行するように設定する。

APIとか言われても意味分からないです、という人が大半だと思うけれど、未だに全然分かってない僕でもどうになかったのできっと大丈夫。

一応説明すると、APIというのは何というか、機能を借りてくるみたいなイメージのもので、例えばDeepL APIを使うと、DeepLのサイトに行かなくても翻訳機能だけ借りてきて使わせてもらえる、みたいな感じ。たぶん。

ちなみに、この単語帳は入力したものをただdeepLに翻訳してもらっているだけなので、熟語でも文章でも和訳してくれるし、なんなら世界中のどの言語の言葉でも日本語に訳してくれる。もっと言うと、プログラムのコードを2文字変えれば翻訳結果を日本語以外にすることもできる。

なので、工夫すればフランス語の単語を入力すると日本語と英語の翻訳結果が表示される、みたいなこともできる。(コード書き換える必要があるのでやりたい人いたらコメントしてください。)

作り方

(以下の手順は全部パソコンでやってください。ちゃんと確認してないけれど、スマホやiPadだとできないはず。)

Google Apps Script作成

  1. まず、このリンクからGoogle Spread Sheetを作成する。https://docs.google.com/spreadsheets

  2. 上のほうにある「新しいスプレッドシートを作成」から「空白のスプレッドシート」を選んで開く。

  3. 開いたスプレッドシートの上部にある、「拡張機能」というところから「Apps Script」を選んで開く。

  4. Apps Scriptを開くと、

function myFunction() {
  
}

というものが書いてあると思うので、それを全部削除して、以下のコードをそのままコピー&ペーストする。

// Main function to run the translation process
function runTranslationProcess() {
  try {
    const lastCheckedTime = getLastCheckedTime();
    const notionPages = fetchNotionPages(lastCheckedTime);

    if (notionPages && notionPages.length > 0) {
      processNotionPages(notionPages);
    } else {
      Logger.log("No data found.");
    }
  } catch (error) {
    Logger.log('Error during translation process: ' + error);
  }
}

// Function to fetch pages from Notion
function fetchNotionPages(lastCheckedTime) {
  const databaseId = getScriptProperty('DATABASE_ID');
  const notionApiKey = getScriptProperty('NOTION_API_KEY');
  const url = `https://api.notion.com/v1/databases/${databaseId}/query`;

  const options = {
    method: 'post',
    headers: {
      'Authorization': `Bearer ${notionApiKey}`,
      'Content-Type': 'application/json',
      'Notion-Version': '2022-06-28'
    },
    payload: JSON.stringify({
      filter: {
        property: 'last edited time',
        created_time: {
          after: lastCheckedTime,
          before: '2100-02-01'
        }
      }
    }),
    sorts: [
      {
        property: 'last edited time',
        direction: 'ascending'
      }
    ],
    muteHttpExceptions: true
  };

  const response = UrlFetchApp.fetch(url, options);
  const data = JSON.parse(response.getContentText());
  Logger.log('Notion API Response:', JSON.stringify(data));

  return data.results || [];
}

// Function to process Notion pages
function processNotionPages(pages) {
  pages.forEach(processNotionPage);
}

// Function to process a single Notion page
function processNotionPage(page) {
  try {
    Logger.log('Page Properties:', JSON.stringify(page.properties));

    const words = page.properties['words'];
    if (!words || !words.title || !words.title[0]) {
      Logger.log("Word property not found.");
      return;
    }

    const wordText = words.title[0].text.content;
    if (!wordText) {
      Logger.log("Word title not found.");
      return;
    }

    if (!isWordTranslated(page.id)) {
      const translatedText = translateTextWithDeepL(wordText);
      const createdTime = page.properties['last edited time'].created_time;

      const properties = {
        'meanings': {
          'rich_text': [
            {
              'text': {
                'content': translatedText
              }
            }
          ]
        }
      };

      // Update page properties even if 'meanings' is empty
      updateNotionPageProperties(page.id, properties);
      saveLastCheckedTime(createdTime);

      // Mark the word as translated
      markWordAsTranslated(page.id);
    } else {
      Logger.log("Word is already translated.");
    }
  } catch (error) {
    Logger.log('Error processing Notion page: ' + error);
  }
}

// Function to check if a word is already translated
function isWordTranslated(pageId) {
  const translatedWords = getTranslatedWords();
  return translatedWords.includes(pageId);
}

// Function to translate text with DeepL
function translateTextWithDeepL(text) {
  const deepLApiKey = getScriptProperty('DEEPL_API_KEY');

  const url = `https://api-free.deepl.com/v2/translate?auth_key=${deepLApiKey}`;
  const payload = {
    'text': text,
    'target_lang': "JA"
  };

  const options = {
    'method': 'post',
    'payload': payload,
    'muteHttpExceptions': true
  };

  const response = UrlFetchApp.fetch(url, options);
  const jsonResponse = JSON.parse(response.getContentText());

  if (jsonResponse.translations && jsonResponse.translations.length > 0) {
    Logger.log(jsonResponse.translations);
    return jsonResponse.translations[0].text;
  }
}

// Function to update Notion page properties
function updateNotionPageProperties(pageId, properties) {
  const notionApiKey = getScriptProperty('NOTION_API_KEY');
  const url = `https://api.notion.com/v1/pages/${pageId}`;

  const options = {
    method: 'patch',
    headers: {
      'Authorization': `Bearer ${notionApiKey}`,
      'Content-Type': 'application/json',
      'Notion-Version': '2022-06-28'
    },
    payload: JSON.stringify({
      properties: properties
    }),
    muteHttpExceptions: true
  };

  const response = UrlFetchApp.fetch(url, options);
  const jsonResponse = JSON.parse(response.getContentText());
}

// Function to get the last checked time from the spreadsheet
function getLastCheckedTime() {
  const sheet = getSpreadsheetSheet();
  return sheet.getRange('A1').getValue();
}

// Function to save the last checked time in the spreadsheet
function saveLastCheckedTime(time) {
  const sheet = getSpreadsheetSheet();
  sheet.getRange('A1').setValue(time);
}

// Function to mark a word as translated in the spreadsheet
function markWordAsTranslated(pageId) {
  const sheet = getSpreadsheetSheet();
  const translatedWords = getTranslatedWords();
  translatedWords.push(pageId);
  sheet.getRange('B1').setValue(translatedWords.join(','));
}

// Function to get the translated words from the spreadsheet
function getTranslatedWords() {
  const sheet = getSpreadsheetSheet();
  const translatedWords = sheet.getRange('B1').getValue();
  return translatedWords ? translatedWords.split(',') : [];
}

// Helper function to get the spreadsheet sheet
function getSpreadsheetSheet() {
  return SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
}

// Helper function to get script property
function getScriptProperty(propertyName) {
  return PropertiesService.getScriptProperties().getProperty(propertyName);
}

ちなみに、このコードは僕とchatGPTの共同制作で、共同制作と言っても僕は指図しただけで99%はchatGPTが作ってくれたもので、コードに分からない部分があっても僕ではなくchatGPTに質問してくださいね。

完成したら、スプレッドシートに好きな名前(My Notion Dictionaryなど)をつける。

Notion APIキー取得

この先は、「暗号のような文字列を取得→Google Apps Scriptのプロパティに追加」という作業を3回繰り返す。
そろそろGoogle Apps Scriptと毎回書くのは面倒になってきたのでこの先ではGASと略すことにする。

1回目はNotion APIキーの取得。

  1. リンクを開き、自分のNotionアカウントにログイン。
    https://www.notion.so/my-integrations

  2. create new integrationを選択。指示に従って項目を埋めていく。
    integrationの名前は何でもよい。僕はとりあえず「my integration」とかにした。

  3. 基本情報を入力後、左端にある「Secrets」を選択。「Internal Integration Secret」にある「Show」をクリックして、表示された暗号のような文字列(Notion APIキー)をコピー、あるいはメモしておく。(図2.1参照)

  4. 先ほど作成したスプレッドシートから再度GASを開く

  5. 左端の歯車マークを押して設定画面を開き、1番下までスクロールして「スクリプト プロパティ」という項目を探す。

  6. スクリプト プロパティのプロパティに「NOTION_API_KEY」と入力し(「かぎかっこ」は付けない)、値に先ほどコピーしたNotion APIキーを入力する。(図2.2参照)

これでNotion APIの設定は完了。あと2回同じようなことを繰り返す。

図2.1:Notion APIキー取得
図2.2:GASのスクリプトプロパティにNotion APIキーを入力

DeepL APIキー取得

2回目はDeepLのAPIキー。

  1. DeepLのAPIを使うため、このリンクへ行く。
    https://www.deepl.com/ja/pro-api?cta=header-pro-api

  2. 開いたページから、無料版の「DeepL API Free」を選択。
    (お金に余裕ある人はぜひ有料プランをどうぞ)

  3. ページの指示に従って登録。途中で本人確認のためにカード情報が求められますが、無料プランから有料に変更しない限り1円も請求されないのでご安心を。一定期間経つと自動で有料プランになる、みたいなこともないので大丈夫。

  4. 登録を終えてログインし、「ご利用中のDeepLアカウント」と大きく書かれているマイページに行く。

  5. 上のほうに「プラン」、「アカウント」、「ご利用状況」というタブがあるので、アカウントを選んでクリック。

  6. 開いたページの下のほうに、「DeepL APIで使用する認証キー」という表示のところにある、暗号のような認証キーをコピー。

  7. Notion APIの時と同じように、スプレッドシートからGASを開き、スクリプト プロパティを追加する。今回は、プロパティを「DEEPL_API_KEY」として、値にコピーしていた「DeepL APIで使用する認証キー」を貼り付ける。

Nortonにデータベースを作成

3回目はNotionのデータベースID。
Notionのことを書いているnoteなのに、やっとここでNotionが出てきた。もう少しで完成なので頑張りましょう。

  1. Notion内で単語帳を作りたい階層・場所を決めて、+ボタンからテーブルビューを選択し、新しいデータベースを選んで作成。このデータベースが単語帳になる。

  2. データベース上部に「名前」と「タグ」というプロパティが表示されているので、「名前」を「Words」に変更。そして、「タグ」というプロパティを全部削除して、データベースに「Words」という列だけになるようにする(図2.3参照)

  3. 次に「Words」横にある+ボタンを押して「テキスト」を選択し、データベースの2列目に追加。プロパティの名前を「Meanings」に変更。(図2.3参照)

  4. Meaningsの横にある+ボタンを押して「作成時間」を選択し、プロパティの3列目に追加。プロパティの名前を「Created time」に変更。(図2.3参照)

  5. もう一度+ボタンを押して「最終更新日時」を選択し、4列目に追加。プロパティの名前をそれぞれ「Created time」と「Last edited time」に変更。(図2.3参照)

  6. (この「created time」と「last edited time」は非表示にしても大丈夫。でも削除してはダメです)

  7. 作成したテーブルの上のほうにマウスをかざすと青い「新規」というボタンが現れるので、その横の3つのドットを選択。下のほうにある「ビューのリンクをコピー」をクリックする。(図2.4参照)

  8. リンクがコピーされるので、そのコピーしたものをどこか好きな場所にペーストする。

  9. すると、https://www.notion.so/work-space/aaaabbbbccccdddd1111222233334444?v=○○○○みたいなものが出てくるので、この /から?v の間にある「abcd1234efgh5678abcd1234efgh5678」みたいな32文字の意味不明な文字列のみをコピーする。

  10. これまでのAPIキーと同じように、スプレッドシートを開いて、GASのスクリプトプロパティを追加する。今回は、プロパティを「DATABASE_ID」、値に先ほどコピーした32文字の文字列をペーストする。

  11. この時点で、GASのスクリプトプロパティに、「NOTION_API_KEY」 「DEEPL_API_KEY」「DATABASE_ID」の3つが登録されていれば完璧。

  12. 最後に、データベースを作ったページの一番上の右端にある3つのドットをクリックすると、下のほうに「コネクト」という欄があるので、コネクトの追加から、少し前に作成したNotionのintegrationを選択する。コネクト欄にintegrationが追加されていればOK

  13. (データベースの並び替えでWordsが昇順になるように設定すると、辞書のように単語が並べられる)

図2.3:データベース見本
図2.4:ビューのリンクをコピー

テスト

僕が手順を書き忘れてなければ、これで97%くらいは完成したはず。
ということで、ちゃんと動くかテストしてみましょう。

  1. まず、Notionに作成したデータベースのWordsという列に、好きな英単語や英熟語をいくつか書き込む。

  2. Google Apps Script (GAS) に戻って左端の選択欄からエディタを選択し、前に張り付けたプログラムのコードが書かれているページに移動する。

  3. デバッグと書いてあるところの横に「runTranslationProcess」と表示されていることを確認して、画面上部の「実行」をクリック。すると、画面下部のところに黄色く「実行完了」と表示されるはず。(もし赤い文字でエラーが出ていたら、ここまでの手順の何かが間違ってます…)

  4. Notionのページに戻ると、先ほど入力した単語の横に、意味が表示されている!(はず!)

  5. ちゃんと表示されていたら、GASの左端にある「トリガー」を選択。ここでは、翻訳を自動で実行する時間間隔を設定する。

  6. 図2.6を参考にして、実行する関数を「runTranslationProcess」、イベントソースを「時間主導型」に設定。トリガータイプと時間間隔はお好みで。図2.6 の設定だと、1分に1回翻訳が自動で実行されるようになっている。


図2.5:実行画面
図2.6:トリガー設定

完成!

なかなか大変な設定の連鎖でしたが、これで自分専用の単語帳が完成です!
明日、先日注文したハリーポッターの英語版小説が全巻届くので、わからない単語をこの単語帳に書き込んでいこうと思う。

みんなで英語の勉強頑張りましょう。

実行でエラーが出ちゃったり、うまくAPIキーが取得できなかったりした場合はコメント下さい。可能な限りお手伝いしようと思います。

この記事が気に入ったらサポートをしてみませんか?