0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

たった10分で作る、Slackリアクション翻訳Bot(Cloudflare Workers AI・完全無料)

0
Last updated at Posted at 2026-04-28

はじめに

Slackで「このメッセージを英訳/和訳したい」というシーンで、毎回コピペして翻訳サイトに貼るのは面倒です。
そこで メッセージにリアクションを付けるだけで、スレッドに翻訳を返してくれるbot を作りました。


お知らせ(採用情報)

AppTime では一緒に働くメンバーを募集しております。
詳しくは採用情報ページをご確認ください。

みなさまからのご応募をお待ちしております。


過去にも同様の記事は多数あり、多くは Google Apps Script + Google翻訳 の構成です。
特にkazuhi-sさんのこちらの記事を参考にさせていただきました。今回はそれを次のように再構築しました。

参考記事 本記事の構成
実行基盤 GAS Cloudflare Workers
翻訳エンジン LanguageApp.translate Cloudflare Workers AI (@cf/meta/m2m100-1.2b)
言語 JavaScript TypeScript
外部APIキー 不要 不要
月額コスト 0円 0円 (通常利用)
デプロイ スクリプトエディタ wrangler deploy ワンコマンド

ポイントは 「Workers AI を使うことで翻訳APIキーを一切持たない」 ことです。
Slack の Bot Token と Signing Secret 以外、シークレットを管理する必要がありません。

完成イメージ:
スクリーンショット 2026-04-28 12.07.01.png

翻訳先 受け付けるリアクション例
英語 :flag-us: :flag-us: :us: :gb: :english:
日本語 :flag-jp: :flag-jp: :jp: :japanese:
韓国語 :flag-kr: :flag-kr: :kr: :korean:
中国語 :flag-cn: :flag-cn: :cn: :chinese:
ベトナム語 :flag-vn: :flag-vn: :vn: :vietnamese:

リアクションを付けると、スレッドに国旗絵文字付きで翻訳が返ります。
元メッセージの言語はUnicode範囲で自動判定するので、ソース言語の指定は不要です。


アーキテクチャ

Slack (リアクション)
   │  Event Subscription (reaction_added)
   ▼
Cloudflare Worker
   │  ① Slack署名を検証 (HMAC-SHA256)
   │  ② conversations.replies で元メッセージを取得
   │  ③ 既に同言語の翻訳がスレッドにあるかチェック
   │  ④ ソース言語をUnicode範囲で判定
   │  ⑤ Workers AI (m2m100) で翻訳
   │       └─ Slack記法 (<@U…>, :emoji:, <https://…|label>) は
   │          プレースホルダーで保護してから翻訳→復元
   │  ⑥ chat.postMessage でスレッド返信
   ▼
Slack (翻訳結果)

Slack の Event API は 3秒以内に200を返す必要がある ので、翻訳処理は ctx.waitUntil() でバックグラウンド実行します。


セットアップ

0. プロジェクト作成

mkdir slack-translate-bot && cd $_
npm init -y
npm install -D wrangler typescript @cloudflare/workers-types

wrangler.jsonc:

{
  "name": "slack-translate-bot",
  "main": "src/index.ts",
  "compatibility_date": "2025-11-01",
  "compatibility_flags": ["nodejs_compat"],
  "ai": { "binding": "AI" }
}

ai.binding を書くだけで Workers AI が env.AI から呼べるようになります。ここがミソで、APIキーの登録もURLの組み立ても不要です。

1. Slack App を作成

  1. https://api.slack.com/appsCreate New AppFrom scratch

スクリーンショット 2026-04-28 11.17.47.png

スクリーンショット 2026-04-28 11.18.03.png
スクリーンショット 2026-04-28 11.18.15.png

2.OAuth & Permissions で以下のBot Token Scopesを追加:

  • chat:write, chat:write.customize, reactions:read, channels:history, groups:history

スクリーンショット 2026-04-28 11.31.55.png

3.Install to Workspace → 認可 → xoxb-... トークンと Signing Secret を控える

スクリーンショット 2026-04-28 11.20.01.png

2. Cloudflareにシークレット登録

スクリーンショット 2026-04-28 11.36.20.png

npx wrangler login
npx wrangler secret put SLACK_BOT_TOKEN       # xoxb-...
npx wrangler secret put SLACK_SIGNING_SECRET  # Slack の Signing Secret

シークレットは2つだけ。翻訳API用のキーが不要なのが Workers AI 構成の最大のメリット。

3. デプロイ

npx wrangler deploy

https://slack-translate-bot.<your-account>.workers.dev のURLが発行されます。

4. Event Subscriptions を有効化

スクリーンショット 2026-04-28 11.42.01.png

Slack App → Event Subscriptions:

  • Enable Events をON
  • Request URL に上記のWorker URLを貼る → Verified ✓ になるはず
  • Subscribe to bot eventsreaction_added を追加 → Save Changes

スクリーンショット 2026-04-28 11.42.47.png

これで完成。テストチャンネルに bot を /invite してリアクションを付ければ動作します。

スクリーンショット 2026-04-28 11.48.13.png


コア実装

全部1ファイル (src/index.ts) で230行ほど。要点だけ抜粋して解説します。

Worker のエントリポイント

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    const body = await request.text();
    const signature = request.headers.get("x-slack-signature") ?? "";
    const timestamp = request.headers.get("x-slack-request-timestamp") ?? "";

    // 署名検証 (HMAC-SHA256)
    if (!await verifySlackSignature(body, timestamp, signature, env.SLACK_SIGNING_SECRET)) {
      return new Response("Unauthorized", { status: 401 });
    }

    const payload = JSON.parse(body);

    // SlackからのURL検証チャレンジ
    if (payload.type === "url_verification") {
      return new Response(payload.challenge, { headers: { "Content-Type": "text/plain" } });
    }

    // 3秒制限を守るため、翻訳は背景で
    if (payload.type === "event_callback" && payload.event?.type === "reaction_added") {
      ctx.waitUntil(handleReaction(payload.event, env));
    }
    return new Response("OK");
  },
};

ctx.waitUntil() を使うと「Response を返した後も非同期処理を継続できる」ので、Slackの3秒制限を確実にクリアできます。

多言語対応マッピング

type SupportedLang = "english" | "japanese" | "korean" | "chinese" | "vietnamese";

const EN = { language: "english" as const,    prefix: ":flag-us:" };
const JA = { language: "japanese" as const,   prefix: ":flag-jp:" };
const KO = { language: "korean" as const,     prefix: ":flag-kr:" };
const ZH = { language: "chinese" as const,    prefix: ":flag-cn:" };
const VI = { language: "vietnamese" as const, prefix: ":flag-vn:" };

const REACTION_TO_LANG: Record<string, typeof EN> = {
  english: EN, en: EN, us: EN, gb: EN, "flag-us": EN, "flag-gb": EN,
  japanese: JA, jp: JA, "flag-jp": JA,
  korean: KO,   kr: KO, "flag-kr": KO,
  chinese: ZH,  cn: ZH, "flag-cn": ZH,
  vietnamese: VI, vn: VI, "flag-vn": VI,
};

ワークスペースによって :vn: が国旗としてレンダリングされない場合があるので、Unicode CLDR標準の :flag-XX: 形式を bot 返信のプレフィックスに採用 しました。トリガー側は :vn: :vietnamese: などの別名も全て受け付けます。

Unicode範囲によるソース言語判定

function detectLang(text: string): SupportedLang {
  if (/[가-힯ᄀ-ᇿ㄰-㆏]/.test(text)) return "korean";       // Hangul
  if (/[぀-ゟ゠-ヿ]/.test(text)) return "japanese";          // ひらがな・カタカナ
  if (/[一-鿿㐀-䶿]/.test(text)) return "chinese";           // CJK 漢字 (かな無し)
  if (/[Ḁ-ỿ]/.test(text)) return "vietnamese";              // Vietnamese 特有のダイアクリティクス
  return "english";
}

判定順序が重要で、Hangul → かな → CJK の順にすると次のような誤判定を避けられます:

  • 韓国語に少しでも漢字が混じっていても Hangul を優先 → 韓国語
  • 日本語の漢字+かな文 → かなを検出 → 日本語
  • かなが無い CJK のみ → 中国語
  • 西欧文字でベトナム特有のダイアクリティクス (ằ ặ ể ễ ệ ụ ư đ など) → ベトナム語
  • それ以外のラテン文字 → 英語

LLMで言語判定すると遅くて課金もされますが、Unicode範囲なら正規表現一発で十分です。

Workers AI で翻訳 (Slack記法保護)

const SLACK_TOKEN_RE = /<[^>]+>|:[a-z0-9_+-]+:/g;

async function translate(text: string, sourceLang: SupportedLang, targetLang: SupportedLang, ai: Ai) {
  // <@U…> や :emoji: をプレースホルダーに置換
  const tokens: string[] = [];
  const masked = text.replace(SLACK_TOKEN_RE, (match) => {
    const idx = tokens.length;
    tokens.push(match);
    return `[[${idx}]]`;
  });

  const response = await ai.run("@cf/meta/m2m100-1.2b", {
    text: masked,
    source_lang: sourceLang,
    target_lang: targetLang,
  }) as { translated_text: string };

  // [[0]] [[1]] を元のSlack記法に戻す
  return response.translated_text.replace(
    /\[\[(\d+)\]\]/g,
    (_, i) => tokens[Number(i)] ?? ""
  );
}

m2m100 は翻訳専用モデルなので、LLMのように「Slack記法を翻訳しないで」とプロンプトで指示することができません。
そこで 正規表現で Slack記法をマスクし、翻訳後に復元する アプローチを取りました。

入力:  「<@U123ABC> こんにちは :wave:」
↓ マスク
       「[[0]] こんにちは [[1]]」
↓ 翻訳 (ja → en)
       「[[0]] hello [[1]]」
↓ 復元
出力:  「<@U123ABC> hello :wave:」

これで :emoji:<@user> メンション・ <https://...|リンク> などが翻訳結果でも崩れずに保持されます。

Slack署名検証 (Web Crypto API)

async function verifySlackSignature(body, timestamp, signature, signingSecret) {
  // リプレイ攻撃対策: 5分以上ズレてたら拒否
  if (Math.abs(Math.floor(Date.now() / 1000) - parseInt(timestamp, 10)) > 300) return false;

  const baseString = `v0:${timestamp}:${body}`;
  const key = await crypto.subtle.importKey(
    "raw", new TextEncoder().encode(signingSecret),
    { name: "HMAC", hash: "SHA-256" }, false, ["sign"],
  );
  const sigBuf = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(baseString));
  const computed = "v0=" + Array.from(new Uint8Array(sigBuf))
    .map((b) => b.toString(16).padStart(2, "0")).join("");

  return timingSafeEqual(computed, signature);
}

Cloudflare Workers では Node.js の crypto.createHmac は使えませんが、Web Crypto API (crypto.subtle) が標準で利用可能です。
Node.jsのcryptoをimportする例をネットで見ると古い書き方が多いので注意。


コスト

通常利用なら 完全無料 です。

サービス 無料枠 超過時
Cloudflare Workers 100,000 リクエスト / 日 $5/月から
Workers AI 10,000 ニューロン / 日 $0.011 / 1,000ニューロン

m2m100 で短文を翻訳する場合 1回あたり 5〜50ニューロン程度。
1日数百〜数千回 までは無料枠内に収まります。

ゼロ円を絶対死守したい場合は Cloudflare Dashboard で Daily neuron limit を9,000等に設定すればOK。


ハマりどころと対策

1. :vn: が国旗として表示されないワークスペースがある

Slack のワークスペース設定や絵文字パックによって :vn: が国旗としてレンダリングされない場合があります。
解決策: Unicode CLDR標準の :flag-vn: を使う。bot の返信プレフィックスはこちらに統一しました。
トリガーは :vn: :flag-vn: :vietnamese: の3つ全部受け付けるよう冗長化しています。

2. Bot Tokenを-secret putの引数に渡してしまう事故

# NG: 引数として渡せない
wrangler secret put SLACK_BOT_TOKEN xoxb-...
# OK: コマンドだけ実行→対話プロンプトで貼り付け
wrangler secret put SLACK_BOT_TOKEN
# Enter a secret value: › ▮

シェルヒストリにトークンが残るのを避けるため、wrangler は対話入力のみを許容しています。
万が一漏らしてしまったら、Slack App → Install AppReinstall to Workspace で即座にローテーション可能です。

3. m2m100 の言語名

@cf/meta/m2m100-1.2b は ISO コードではなく 言語名 (english, japanese, korean, ...) を受け付けます。
全100言語対応なので、言語追加は REACTION_TO_LANGdetectLang() の拡張だけで済みます。


開発手法: Claude Code でバイブコーディング

実は本記事の bot は、ほぼ全工程を Claude Code (Anthropic公式CLI) とのバイブコーディングで実装しました。
バイブコーディングとは、AIに高レベルな指示を出して実装を任せ、生成結果を確認しながらノリ (vibe) で進める開発手法です。

実際の対話を時系列で振り返ります。引用 ( > ) はこちらが Claude に投げた一言、地の文がそれに対する Claude の動きです。

Phase 1: 初期実装 (最初は Claude API 版だった)

/Users/developer/slack-translate-bot/README.md進めたい

最初の状態では、元記事 (@kazuhi-s さんのGAS版) を Cloudflare Workers + TypeScript に置き換えて、翻訳エンジンに Claude Haiku 4.5 API を使う設計が src/index.ts 1ファイル(約230行)で出来上がっていました。

  • fetch ハンドラ + ctx.waitUntil() で3秒制限を確実にクリア
  • crypto.subtle で Slack署名 (HMAC-SHA256) を検証
  • conversations.replies で元メッセージ取得 → chat.postMessage で返信
  • 重複翻訳をプレフィックスで検知してスキップ

npm installtypecheck → Slack App作成手順案内 → wrangler secret putwrangler deploy まで、Claude がチャット上で逐次手順を出してくれるので、自分で資料を行ったり来たりせず一直線で進められました。

Phase 2: 「無料でできないの?」のひと言で全面リプレース

デプロイ後、API課金が気になってこう聞きました:

無料でできないの?

Claude は4つの代替案を比較表で提示:

  • Cloudflare Workers AI (@cf/meta/m2m100-1.2b) ← 採用
  • DeepL API Free
  • GAS + LanguageApp.translate
  • LibreTranslate

「Cloudflare Workers AI」と返すだけで、

  • @anthropic-ai/sdk 依存を package.json から削除
  • wrangler.jsonc"ai": { "binding": "AI" } を追加
  • translate()env.AI.run("@cf/meta/m2m100-1.2b", ...) に書き換え
  • 言語の自動判定が必要になるので detectLang() を新設

がまとめて行われ、バンドルサイズが 148KiB → 25KiB (-83%) に縮小しました。

このとき出色だったのは、Claude が m2m100 の制約 (LLMじゃないのでプロンプトで「Slack記法を翻訳しないで」と指示できない) を察して、

正規表現で Slack記法 (<@U…> :emoji: <URL|label>) を [[0]] [[1]] … でマスク → 翻訳 → 復元する

というアプローチを 頼んでもいないのに入れてくれたことです。

Phase 3: 多言語拡張もひと言

英語の他に韓国語、中国、ベトナム語

これだけで:

  • REACTION_TO_LANG に韓・中・ベトナムを追加
  • detectLang() を Hangul → かな → CJK → Vietnamese diacritic → Latin の優先順位で拡張
  • :kr: :cn: :vn: は Slack 標準絵文字なのでカスタム追加不要」と補足

判定順序が大事で、Hangul → かな の順にしないと「韓国語に少しでも漢字が混じると中国語扱いされる」誤判定が起きる、というところまで Claude が考慮してきました。

Phase 4: 「ベトナムだけ動かない」のリアル・デバッグ

ベトナムだけ動かない

このひと言で Claude は3つの可能性 (リアクション名がワークスペースで未登録 / モデルがベトナム語を拒否 / イベント未到達) を列挙し、保険として :flag-vn: :vietnamese: を別名で受け付けるよう修正してデプロイ。

その後こちらが「翻訳はされたけど国旗が出ない」と返した瞬間、原因は 返信プレフィックスの :vn: がそのワークスペースで国旗としてレンダリングされていない ことだと特定し、Unicode CLDR 標準の :flag-vn: プレフィックスに変更。
ついでに英語・日本語も同じ仕組み (:flag-us: :flag-jp: プレフィックス + 複数のエイリアス受付) に統一されました。

バイブコーディングで気づいたこと

  • 要件変更にとにかく強い: Claude API 版 → Workers AI 版の全面書き換えも、3言語追加も、絵文字仕様の修正も、ぜんぶ1メッセージで完結

  • 細かい工夫を勝手に入れてくれる: Slack記法のプレースホルダー保護、Unicode 判定の順序、crypto.subtle の選定など

  • ハマりどころを先回り: Slack 3秒制限、wranglerシークレットを引数で渡せない仕様、m2m100の言語名仕様、:vn: の絵文字エイリアス問題、すべて Claude 側から「ここ注意」と教えてくれた

  • 注意点として、Bot Token などを誤ってチャットに貼ると履歴に残るので、ローテーション (Slack の Reinstall to Workspace) は必須

「インフラも API 仕様も全部覚えてられない」状態でも、Claude に伴走してもらえば10分で動くものが手に入る時代になりました。


まとめ

  • Cloudflare Workers + Workers AI で、Slackリアクション翻訳botを完全無料で動かせる
  • 外部翻訳APIキーが不要、シークレット管理が圧倒的に楽
  • Unicode範囲で言語自動判定 → ソース言語の指定不要
  • Slack記法はプレースホルダーで保護して翻訳エンジンに渡す
  • 230行のTypeScriptコード1ファイルで完結
  • Claude Code とのバイブコーディングで実装時間は約10分

リポジトリ:


参考

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?