1つのブログ記事を1つのエージェントで13言語に翻訳する方法
1つのTypeScriptエージェントがClaude APIを並列で呼び出し、英語記事を90秒以内に12言語へ翻訳します。文体保持にはシステムプロンプトを2部構成にする必要があります。まずスタイル制約、次にロケール固有のノート。Haikuでのコストは1記事あたり約$0.004〜$0.02。私のサイトは60日以内に国際トラフィックが34%増加しました。
毎週水曜。28,400人以上の読者。無駄なし。
✓ Check your inbox — click the confirmation link to complete sign-up.
✓ You're subscribed!
✓ You're already on the list.
目次
2026年5月更新。
TL;DR: 1つのTypeScriptエージェントがClaude APIを並列で呼び出し、英語記事を90秒以内に12言語へ翻訳します。文体保持にはシステムプロンプトを2部構成にする必要があります。まずスタイル制約、次にロケール固有のノート。Haikuでのコストは1記事あたり約$0.004〜$0.02。私のサイトは60日以内に国際トラフィックが34%増加しました。
[オペレーターの視点] 新しい記事を公開するたびにこのエージェントを実行しています。341本の記事を12言語で処理しましたが、翻訳を手動で触ったことは一度もありません。その仕組みを正確に説明します。
翻訳者を雇う代わりにエージェントを構築した理由
多言語SEOの議論は省きます。重要なのはわかっているはずです。私が抱えていた問題はワークフローでした。記事ごとに翻訳者を雇うのは高コスト($40〜$120/記事 × 12言語 = $480〜$1,440/記事)で、遅く(納期3〜7日)、341本の既存記事を一括処理するのは不可能です。
他に勧める人が多いのはGoogle TranslateやDeepLです。どちらも精度は高いですが、文体を壊してしまいます。私の文章スタイルは直接的で一人称、やや逆説的です。機械翻訳はすべてを形式的で受動的にする傾向があります。これはブランドの文体一貫性が重要な場合に問題になります。
そこで、Claude連携のTypeScriptエージェントを構築しました。mainへのマージごとにCIで実行し、翻訳を並列で展開し、ファイルをディスクに書き戻し、すでにファイルのあるロケールはスキップします。新規記事なら全体で90秒以内です。
プロジェクト構造
エージェントはscripts/agent/translate-worker.tsにあります。英語記事を読み込み、frontmatterを抽出し、ロケールごとに翻訳ジョブをディスパッチするオーケストレーターから呼び出されます。
scripts/
agent/
translate-worker.ts # ロケールごとの翻訳ロジック
translate-all.ts # オーケストレーター: ENを読み込み12言語に展開
lib/
frontmatter.ts # gray-matterのfrontmatterをパース/シリアライズ
voice-prompt.ts # 共有システムプロンプトビルダーオーケストレーター(translate-all.ts)はPromise.allSettledを使用しているため、1つのロケールが失敗しても残りはブロックされません。
システムプロンプトの設計
ここで多くの人が間違えます。「これをフランス語に翻訳して、著者の文体を保持して」のような一行指示を書いてしまいます。それでは凡庸な出力になります。
私のシステムプロンプトには2つの必須セクションがあります。
セクション1 — スタイル制約(全言語共通、すべての呼び出しに先頭追加):
// scripts/agent/lib/voice-prompt.ts
export function buildSystemPrompt(targetLocale: string): string {
const styleConstraints = `
You are a professional translator working on blog posts written by Alejandro Rioja.
STYLE RULES — apply to every locale:
- Short paragraphs (1–3 sentences max). Do not merge them.
- First-person, direct voice. Never passive if active is natural.
- No filler phrases: no "In today's world", no "It is worth noting that".
- Preserve all markdown: headings, bold, italics, code blocks, links.
- Translate heading text but keep the ## / ### prefix exactly.
- Code blocks: translate comments only. Keep all variable names, strings, and syntax in English.
- Preserve frontmatter keys exactly. Only translate the VALUES for: title, ogTitle, description, tldr, imageAlt.
- Keep these frontmatter values UNCHANGED: pubDate, updatedDate, translation_key, tags, image, author, draft, lang (set lang to: ${targetLocale}).
`.trim();セクション2 — ロケール固有のノート(呼び出しごとに追加):
const localeNotes: Record<string, string> = {
ar: "Arabic: use Modern Standard Arabic (MSA). RTL layout is handled by the CMS — do not add any RTL markup. Avoid overly formal Classical Arabic registers.",
de: "German: use informal 'du' not formal 'Sie'. Compound nouns are fine; don't over-hyphenate. Keep tech terms in English when that's the industry standard (e.g. 'Content Marketing', 'SEO').",
es: "Spanish: use neutral Latin American Spanish, not Castilian. Tuteo ('tú') over 'usted'. Keep anglicisms that are standard in tech (SEO, agente, prompt).",
fr: "French: use informal 'tu'. Avoid over-formalizing. Tech anglicisms are acceptable when widely used (SEO, agent, prompt).",
hi: "Hindi: use Devanagari script. Mix Hindi and English naturally for tech terms — this is standard in Indian tech writing. Don't force Hindi equivalents for words like 'agent', 'prompt', 'SEO'.",
it: "Italian: use 'tu' form. Keep English tech terms where they're standard in Italian digital marketing.",
ja: "Japanese: use です/ます (polite) style, not casual or keigo. Keep technical English terms in katakana where standard (e.g. エージェント, プロンプト, SEO).",
ko: "Korean: use 합쇼체 (formal polite). Tech terms in English or standard Korean loanwords. Keep SEO, agent, prompt as-is or standard loanwords.",
nl: "Dutch: use 'je/jij' (informal). Keep English tech terms standard in Dutch digital marketing.",
pt: "Portuguese: use Brazilian Portuguese (pt-BR). Informal 'você'. Keep tech anglicisms standard in Brazilian digital marketing.",
ru: "Russian: use modern, accessible Russian. Avoid overly bureaucratic phrasing. Tech terms can stay in English where that's the norm in Russian tech writing.",
zh: "Chinese: use Simplified Chinese (zh-CN). Modern, accessible tone. Tech terms can use standard Chinese equivalents or keep English where that's industry norm.",
};
return `${styleConstraints}\n\nLOCALE-SPECIFIC NOTES for ${targetLocale}:\n${localeNotes[targetLocale]}`;
}翻訳ワーカー
完全なワーカーです。ENファイルを読み込み、Claudeを呼び出し、出力をディスクに書き込みます。
// scripts/agent/translate-worker.ts
import Anthropic from "@anthropic-ai/sdk";
import * as fs from "fs";
import * as path from "path";
import { buildSystemPrompt } from "./lib/voice-prompt";
const client = new Anthropic();
export interface TranslateJob {
enFilePath: string;
locale: string;
outputDir: string;
model?: "claude-haiku-4-5" | "claude-sonnet-4-5";
dryRun?: boolean;
}
export async function translatePost(job: TranslateJob): Promise<string> {
const { enFilePath, locale, outputDir, model = "claude-haiku-4-5", dryRun = false } = job;
// 冪等性: 翻訳がすでに存在する場合はスキップ
const filename = path.basename(enFilePath);
const outPath = path.join(outputDir, locale, filename);
if (fs.existsSync(outPath)) {
console.log(`[${locale}] すでに存在します — スキップ: ${outPath}`);
return outPath;
}
const enContent = fs.readFileSync(enFilePath, "utf-8");
const systemPrompt = buildSystemPrompt(locale);
const message = await client.messages.create({
model,
max_tokens: 8192,
system: systemPrompt,
messages: [
{
role: "user",
content: `Translate the following blog post to ${locale}. Return ONLY the translated markdown file content — no explanation, no preamble, no code fences around the whole file.\n\n${enContent}`,
},
],
});
const translated = (message.content[0] as { type: string; text: string }).text;
if (!dryRun) {
fs.mkdirSync(path.join(outputDir, locale), { recursive: true });
fs.writeFileSync(outPath, translated, "utf-8");
console.log(`[${locale}] 書き込み完了: ${outPath}`);
}
return outPath;
}オーケストレーター
// scripts/agent/translate-all.ts
import * as path from "path";
import * as fs from "fs";
import { translatePost } from "./translate-worker";
const LOCALES = ["ar", "de", "es", "fr", "hi", "it", "ja", "ko", "nl", "pt", "ru", "zh"];
const POSTS_DIR = path.resolve("src/content/posts");
const MODEL = (process.env.TRANSLATE_MODEL as "claude-haiku-4-5" | "claude-sonnet-4-5") ?? "claude-haiku-4-5";
async function main() {
// 特定ファイルを受け取るか、全EN記事を翻訳
const targetFile = process.argv[2];
const enFiles = targetFile
? [path.resolve(targetFile)]
: fs.readdirSync(path.join(POSTS_DIR, "en")).map((f) => path.join(POSTS_DIR, "en", f));
console.log(`${enFiles.length}本の記事 × ${LOCALES.length}言語を翻訳中。モデル: ${MODEL}`);
for (const enFile of enFiles) {
const results = await Promise.allSettled(
LOCALES.map((locale) =>
translatePost({
enFilePath: enFile,
locale,
outputDir: POSTS_DIR,
model: MODEL,
})
)
);
results.forEach((r, i) => {
if (r.status === "rejected") {
console.error(`[${LOCALES[i]}] 失敗:`, r.reason);
}
});
}
console.log("完了。");
}
main();実行コマンド:
# 新しい1本の記事を翻訳
npx ts-node scripts/agent/translate-all.ts src/content/posts/en/my-new-post.md
# 全記事を翻訳(冪等 — 既存はスキップ)
npx ts-node scripts/agent/translate-all.tsコスト比較:Haiku vs Sonnet
私の実際の使用状況に基づく1記事あたりのコスト:
| モデル | 入力トークン(平均) | 出力トークン(平均) | 言語あたりコスト | × 12言語のコスト |
|---|---|---|---|---|
| claude-haiku-4-5 | ~2,400 | ~2,600 | ~$0.0004 | ~$0.005 |
| claude-sonnet-4-5 | ~2,400 | ~2,600 | ~$0.015 | ~$0.18 |
Haikuで341本 × 12言語:合計約**$1.70**。これがバックログ全体のコストです。
Sonnetの方がわずかにイディオム表現が自然ですが、ほとんどの記事ではその差が36倍の価格に見合いません。Sonnetを使うのは、ニュアンスのある説得力のあるトーンが重要な記事(セールスページや高トラフィックのコーナーストーンコンテンツ)のみです。
TRANSLATE_MODEL環境変数で実行ごとにモデルを切り替えられます:
TRANSLATE_MODEL=claude-sonnet-4-5 npx ts-node scripts/agent/translate-all.ts src/content/posts/en/flagship-post.md実際の結果:トラフィックへの影響
2025年12月にバックログ全体(341本)の翻訳を公開しました。60日以内の結果:
- オーガニックセッション+34%(サイト全体、Google Search Console、2026年1〜2月 vs 2025年10〜11月)
- セッション数トップの新言語: ブラジルポルトガル語(pt)— 新規国際トラフィックの11%
- コンバージョン率トップの新言語: ドイツ語(de)— コンサルティング予約率2.1% vs 全体平均1.8%
- 最低パフォーマンス: アラビア語(ar)— トラフィックは来たものの、コンバージョンはゼロ。予約フローが記事コンテンツを超えてローカライズされていないと思われます。
- 日本語(ja)と韓国語(ko): 大きなトラフィック増加(それぞれ国際セッションの8%と6%)、エンゲージメントも平均以上(ページ滞在時間が英語ベースラインより40%増)
日本語と韓国語の結果は想定外でした。どちらも質の高いAIコミュニティを持ち、実践的なオペレーターコンテンツへの需要があるようです。
オペレーターの結論
1つのエージェント、1時間のセットアップ、API費用$1.70。341本の記事を12言語でインデックス可能にするために必要だったのはそれだけです。SEOの伸びだけで、初週に計算コストは回収できました。コンテンツが多いサイトを運営していてまだこれを構築していないなら、国際トラフィックを逃しています。上記のコードが完全な実装です。フォークして、自分のvoice-promptのノートに差し替えて、今夜バックログに対して実行してください。
毎週水曜。28,400人以上の読者。無駄なし。
✓ Check your inbox — click the confirmation link to complete sign-up.
✓ You're subscribed!
✓ You're already on the list.
AIプレイブックをメールでお届け
毎週水曜。28,400人以上の読者。無駄なし。
Check your inbox.
We sent you a confirmation email — click the link inside to complete your subscription. Check spam if you don't see it within a minute.
You're subscribed.
Welcome — the next edition lands in your inbox soon.
You're already on the list — look for it every Wednesday.