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 });
})();