メインコンテンツまでスキップ

Claude Codeのサブエージェントとスキルでサイトの英語翻訳を自動化した

  • 作成:
  • 更新:
  • 著者:
    Takeshi Takatsudo

概要

自分が運営しているモジュラーシンセのWebショップ Takazudo Modular の英語版を作り始めた。手動でやるには膨大な作業量になるが、Claude Codeのカスタムエージェントとスキルを組み合わせて、日本語PRから英語版の実装を一気通貫で自動化する仕組みを作った。そのまとめ。

英語版を作る動機

Takazudo Modularにとって、英語版を作る意味は大きく2つある。

1つ目は、日本に住んでいる外国人のお客さん。日本のモジュラーシンセの大きなイベントに行くとわかるが、日常的に英語を使って生活している外国人の方がかなりいる。そういう方々にとって英語のコンテンツがあるサイトは価値がある。

2つ目は、海外のブランドからのメール。自分のショップは個人運営のWebのみの小さなショップだが、海外のシンセブランドからメールが来ることがある。シンセ開発者として自分のプロダクトを世界中で使ってほしいという情熱が伝わってくる。そういう人たちにとって、英語版があればサイトの内容をずっとよく理解してもらえる。

手動でやるとどうなるか

サイトにはMDXで書かれた商品紹介ページやガイド記事が大量にある。これを全部手動で英語に翻訳していくのは現実的ではない。しかも、ただテキストを翻訳するだけでは済まない。MDXコンポーネントはそのまま保持しないといけないし、シンセ特有の用語は正しく訳す必要がある。frontmatterの中で翻訳すべきフィールドとそうでないフィールドがある。ENページのルーティングも作る必要がある。

こういう「定型的だけどドメイン知識が必要な繰り返し作業」はClaude Codeのサブエージェントとスキルの組み合わせが向いている。

en-translatorサブエージェント

まず作ったのがen-translatorというサブエージェント。ファイルは.claude/agents/en-translator.mdに置いている。日本語MDXファイルを英語に翻訳する専用のエージェントで、モデルはopusを指定して翻訳品質を高めている。

このエージェントの設計は以下。

シンセ用語辞書の内蔵

モジュラーシンセには独自の用語が多い。エージェントの定義に用語辞書を内蔵している。

翻訳しない用語としてVCO、VCF、VCA、LFO、CV、Gate、Eurorack、HP、PCB、BOM、SMD、SMT、DIYなどをリストアップ。これらは英語圏でもそのまま使われる略語なので翻訳する必要がない。

一方で、日本語→英語のマッピングが必要な用語もある。フィルター→filter、オシレーター→oscillator、はんだ付け→soldering、基板→PCBなど。

文脈依存の翻訳もある。「音を割る」は"breaking sound"ではなく"clipping audio"と訳す。「太い音」は"fat/thick tone"。こういうのは汎用的な翻訳だと間違えがち。シンセの文脈を知っているかどうかで訳が変わるので、エージェントの定義に明示的に書いておく必要がある。

MDXコンポーネントの保持

サイトでは30以上のMDXコンポーネントを使っている。ImgsGridExImgYoutubeInfoBoxPurchaseNavMercariNav、各ブランドコンポーネント、レイアウトコンポーネント等々。

翻訳時にこれらのコンポーネントが壊れると厄介なので、すべてのコンポーネント名をエージェント定義にリストアップして「そのまま保持せよ」と指示している。<ExImg>alt属性だけは翻訳対象にしつつ、他のpropsはそのまま維持する、という具合。

翻訳対象と非翻訳対象を明確にルール化すると以下のようになる。

翻訳するもの:

  • frontmatterのtitledescription
  • 本文テキスト、見出し
  • <ExImg>alt属性

翻訳しないもの:

  • frontmatterのその他フィールド(heroImgUrltagscategories等)
  • MDXコンポーネントとそのprops
  • リンクURL
  • ## TOCマーカー

トーンの指定

翻訳のトーンも指定している。「プロフェッショナルだけど親しみやすい、知識豊富なショップオーナーが書いているような文体」で、Takazudoのカジュアルな一人称スタイル、個人的なエピソード、読者への直接的な語りかけを保持するよう定義。

機械的な翻訳だと文体が平坦になりがちだが、こういうトーンの指定があると、元の日本語の雰囲気をある程度保てる。

l-create-en-implementationスキル

次に作ったのがl-create-en-implementationというスキル。ファイルは.claude/skills/l-create-en-implementation/SKILL.mdに置いている。

これは日本語PRから英語版の実装を一気通貫で自動化するスキルで、JA実装を行ったブランチ(例: topic/foo)にチェックアウトした状態で/l-create-en-implementationと実行する。PR番号を引数で渡す必要はなく、現在のブランチのPRを自動検出する。

ワークフロー全体像

8ステップのワークフローになっている。

Step 1はPRの解析。現在チェックアウトしているブランチのPRをgh pr viewgh pr diffで自動検出し、メタデータと変更ファイルを取得する。

Step 2はファイルの分類。変更ファイルをカテゴリ分けして、ユーザーに報告してから進める。MDXコンテンツ(src/mdx/**/*.mdx)は翻訳が必要、アプリルート(app/(ja)/**)はEN版ルートの有無を確認、コンポーネント(components/**)はenComponentsの更新確認、データファイル(src/data/lib/data/)はnameEn/labelEnの有無確認、という具合。

Step 3はブランチ作成。JA PRのheadブランチから<headRefName>/enブランチを作成する。Netlifyのブランチ名制限(46文字以内)のバリデーション付き。

Step 4はMDXファイルの翻訳。ここで先述のen-translatorエージェントを使う。複数ファイルがある場合は並列実行する。

Step 5はENルートのスキャフォールド。新しいJAルートにEN版がない場合、既存のテンプレート(app/(en)/en/notes/[slug]/page.tsx等)を参考にENページを生成する。getArticleForLocaleenComponentslocale="en"、hreflang alternatesなどのパターンに従う。

Step 6はコンポーネント/データの更新。新しいMDXコンポーネントがあればenComponentsにlocale-awareラッパーを追加し、新しい商品/タクソノミーデータがあればnameEn/labelEnフィールドを追加する。

Step 7はバリデーション。pnpm check(typecheck + lint + format)を実行し、問題があればpnpm check:fixで修正する。

Step 8はコミットとPR作成。

PRベースの運用

PRベースの運用についても書いておく。EN PRのターゲットはmainではなく、JA PRのheadブランチにする。こうすると、JA PRがmainにマージされたとき、JA + ENの変更が一緒にランディングする。

なぜこの構成が良いかというと、JA PRとEN PRが独立してmainにマージされる形だと、タイミングによっては日本語コンテンツはあるのに英語版がない、という状態が一時的に発生し得る。JA PRのheadをEN PRのターゲットにすることで、この問題を構造的に防げる。

skill + agentの組み合わせ

l-create-en-implementationスキルがオーケストレーター、en-translatorエージェントがワーカーという構成。以前の記事で書いた/globalsyncと同じパターン。

このパターンがドメイン固有の反復作業に向いているというのは、使ってみるとよくわかる。

スキルにはワークフロー全体の段取り(PR解析→ファイル分類→ブランチ作成→翻訳→スキャフォールド→バリデーション→PR作成)を書き、エージェントにはドメイン知識(シンセ用語辞書、MDXコンポーネントのリスト、翻訳ルール)を書く。責務が分かれているので、どちらか片方を更新しても他方に影響しない。

例えば新しいMDXコンポーネントを追加したらエージェント定義のコンポーネントリストに足すだけ。ワークフローのステップを変えたければスキルを編集するだけ。

余談

英語版は現在進行中で、もうすぐ完成する。

この英語化作業を始めたきっかけは、実はClaude Codeの週次トークンリミットが余っていたから。以前から英語版はやりたいと思っていたが、優先度が高くなくて手を付けていなかった。トークンが余っているなら使おう、と。

結果的にこれはとても良い取り組みになった。トークンに余裕があることで、こういう「やった方がいいけど優先度が高くなかった作業」にAIを使える。十分なトークンがあることがAIを効率的に使う助けになるのかもしれない。