はじめに
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 以外、シークレットを管理する必要がありません。
| 翻訳先 | 受け付けるリアクション例 |
|---|---|
| 英語 :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 を作成
- https://api.slack.com/apps → Create New App → From scratch
2.OAuth & Permissions で以下のBot Token Scopesを追加:
-
chat:write,chat:write.customize,reactions:read,channels:history,groups:history
3.Install to Workspace → 認可 → xoxb-... トークンと Signing Secret を控える
2. Cloudflareにシークレット登録
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 を有効化
Slack App → Event Subscriptions:
- Enable Events をON
- Request URL に上記のWorker URLを貼る → Verified ✓ になるはず
-
Subscribe to bot events に
reaction_addedを追加 → Save Changes
これで完成。テストチャンネルに bot を /invite してリアクションを付ければ動作します。
コア実装
全部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 App → Reinstall to Workspace で即座にローテーション可能です。
3. m2m100 の言語名
@cf/meta/m2m100-1.2b は ISO コードではなく 言語名 (english, japanese, korean, ...) を受け付けます。
全100言語対応なので、言語追加は REACTION_TO_LANG と detectLang() の拡張だけで済みます。
開発手法: 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 install → typecheck → Slack App作成手順案内 → wrangler secret put → wrangler 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分
リポジトリ:









