あなたはClaude Codeにサインインフォームへローディング状態を追加するよう依頼しました。ローディング状態は正しく動いています。しかしボタンの文言はいつの間にか「Sign in」から「Log in」に変わり、エラーメッセージはより親しみやすい表現になり、ある見出しは突然センテンスケースになっていました。誰もそんなことは頼んでいません。それでもすべてのテストは通過し、誰かが差分を一語一句読まない限り、そのままリリースされてしまいます。
この記事が扱うのは、まさにその特定の失敗——既存の承認済みコピーがAIコーディングエージェントによって静かに書き換えられてしまう問題と、それを防ぐワークフローです。エージェントが新たにハードコードしてキー化されていない文字列を「作り出す」問題は別物で、なぜAIコーディングツールはハードコードされた文字列を追加し続けるのかで扱っています。プロダクトコピーが一般的にどこに存在すべきかはアーキテクチャの問題であり、コードネイティブなプロダクトコピーで扱っています。この記事はあくまで一つのインシデント——文言が変わり、誰もそれを決めていなかった、とある月曜の朝——に焦点を当てます。globalize.nowは、ローカライズ機能を内蔵したコードネイティブなコピー管理システムであり、このインシデントこそがその存在理由の一つです。
なぜAIコーディングエージェントはUIコピーを書き換えてしまうのか?
AIコーディングエージェントがUIコピーを書き換えてしまうのは、コンポーネント内に文章がそのまま存在しているように見えるからです。そのコピーがソースロケールファイル内でキーの裏側に置かれるようになれば、変更は他のタスクの副作用ではなく、明示的でレビュー可能な編集になります。
コンポーネントの中では、「Sign in」に特別な地位はありません。JSX属性の中の単なる文字列リテラルであり、classNameやtest idと区別がつきません。エージェントは、近くのコードを改善することが良い振る舞いとされる何百万もの差分で学習されており、モデルにとってラベルを「わかりやすく」することは、変数名を変更することと同じ行為なのです。そこには待ち構える型エラーもなく、失敗するテストもなく、言葉に所有者の注釈もありません。書き換えはエージェントにとって何のコストもかかりません。
そしてその変更は、同じ理由でレビューをすり抜けてしまいます。レビュアーはロジックを読みます。文言の変更は、正当にローディング状態を扱っている差分の中に紛れ込み、コンパイルも通り、表示も問題なく、もしスナップショットテストが警告を出しても、定番の対応はスナップショットを更新することです——それはまるで新しい文言が承認済みであるかのように記録してしまいます。
AGENTS.mdのようなルールファイルは、実際にそれを止められるのか?
エージェント用のルールファイルは頻度を下げますが、UIコピーを書き換え不能にするわけではありません。編集を可視化するのはアーキテクチャの役割です。
それでもルールファイルは用意しておきましょう——最も低コストな層であり、30秒かける価値があるほどには機能します。このブロックはAGENTS.md、CLAUDE.md、そしてCursorルール(.cursorrulesまたは.cursor/rules/)で機能します。
## UI copy policy — do not modify user-facing text
- Never change user-visible strings (button labels, headings, empty states,
error messages, tooltips, placeholder text) unless the task explicitly
asks for a copy change.
- User-visible text lives in the source locale file (e.g. locales/en.json).
Reference it by key. Never inline a new user-facing string in a component.
- If a task needs new user-facing text, add a key to the source locale file,
reference it, and list the new key in your summary for copy review.
- Any diff to the source locale file is a copy change. Call it out
separately — it needs its own approval, apart from code review.
ここからが正直な話です。ルールは文脈であり、文脈は競合します。長いタスクでは、その指示は何千ものコードトークンの中の1行にすぎず、時には負けてしまいます——コンパクション処理に負け、その指示を見たことのないサブエージェントに負け、周囲のファイルにある強いパターンに負けるのです。さらに厄介なことに、ルールを破ったエージェントはそれを告げません。文言が変わったという通知は来ず、気づけば変わっている差分だけが手元にあるのです。慣習はチェックではありません。
だからこそルールファイルは第一層であり、それ単体で信頼すべきではない層でもあるのです。
コピーの編集を、静かなものではなく目に見える形にするにはどうすればよいか?
すべてのユーザー向け文字列をコンポーネントの外へ移し、ソースロケールファイル内のキーの裏側に置きましょう。そうすれば、文言を変えたいエージェントは、文言そのものが唯一の目的であるファイルを編集せざるを得なくなります。
これはルールファイルにはできないステップであり、その仕組みは単純です。コンポーネントが「Sign in」を書き直すのではなくt('auth.signIn')を参照するようにすると、2つのことが同時に変わります。まず、コンポーネントの差分にコピーが含まれなくなるため、ローディング状態を作業しているエージェントには、その場で書き換えるべき文言が存在しません。そして、エージェントによるものであれ人間によるものであれ、あらゆるコピーの変更は一つのファイルに集約されます——そこでは変更された行そのものが注目すべき出来事であり、周囲のノイズではありません。ロケールファイルの差分をスキャンするのは数秒で済みます。まずい書き換えを元に戻すのもたった1行です。
正確に言うと、エージェントは依然としてロケールファイルを編集できます。重要なのは、文言が触れられなくなることではなく、触れることが副作用ではなく、目に見えてレビュー可能な行為になることです。それが、差分で気づくのと、顧客から知らされるのとの違いです。
信頼できる情報源を、それと同期する外部のデザインツールではなく、コードベース自体に置くべきだという完全な論拠は、柱記事であるコードネイティブなプロダクトコピーで扱っています。この記事に必要なのは一つの帰結だけです。キー化されたコピーこそが、ルールファイルを単なる「お願い」から検証可能なものへと変えるのです。
レビューゲートとは具体的にどのようなものか?
ソースロケールファイルへのあらゆる差分をコピーの変更として扱い、文言の責任者にルーティングしましょう。
具体的には、localesディレクトリをCODEOWNERSに登録し、変更があるたびにコピーの責任者であるレビュアーが自動的にリクエストされるようにします。PRテンプレートに「このPRはユーザー向け文言を変更していますか、それはタスクの一部でしたか?」という一文を加えます。そして、エージェントが変更したキーを要約に列挙するというルールファイル上の要求も維持しましょう。エージェントがそれに従えば、レビューは一瞬で済みますし、従わなくてもCODEOWNERSのフックが拾ってくれます。
小規模なチームであれば、これは思っているより軽い運用で済みます。ロケールの差分は短く、平易な言葉で読め、「このボタンは『Log in now』と表示すべきか?」という判断は、誰かが5秒で下せるものです——ただし、その問いが実際にその人の目の前に提示されることが前提であり、それこそがこのゲートの仕事のすべてです。
もしすでにエージェントがコピーを書き換えてしまっていたら?
まず承認済みの文言を復元し、その後で文字列をキー化して、次の書き換えが差分として現れるようにしましょう。
被害箇所を見つけるのはgitの作業です。影響を受けたコンポーネントを、信頼できる最後のコミットと比較し、文字列リテラルだけを読み込みます。変更点を復元しましょう。もし、どちらが承認済みの文言だったのか自信を持って言えない場合——ボタンには「Log in」、マーケティングページには「Sign in」と書かれていて、どちらが正しいかを記録した成果物が存在しない場合——それは、このインシデントの根底にあるより深刻な状態に行き着いています。コピードリフトとは、ユーザー向けテキストが複数の画面や時間の経過とともに、承認済みの情報源から乖離していく現象であり、そもそも乖離元となる単一の信頼できる情報源が存在しないことを指します。この用語の定義、原因、そして下流への影響については、コピードリフトとは何かで個別に扱っています。
次に、復元した文字列をキー化しましょう。まずはエージェントが手を加えた文字列から始めます。それらこそ、明らかに火線上にある文字列だからです。
ローカライゼーションはどこに関わってくるのか?
静かな書き換えは、1つの言語ではちょっとした迷惑事ですが、10の言語ではリスクになります。古い文言から派生したすべての翻訳が今や古くなり、誰も再翻訳が必要だと教えてくれないのです。
ここでエージェントのインシデントは複利のように悪化します。書き換えられた英語をそのまま公開すれば、ドイツ語版、日本語版、スペイン語版は依然として古い文言のままです——今や画面間だけでなく、言語間でも表現が食い違ってしまいます。カタログが同期からずれていくことは、それ自体が独自の失敗モードであり、独自の対処法があります。詳しくは翻訳ファイルの同期ずれで扱っています。
キー化されたコピーは、このループも閉じてくれます。変更されたソース文字列は、静かに移動した単なるリテラルではなく、その翻訳が連動する目に見えるイベントになるからです。これがglobalize.nowの設計思想です。コピーはコードベース内に単一の情報源としてキーの裏側に存在し、ローカライゼーションは後付けの作業ではなく同一の操作になります——リポジトリを接続すれば、すべての言語がその唯一の情報源から派生します。AIコーディングツールで開発しているチームほど、この問題に真っ先に直面します。だからこそ私たちは開発者向け、そしてAIツールで構築している人々向けに記事を書いているのです。
次にエージェントがあなたのコンポーネントに手を加えるとき、問われるのは、変更されたラベルが「意思決定」として現れるか、それとも「不意打ち」として現れるかです。globalize.nowは、あなたが接続するリポジトリ内でユーザー向け文字列を単一の情報源として管理し、その一つのコピーからすべての言語を導き出します。
globalize.nowは、ハードコードされたアプリ内テキストを翻訳可能なロケールファイルに変換し、リリースのたびに自動で最新の状態を保ちます。
globalize.nowを無料で試す