🤖

約7年物のRailsアプリケーションをAIにコードを書き換えてもらいながら多言語対応した話

2024/09/30に公開

概要

先日、マシュマロの多言語対応をリリースしました。
マシュマロはリリースからもうすぐ7年経つ Rails アプリケーションで、 i18n 関しては全く意識せずに開発してきました。そのため、日本語がコードの色んなところに散りばめられていました。
そこからAIに手伝ってもらいつつ、どう多言語対応をしたのかをコードも含めて書いてみました。

最終的なアプローチ

最終的には bin/i18n <subcommand> と使う、小さなコマンド達を作って組み合わせることにしました。

👇実際に使っているところ。
https://youtu.be/9QsSOt29C8w

コードの変換

  1. bin/i18n suggest $fileで変換したいファイルを渡し、コード変換用JSONファイルを出力する
    • AI に $file から日本語の部分を検出して t(key) に書き換えてもらう
      • key は適当なものを考えてもらう
    • コード変換用JSONファイル
      • file: 元のファイル名
      • content: 変換後のファイル内容
      • ja: config/locales/ja.ymlに書き込む内容をkeyをフラットにして入れたもの
  2. bin/i18n diff $fileで現在の内容と、変換後のファイル内容の比較
    • 壊れていたり、key名を変えたかったら bin/i18n open $file してコード変換用JSONファイルを開いて書き換え
  3. bin/i18n apply $file でコード変換用JSONファイルを適用
    • $fileconfig/locales/ja.yml が書き換えられる

AI を信じるなら自動で一括変換することも可能です。

find app/views -type f | grep -v json.jbuilder | grep -v DS_STORE | while read -r file; do
  bin/i18n suggest "$file" && bin/i18n apply "$file"
done

最初はこんな感じで一気にやっつけるぞと妄想していたのですが、実際にやってみたところAIの出力を即採用できないこともあって確認が必要なので、細かく確認しつつ変換していく方針に落ち着きました。

locale ファイルの変換

  • bin/i18n translate $localeconfig/locales/ja.ymlを翻訳して config/locales/${locale}.yml を生成
    • config/locales/ja.yml全体を渡すことで、表現の一貫性やよりよい翻訳のためのコンテキストを渡せるのではないかという判断です
    • ただ、最終的には機能追加時など一部分のみの翻訳が欲しくなったので部分的な変換も追加しました
    • また実プロジェクトでは en に変換してレビューしたあと、en 以外への変換には ja, en どちらも渡すようにしました。
      • 英語なら一応レビューでき、英語でも調整したい箇所があったこと
      • en も渡せるほうが、より良いコンテキストをを与えられるのではないかという判断

実装

GitHub に公開しました。
https://github.com/en30/ai18n

モデルが変わったら調整が必要になったり、プロジェクトごとのカスタマイズが必要になってくるなど、 gem にするには適していなさそうなのでコードの公開にしました。

上述した主要部分のコードはこんな感じになっています。

https://github.com/en30/ai18n/blob/0b5e98a19399081fc2cfa763523ec97e024bcbfa/bin/i18n

https://github.com/en30/ai18n/blob/0b5e98a19399081fc2cfa763523ec97e024bcbfa/bin/i18n-suggest

https://github.com/en30/ai18n/blob/0b5e98a19399081fc2cfa763523ec97e024bcbfa/bin/i18n-translate

jq, i18n-tasks gem に依存しています。
環境変数 ANTHROPIC_API_KEY が設定されている前提です。

以下で既存のRailsアプリに追加できます。

for file in i18n i18n-suggest i18n-translate; do
  curl -L -o "bin/${file}" "https://raw.githubusercontent.com/en30/ai18n/refs/heads/main/bin/${file}"
  chmod +x "bin/${file}"
done

詳細まで書かれたプロンプトで、大きな労力がかかっているように見えるかもしれませんが、プロンプトもAIに改善してもらったのでそれほどでもありません。
以下のような流れで最終的なプロンプトに至っています。

  1. 雑なプロンプトを書く
  2. AIに改善してもらう
    • Anthropic Dashbaord にある Generate Prompt とか
    • 書いてもらったものから、確かに必要だったなという部分がわかる
    • 意図しているものと違うところは書き変えて誤解を少なくする
  3. 実際に使ってみて小さく実験
    • 意図しない動きが発生したら
      • プロンプトに足りない指示を書き足す
      • 2へ戻る
    • 意図通り動いたら完成

最初はこの程度のプロンプトだったのが、

## Goal
Enable multi-language support in a Rails project by refactoring files to use the I18n API.

## Task
Generate a JSON object based on the following input:
- Identify and replace Japanese text with `t` calls, choosing appropriate key names and generate patch file content to a `patch` value.
- Add the corresponding key-value pairs to a `ja` value.

## Example Input
```app/views/users/show.html.erb
...
```

## Example Output
```json
{
  "file": "app/views/users/show.html.erb",
  "patch": "...",
  "ja": {
     "users.show.title": "...",
	 "users.show.description": "..."
  }
}
```

## Input
...

最終的にはこんなものになりました。

systemプロンプト
You are an expert Rails developer and internationalization specialist. Your task is to refactor Rails view files to support multiple languages using the I18n API, with a focus on Japanese translations. You have deep knowledge of Ruby on Rails, ERB templates, and the I18n gem.

Your capabilities include:
1. Identifying Japanese text in Rails files
2. Refactoring code to use I18n \`t\` helper methods
3. Creating JSON structures with proper formatting and escaping
4. Organizing translations into appropriate key-value pairs

You will receive input containing Rails view file content. Your goal is to process this input and produce a JSON output that includes:
1. The name of the processed file
2. A refactored file content
3. A Japanese translations object with appropriate key-value pairs

You must follow the exact format and structure specified in the task description, paying close attention to detail and ensuring all requirements are met.
userプロンプト
You are tasked with enabling multi-language support in a Rails project by refactoring files to use the I18n API. Your goal is to generate a JSON object that includes a patch for the changes and the corresponding Japanese translations.

Follow these steps to complete the task:
1. Identify Japanese text in the file content.
2. For each identified Japanese text, replace it with an appropriate \`t\` call. Choose a descriptive key name for each translation. For view and controller files the format should be \`t('.[key_name]', variable: value)\` for local translations within the file.
3. Generate a refactored file content. This will be stored in the "content" field of the output JSON. Ensure that the output file is properly formatted and escaped.
4. Create a \`ja\` translations object that includes key-value pairs for each replaced Japanese text. The keys should match those used in the \`t\` calls, and the values should be the original Japanese text.
5. Construct the output JSON object with the following structure:
  - "file": The name of the processed file (extracted from the first line of the input)
  - "content": The refactored file content
  - "ja": The Japanese translations object
6. Ensure that the output JSON is properly formatted and escaped.

Here's examples of input and what the output should look like:

<example>
<input>
```app/views/users/show.html.erb
...
```
</input>

<output>
{
  "file": "app/views/users/show.html.erb",
  "content": "...",
  "ja": {
    "users.show.title": "...",
    "users.show.description": "..."
  }
}
</output>
</example>

Make sure to follow this exact format and structure in your output.

Here is the input to be processed:
<input>
```${filePath}
${fileContent}
```
</input>

他に検討したアプローチ

  • Agent
    • どういう手段でi18nするかも任せる
    • 計画を作ってみてもらってもあまりいい感じのが出てこなかったのですぐにやめました
  • Cursor
    • 普通に使えそうでした
      • 実際のことろ最初に Cusor で試してみて、コード変換の行けそう具合を確認しました
    • ただ、大量のファイルを変換したりといった自動化の自由度のためにコマンドを作る方向にしました
      • OpenAI, Anthropic の API を使う経験を積みたかったというのもあります。
    • コマンドを書くのは結構手伝ってもらいました
  • コマンド
    • 出力をpatchファイルにする
      • Cursor を使ってみて、こういう作りなのかなと最初に思ったものだったので試してみました
      • 大体は動きましたが、微妙に patch ファイルのフォーマットに合わないものが出てきた時に全く動かなくなってしまうという致命的な問題がありました。
        • AIのちょいちょい間違えるという性質に合っていませんでした
      • 何行目かということを情報として含むフォーマットなのも、色んなファイルのために何度もconfig/locale/ja.ymlを書き換えるというタスクと合っていませんでした

所感

  • claude-3.5-sonnet賢い
    • 他のモデルもいくつか試してみましたが、claude-3.5-sonnetが断然いい結果でした
  • AIによる改善は、素朴にイメージしていた、AIに何かをしてもらう「一次の改善」ではなく「複数次の改善」になりうる
    • AIに改善してもらうためのプロンプトもAIに改善してもらえる
    • AIを使うためのコマンドもAIに生成・改善してもらえる
  • AIはある程度失敗する
    • 複数ステップにわけて、どこまでをAIに任せるかの設計が重要
      • 決定的な手法があるところをAIにまかせない
        • 例えばi18n-tasksが使えるところはこちらで済ます
      • どこで自動の validation をするか
      • どこで人間の確認を入れるか
  • 作業量を減らすには効いたと思う
    • コードの書き換え
    • key 名を勝手に考えてくれたのもかなり楽だった
      • ただ config/locales/*.yml はアプリケーションの domain に関する辞書的な要素もあって、統一されていなかったり修正したいところはある
      • 叩き台としてはあり
  • 自動テストがあって本当に良かった
    • リファクタリングだけでなく、こういう自動変換のような攻め手を安心して使えるようになる
  • 1回限りの作業などは特にAIとの相性がいい
    • 今回
      • スクリプト作成
      • I18n.tを使うように書き換える部分
    • メンテナンスが関わってくると、現段階でも必ずしもいい結果になるとは限らない
      • localeファイルのkey
        • 命名
        • 共通化
    • 今後どうなるかはわからないけど
      • 現段階では、計画や判断の部分はまかせられない気がする
      • そうすると人間がメンテしやすいことの価値がまだ高い
  • まだまだ色々できることがありそう

GitHub にコードを公開しているのでよかったら見て/触ってみてください。
https://github.com/en30/ai18n

Diver Down Inc.

Discussion