AIコード生成の練習:fedibird.comにDeepL日本語翻訳ボタンを追加するユーザースクリプト

のえる @noellabo@hackers.pub

簡単で実用的なコードをAIに書かせる練習です。

Perplexityでo3をエンジンに指定し、FedibirdのWebUIにDeepLの翻訳ボタンを表示して翻訳機能を追加するユーザースクリプトを、AIに言葉で指示を与えることで生成できるのか、実際に試してみました。

だいたい一発で目的のコードに近いものを吐き出してくれるのですが、ところどころ間違えたコードを吐きます。修正すれば動きますが、どうせなら必要な情報を最初から与えて、そのまま動くコードを吐き出させてみたかったので、指示の詳細度をあげていきました。最終的に、一切修正せずに動くコードが完成しました。

DeepLのAPIキーを取得して指定すれば動きますので、これ、使ってみたい人は使ってみてください。Mastodonの現行版は内蔵している機能ですが、Fedibirdはいまのところサポートしていないので、代わりになります。

与えた指示

FedibirdのWebUIに翻訳機能を追加するユーザースクリプトを書いてください。
PCのブラウザの拡張でユーザースクリプトを実行するTampermonkey/Greasemonkey用を想定しています。

対象となるサイトはfedibird.comとnightly.fedibird.comとします。
WebUIが対象なので、パスは/web/*です。

投稿の言語が日本語ではない場合にだけ、投稿の下に「翻訳」ボタンを追加します。
翻訳ボタンはDeepLの無料APIを呼び出し、原文の直後に訳文を追加します。
翻訳ボタンの挿入位置は投稿のdivの次、
翻訳済みの時は表示を隠すトグルスイッチにします。
訳文にはlang属性で日本語を明示します。
スタイルはCSP対策で既存のクラスだけを使用します。
インラインスタイルは使用不可。
訳文の表示状態は、表示時に<div class="status__content__text--visible">を指定し、隠す時にclassを外すように実装してください。

DeepLのAPIキーは構成をシンプルにするためスクリプトに直書きします。
GM_xmlhttpRequestを使ってOK。

FedibirdのDOMでは、

投稿は<article>または<div class="detailed-status">、
本文は<div class="status__content__text">、
言語は本文のdivのlang属性で判定、

となります。

AIが生成したコード

// ==UserScript==
// @name         Fedibird DeepL Translator
// @namespace    https://fedibird.com/
// @version      0.9
// @description  Add a DeepL translation toggle for non-Japanese posts on Fedibird WebUI
// @match        https://fedibird.com/web/*
// @match        https://nightly.fedibird.com/web/*
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// ==/UserScript==

(() => {
  "use strict";

  /* DeepL Free API キー(必ず自分のキーに置き換えてください) */
  const DEEPL_API_KEY = "YOUR_FREE_API_KEY_HERE";

  /* 監視対象の投稿セレクタ */
  const ARTICLE_SELECTOR =
    'article:not(.fb-translate-processed), div.detailed-status:not(.fb-translate-processed)';

  /* 「翻訳」ボタンを作成 */
  function createTranslateButton() {
    const btn = document.createElement("button");
    btn.type = "button";
    btn.textContent = "翻訳";
    // 既存クラスで Mastodon のボタン UI に合わせる
    btn.className = "button button--link status__cta";
    return btn;
  }

  /* DeepL API で翻訳を取得 */
  function fetchTranslation(text) {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: "POST",
        url: "https://api-free.deepl.com/v2/translate",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        data:
          `auth_key=${encodeURIComponent(DEEPL_API_KEY)}` +
          `&text=${encodeURIComponent(text)}` +
          "&target_lang=JA",
        onload: (res) => {
          try {
            const obj = JSON.parse(res.responseText);
            resolve(obj.translations[0].text);
          } catch (e) {
            reject(e);
          }
        },
        onerror: reject,
      });
    });
  }

  /* 訳文を DOM に追加 */
  function insertTranslation(statusTextDiv, jpText) {
    const translationDiv = document.createElement("div");
    translationDiv.className = "status__content__text status__content__text--visible"; // 表示状態
    translationDiv.setAttribute("lang", "ja");
    translationDiv.textContent = jpText;
    statusTextDiv.after(translationDiv);
    return translationDiv;
  }

  /* 各投稿を処理 */
  async function processArticle(article) {
    article.classList.add("fb-translate-processed");

    // 本文と lang を取得
    const textDiv = article.querySelector(".status__content__text");
    if (!textDiv) return;

    const lang = textDiv.getAttribute("lang") || "und";
    if (lang.startsWith("ja")) return; // 日本語はスキップ

    const translateBtn = createTranslateButton();
    textDiv.after(translateBtn);

    let translationDiv = null;
    translateBtn.addEventListener("click", async () => {
      if (!translationDiv) {
        translateBtn.textContent = "翻訳中…";
        try {
          const jpText = await fetchTranslation(textDiv.textContent.trim());
          translationDiv = insertTranslation(textDiv, jpText);
          translateBtn.textContent = "非表示";
        } catch {
          translateBtn.textContent = "エラー再試行";
        }
      } else {
        const visible = translationDiv.classList.toggle(
          "status__content__text--visible"
        );
        translateBtn.textContent = visible ? "非表示" : "表示";
      }
    });
  }

  /* MutationObserver で動的ロード分を監視 */
  const observer = new MutationObserver((mutations) => {
    mutations.forEach(() => {
      document
        .querySelectorAll(ARTICLE_SELECTOR)
        .forEach((article) => processArticle(article));
    });
  });

  /* 初期ロード分 */
  document.querySelectorAll(ARTICLE_SELECTOR).forEach(processArticle);

  /* 監視開始 */
  observer.observe(document.body, { childList: true, subtree: true });
})();
1

No comments

If you have a fediverse account, you can comment on this article from your own instance. Search https://hackers.pub/ap/articles/0199761f-478f-7332-9f6b-e6254b9d1d70 on your instance and reply to it.