コンテンツへスキップ

4 並列 AI エージェントで i18n 漏れを掃除した話 — 300 件検出 → AST で誤検出排除 → 真の漏れ 60 件

ある程度の規模のアプリを日英対応している状態で「まだ日本語ハードコードがどこに残っているか分からない」という不安は、ずっと残ります。grep で [ぁ-んァ-ヶ一-龯] を引っ掛けると数千件ヒットして、その大半は対訳テーブル内・既に分岐済みのコード・コメント — 真の漏れは見つけにくい。

このラウンドではこの問題に対して、4 並列の AI 調査エージェント + AST ベースの誤検出排除で立ち向かいました。結果は 300 件検出 → 真の漏れ 60 件 → 5 ラウンドで対応。今回はその流れと、最大の発見だった「海外で課金した英語ユーザーに日本語メールが届き続けていた」バグの話を整理しておきます。

なぜ単純 grep だけでは足りないのか

リポジトリ全体に grep -nE '[ぁ-んァ-ヶ一-龯]' をかけると数千件ヒットしますが、中身は 対訳テーブル内 / lang == 'en' 分岐済み / コメント・docstring / 真の漏れ の 4 種が混在します。前 3 つは無害で、最後の「真の漏れ」だけが英語ユーザーに JP を見せる事故になる。grep だけでは 4 種が分離できず、人間が 1 件ずつ見るには量が多すぎる、というのが出発点です。

4 並列の調査エージェントで「広く浅く」検出する

ここで採用したのが、AI 調査エージェントを並列起動して検出担当を分業させるやり方です。

[Agent 1] templates/*.html + lang/*.json の整合(data-i18n 抜け)
[Agent 2] server/wpmm-license/*.php(ライセンス API)
[Agent 3] server/wpmm-web/*.php(LP 側 API)
[Agent 4] core/*.py + tools/*.py(アプリ本体)

各エージェントには「ユーザー視認系の JP ハードコードを列挙する」「分岐済みかどうかは可能な範囲で判定する」というプロンプトを与え、それぞれが独立に走る設計です。並列なので所要時間が単一エージェント実行時より小さく、また「同じ問題を 4 つの視点で見る」効果でカバレッジが上がる利点もあります。

検出結果は合計 約 300 件。これだけだとまだ「ノイズ込み」の状態です。

誤検出は AST で機械的に削る

300 件の中には大量の誤検出が混ざっていました。具体的に多かったのは:

場所 件数 種類
templates/tos.html 63 件 tosJa / tosEn 両ブロック + switchLang JS で言語切替済
core/report_generator.py 141 件 if lang == 'en' 分岐内・_JA / _EN バリアントマップ内

これらを目視で 1 件ずつ判定すると 200 件の手作業になります。代わりに、Python の ast モジュールで 「JP リテラルを含む関数が lang 分岐を持っているか」を機械的に判定するスクリプトを書きました。概念図はこんな具合:

import ast

def has_lang_branch(func_node):
    """関数内で lang 変数を条件分岐に使っているかを AST で判定。"""
    for node in ast.walk(func_node):
        if isinstance(node, ast.If):
            for sub in ast.walk(node.test):
                if isinstance(sub, ast.Name) and sub.id == 'lang':
                    return True
    return False

def has_jp_literal(func_node):
    """関数内の Constant ノードに JP 文字を含む文字列があるか。"""
    for node in ast.walk(func_node):
        if isinstance(node, ast.Constant) and isinstance(node.value, str):
            if any('぀' <= c <= '鿿' for c in node.value):
                return True
    return False

# 真の漏れ = JP リテラルあり AND lang 分岐なし
真の漏れ = [f for f in functions
          if has_jp_literal(f) and not has_lang_branch(f)]

このスクリプトを report_generator.py の 141 件に対して走らせると 真の漏れは実質ゼロ件(残った 1 件も関数内コメントの誤検出)。tos.html の 63 件も DOM 構造 + switchLang の存在チェックで全件「対応済み」と確定できました。

差し引き 真の漏れ約 60 件。ここから人間が見て対応する量にやっと収まります。

真の 60 件 — 最大の発見はライセンス webhook だった

60 件の中に紛れていた一番のインパクトは、Stripe の webhook 経由で送られる 4 種類のメール(購入完了・更新・失敗・プラン変更)が、すべて日本語固定だった、というものでした。海外(USD 課金)のユーザーに、購入完了から失敗通知まですべて日本語のメールが届き続けていた、ということになります。確認しないと永遠に存在し続けるタイプの漏れです。

対処は、Stripe イベントから言語を推定するヘルパーを 1 つ置くこと:

/** Stripe イベントの currency から表示言語を推定する */
function lang_from_currency(string $currency): string {
    $usd_langs = ['usd'];   // USD なら英語ユーザー
    return in_array(strtolower($currency), $usd_langs, true) ? 'en' : 'ja';
}

これを send_license_email / send_payment_failed_email / send_plan_changed_email / send_renewal_email の 4 関数に $lang パラメータとして渡し、件名・本文を日英分岐、mb_language('uni'|'Japanese')UTF-8 Base64 件名エンコードを切り替えます。mb_language('Japanese') だと英語件名が ISO-2022-JP で MIME エンコードされ、Gmail / Outlook のスパムスコアが上がる懸念がありました(これも別の漏れとして同じラウンドで修正)。

ライセンス API 側にも i18n ヘルパーを 1 つ集約しました:

// server/wpmm-license/lib/i18n_helpers.php
function resolve_request_lang(?array $body = null): string {
    if (isset($body['language']) && in_array($body['language'], ['ja','en'], true)) {
        return $body['language'];
    }
    // Accept-Language fallback
    if (preg_match('/^en\b/i', $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '')) {
        return 'en';
    }
    return 'ja';
}

このヘルパーを validate.php / release_machine.php / webhook.php / verify_email.php から require_once 経由で使う設計に統一して、API ごとに乱立していた言語判定を 1 箇所に集約しました。プラン名の英訳テーブル(PLAN_NAMES_EN)も同じファイルに置いて、plan_name($code, $lang) 1 関数で名称解決できるようにしています。

残り 60 件には他にも、Python 側の core/license.py core/key_perms.py、デスクトップアプリ起動スクリプト _launcher.sh / .ps1、LP 側 API checkout.php / chat.php / rate.php の i18n 漏れが含まれていて、それぞれ同様のパターンで分岐化していきました。

学び — Agent + AST の二段構えで「広く浅く → 厳密に絞る」

このラウンドから取り出せる原則は次の 3 つでした。

  1. 大量検出には並列調査エージェントが向く — 「リポジトリ全体に対する『〇〇の網羅検出』」のような広域タスクは、担当範囲を分けて並列に走らせると単一実行より広くカバーできる。エージェントごとに視点が違うので、独立した報告を集約することで漏れも減る
  2. Agent 報告の鵜呑みは危険・AST で機械検証する — Agent の判定には誤検出(false positive)が含まれる。「分岐済み」の判断は曖昧な場合があるので、AST のような構造解析で機械的に絞り込む段階を必ず挟む。今回は 200 件の誤検出を AST で削れた
  3. 発見した漏れは「ヘルパーに集約」して再発防止lang_from_currency / resolve_request_lang / plan_name のような小さな関数 1 つに集めると、新しい API を追加する時にも自然にそれを使う流れになる。コードレベルで「うっかり JP リテラルを書く」を起こりにくくする設計

「自分のリポジトリにどれだけ日本語ハードコードが残っているか分からない」状態は、組織として国際化を進めていく上で常に付きまとう不安です。並列エージェント + AST という二段の検出を 1 セット用意しておくと、定期的にこの不安を「数値化」できるようになります。