72言語×133ツールをAI翻訳で回す仕組みと実測コスト|月間100万PVのラッコツールズの場合
こんにちは、mayaです!
私が開発に携わっているラッコツールズは、文字数カウントやQRコード作成などの無料Webツール集です。月間100万PVのこのサイトを、133個のツール × 72言語で提供しています。
この記事では、この規模の多言語対応をどんな仕組みで回しているのかを、URL設計・翻訳データの管理・AI翻訳パイプラインの3つに分けて紹介します。本丸は3つめのAI翻訳パイプラインなので、すでに多言語サイトを運用中の方はそこから読んでいただいても大丈夫です。
規模感と前提
まず数字から。
| 項目 | 数値 |
|---|---|
| 対応言語 | 72言語(日本語 + 71言語) |
| ツール数 | 133 |
| 翻訳キー数(日本語原文) | 9,531 |
| 翻訳ユニット総数 | 約68万(9,531キー × 72言語) |
| ロケールファイル数 | 13,752本 |
| サイトマップのURL数 | 11,592 |
ポイントは「翻訳ユニット約68万」です。この量になると、翻訳会社への発注も、訳文を人力で貼り付ける運用も成立しません。実際、ラッコツールズに翻訳を専任で担当するメンバーはいません。開発チームだけで回しています。
技術スタックは Astro + React + Vercel で、ページはSSR(リクエストごとにサーバー側でHTMLを生成する方式)で配信しています。旧サイトはPHP製で、リプレイスにあわせて多言語対応の仕組みも作り直しました。私はこのリプレイスの設計から実装・運用までを一人で担当しており、この記事はその実体験ベースの内容です。
多言語対応は3つの問題に分解できる
「多言語対応」はひとつの機能ではなく、性質の違う3つの問題の集合だと考えています。
- URLと言語判定: どのURLでどの言語を見せるか
- 翻訳データの管理と配信: 増え続ける訳文をどう持ち、どう届けるか
- 翻訳の生成と品質担保: 誰が71言語分を書き、間違いをどう見つけるか
1と2は一度設計すれば安定しますが、3はコンテンツが増えるたびに動き続ける「運用」です。順番に見ていきます。
① URLと言語判定:「URLだけが真実」にする
パス方式を選び、サブドメイン方式を捨てた
URL構造は、言語コードをパスの先頭に置く方式です。
https://rakko.tools/ja/tools/char-counter ← 日本語
https://rakko.tools/en/tools/char-counter ← 英語
https://rakko.tools/ar/tools/char-counter ← アラビア語
旧サイトは en.rakko.tools のような言語別サブドメイン方式でしたが、リプレイス時に廃止しました。サブドメイン方式はDNS・証明書・CDNなどのインフラ設定が言語の数だけ増えるため、72言語ではっきり破綻するからです。パス方式ならインフラはひとつで済みます。
Accept-LanguageもCookieも使わない
意外に思われるかもしれませんが、ブラウザの言語設定(Accept-Languageヘッダー)による自動リダイレクトも、Cookieでの言語記憶もやっていません。言語を決めるのはURLの先頭セグメントだけです。言語プレフィックスなしのURL(rakko.tools/ など)に来た場合だけは日本語版へリダイレクトし、以降の言語選択はすべてURLに委ねています。
同じURLが閲覧者によって違う言語を返すと、検索エンジンのクロール、CDNのキャッシュ、共有されたリンクの挙動がすべて複雑になります。言語切替UIも素の <a href> によるページ遷移で、「URLを変えること」だけが言語を変える唯一の手段です。おかげでルーティング周りのデバッグは「URLを見れば全部わかる」状態を保てています。
hreflangとsitemapは物量との戦い
多言語サイトのSEOでは、各ページに「このページの他言語版はここにある」と検索エンジンへ伝える hreflang アノテーションを付けます。ラッコツールズではインデックス対象の全ページに72言語分 + x-default(どの言語にも当てはまらない訪問者向けの既定ページ指定)の計73本を出力しています。
<link rel="alternate" hreflang="ja" href="https://rakko.tools/ja/tools/char-counter" />
<link rel="alternate" hreflang="en" href="https://rakko.tools/en/tools/char-counter" />
<!-- …全72言語 + x-default の計73本 -->
sitemapは 72言語 ×(トップ + 133ツール + 27タグ)= 11,592 URL。1つのURLエントリに73本のhreflangが付くため1エントリが約10KBに膨らみ、Googleのsitemapサイズ上限(50MB/ファイル)に実際にぶつかりました。検証環境の実測で5,000 URL/ファイルの分割では51MBを超えたため、4,000 URL/ファイルで分割しています。多言語×大規模では、仕様上の上限が「実際に踏む上限」になります。
② 翻訳データの管理と配信
ロケールファイルは「ツールに寄せて」置く
訳文はTypeScriptのプレーンなオブジェクトとして、共通メッセージとツール別メッセージの2層で持ちます。
src/messages/<namespace>/<locale>.ts # 共通UI・タグなど
src/features/tools/<slug>/messages/<locale>.ts # ツールごとの説明文など
全言語分を1つの巨大な辞書にせず、共通メッセージも含めて計13,752本の小さなファイル(ツール別は1本あたり平均68キー)に分けています。翻訳の生成・差し替え・レビューがツール単位で完結するので、後述の自動化と相性がよい構造です。
フォールバックは「ファイル単位」ではなく「キー単位」
翻訳が一部だけ存在する状態は、増分翻訳(変更されたキーだけをAIで訳す方式。詳細は③で)の仕組み上、常に発生します。そこで訳文の解決は 要求言語 → 英語 → 日本語 の順で、キー単位のdeep-mergeにしています。
これは初めからそうだったわけではなく、ファイル単位でフォールバックしていた時期に「英語ページで、あるツールの安全性説明セクションだけ丸ごと消える」という表示欠落が実際に起きました。ファイルが存在した時点でフォールバックが止まり、その中の欠けたキーは拾えなかったのです。「部分的に訳が欠けたら、そのキーだけ日本語で埋まって表示される」が正解でした。
最初に壊れたのは翻訳ではなくビルドだった
72言語化して最初にぶつかった壁は、翻訳の品質ではなくビルドのメモリでした。
当初はロケールファイルを import.meta.glob で読み込む素直な構成でしたが、これはツール別だけで9,000本超の .ts ファイルをビルドグラフに載せることを意味します。lazyなglobにしてもRollupはビルド時に全モジュールをチャンク化しようとするため、ビルドのピークメモリは約8GiBに達し、6GBヒープの環境ではOOM(メモリ不足によるプロセス強制終了)でビルドが落ちるようになりました。
対策は、ビルドの前段で全ロケールファイルを言語ごとに1つのJSON(計72本)へ集約し、ランタイムはそのJSONをファイルとして読むだけにすることです。ロケールファイルがビルドグラフから消え、ビルドメモリが「言語数×ツール数」に比例しない構造になりました。
③ AI翻訳パイプライン:人間はjaだけ書く
ここが本丸です。開発者が編集するのは日本語のロケールファイルだけ。71言語分は、GitHub Actions上のパイプラインがClaude APIで生成します。
翻訳PRのレビューといっても、人間が71言語の訳文を読むわけではありません。訳文そのものの品質担保は後述の機械検証に委ね、人間は検証結果と差分の規模を確認してマージする割り切りです。
差分はgit diffではなくハッシュ台帳で検出する
毎回全キーを翻訳し直すと、68万ユニット分のAPIコストが繰り返し発生してしまいます。そこで「どのキーが翻訳済みか」の台帳をリポジトリにコミットし、日本語原文のSHA-256ハッシュを記録しています。翻訳対象は台帳と現在のハッシュを突き合わせて決めます。
// 台帳と現在のjaのハッシュを突き合わせ、変わったキーだけ翻訳対象にする
for (const [key, hash] of Object.entries(currentHashes)) {
if (cachedHashes[key] !== hash) diffKeys.push(key)
}
重要な規約は、翻訳の失敗を台帳が記憶する、という点です。一部の言語で翻訳が失敗しても成功分は保全され、失敗した言語×キーは台帳に「未配布」として記録されるので、次回のrunで自動的に再翻訳されます。
モデルは言語ごとに使い分けており、基本はHaiku、学習データの少ない低リソース言語はSonnetに固定しています。また、日本語に変更がないときはAIを一切起動せず即終了するガードがあるため、差分ゼロの日の翻訳コストは0円です。
実際のAPIコスト:27日間で約118ドル
「AI翻訳って結局いくらかかるの?」が一番気になるところだと思うので、直近27日間の翻訳APIキーの実測コストを載せます。
| 項目 | 実測値 |
|---|---|
| 期間合計(27日間) | $117.86 |
| コストが0円だった日 | 27日中8日(日本語に変更がなかった日) |
| 通常日のコスト | $0.4〜$9程度 |
| 最大の日 | $51.42(まとまった文言追加で翻訳が集中した日) |
| モデル別内訳 | Sonnet 56% / Haiku 44% |
約68万の翻訳ユニットを72言語分維持する費用として、月換算で130ドル前後。増分翻訳で差分だけを訳し、プロンプトキャッシュを効かせている(コストの27%はキャッシュ読み取り)ことが効いています。翻訳会社への発注や専任者の人件費と比べれば、桁違いに安い金額です。
面白いのは、低リソース言語だけに使っているSonnetがコストの56%を占めている点です。対象は71言語中19言語だけなのに、単価差でコストの過半を持っていく。多言語対応のコストは「言語数」ではなく「どの言語を高品質にするか」で決まる、というのが実測から見えた感覚です。
「全部英語のまま」が全検査をすり抜けた話
パイプラインには痛い事故もありました。ある言語の新規キーが、翻訳されずに英語のまままとめて出力されたのです。キーの過不足はなく、プレースホルダ({count} のような変数の埋め込み枠)も完全に整合しているため、当時の機械検証はすべて素通りしました。後述の文字体系チェックでも、英語=ラテン文字は技術用語として全言語で正当なので、原理的に検出できません。
対策として、「参照言語(英語)と訳文の同値率」を見る専用の検出層を追加しました。"URL" や "OK" のように翻訳しないのが正しいキーもあるため、1〜2キーの一致では反応せず、同値が3キー以上かつ同値率50%以上で初めてfailにする、しきい値付きの統計的な判定にしています。
文字体系チェックと「πだけ混ざる」問題
もうひとつの検証が文字スクリプト純度チェックです。韓国語のページにタイ文字が混ざるといったAI翻訳特有の事故を、Unicodeの文字体系(script)単位で機械検出します。
ここにも運用してみて初めてわかる話があり、数式の「π」が訳文に正当に含まれるだけで、ギリシャ文字の混入として複数言語が大量failしたことがあります。現在は π・Ω・µ・° のような汎用記号を判定前に除外しています。この種の例外リストは、理屈で導くものではなく運用で発見するものでした。
検証CIは「回帰だけ」をfailさせる
増分翻訳の世界では、「日本語だけ変えた直後で、71言語がまだ存在しない」状態は正常です。これを一律エラーにするとCIは毎日赤くなります。そこで検証CIは翻訳キーの状態を台帳と突き合わせ、次の2×2で判定します。
| 状態 | 台帳にある(配布済み) | 台帳にない(未配布) |
|---|---|---|
| キーが欠けている | fail(配布済み翻訳の消失 = 回帰) | 警告(翻訳待ちの新キー) |
| 余分なキーがある | 警告(原文削除に伴う除去待ち) | fail(管理外キーの混入) |
「過渡状態は許容し、回帰だけを落とす」。この線引きができてから、CIが開発の邪魔をしなくなりました。AI生成物をCIに組み込むとき全般に使える考え方だと思います。
失敗したら自己修復、ただし3回で止まる
検証CIがfailすると自己修復ルーチン(Claude Codeのエージェント)が起動し、失敗をローカルで再現・分類して、既知の失敗2パターンだけを再生成して同じPRブランチへpushします。分類できない未知の失敗は自動修復せず、Slackへエスカレーションして人間に委ねます。
エージェントの暴走対策は「試行が3回に達したらpushしない」というシンプルなルールです。pushがなければCIも走らないので、修復ループは自然に停止します。止め方を仕組みに埋め込んでおくのは、AIを無人運転させる上で欠かせない設計だと感じています。
まとめ:多言語対応は翻訳作業ではなく仕組みづくり
最後に、この記事で紹介した設計判断を一覧にします。
| 問題 | 判断 | 理由 |
|---|---|---|
| URL | パス方式 /{locale}/...
|
サブドメインは72言語でインフラが破綻する |
| 言語判定 | URLのみ(Accept-Language / Cookie不使用) | 「同じURL = 同じ内容」を守る |
| フォールバック | キー単位のdeep-merge | 部分翻訳でもUIを欠けさせない |
| ビルド | ロケールをJSONに集約してfs読み | ビルドメモリを言語数に比例させない |
| 翻訳生成 | jaのみ人間、71言語はAIの増分翻訳 | ハッシュ台帳で差分管理、失敗分は自動再翻訳 |
| 品質担保 | 回帰だけfailさせる検証CI + 統計的検出 | 過渡状態を許容しつつ事故を捕まえる |
| 障害対応 | 自己修復ルーチン、3回で停止 | 既知パターンだけ自動化、未知は人間へ |
数字だけ見ると途方もない72言語対応も、「URL」「データ」「生成と品質」に分解すれば、それぞれは地に足のついたエンジニアリングの積み重ねです。これから多言語対応に踏み出す方や、いままさに翻訳運用に苦しんでいる方の参考になればうれしいです。
最後まで読んでいただき、ありがとうございました!
Discussion