以下のi18nセクションは、AGENTS.mdにそのまま貼り付けて使えます。コーディングエージェントに対して、ユーザーに見える文字列はすべて翻訳関数を通すこと、機能単位でキーを付けること、断片を連結しないこと、三項演算子で複数形を疑似的に処理しないこと、生成されたロケールファイルには触れないことを指示する、10個の短いルールです。globalize.nowはAI搭載のローカライズ基盤であり、エージェントが書いたi18nコードを数多く目にしてきましたが、ミスのほぼすべてはお決まりのいくつかのパターンに集約されます。各ルールは、そのパターンのいずれかに対応しています。
この記事の後半では、このファイルが効かなくなる場面を取り上げます。開発者がよく使うコード生成の場面のうち2つは、このファイルをそもそも読み込まないため、どんな文言にしても解決できません。
AGENTS.mdのi18nセクションには何を書くべきですか?
このブロックです。3つのファイルパスとlintコマンドだけプロジェクトに合わせて書き換え、残りはそのまま使ってください。
## Internationalization
- Every user-visible string goes through the translation function. That
includes JSX text, input hint text, `title`, `aria-label`, `alt`, toast and
error messages, empty states and the document `<title>`.
- The source locale is `en`. The catalog is `locales/en.json`. Add the key in
the same change that uses it.
- Keys are namespaced by feature: `checkout.summary.total`, not `total3`.
Search the catalog for an existing key before creating one.
- Never concatenate translated fragments. Use interpolation:
`t('cart.items', { count })`, not `t('cart.you_have') + count`.
- Plurals use the library's plural forms (`_one` / `_other` keys, or ICU
`{count, plural, ...}`). Never `count === 1 ? 'item' : 'items'`.
- Dates, numbers and currency go through `Intl.DateTimeFormat` and
`Intl.NumberFormat` with the active locale. Never a bare
`toLocaleString()`, never a currency symbol glued to a number.
- Do not hardcode the locale list, language names or text direction in
components. Read them from `i18n/config.ts`.
- Do not translate brand names, code identifiers, URLs or environment values.
- Do not edit locale files other than `en`. They are generated. If a
translation looks wrong, fix the source key or the glossary, not the file.
- Before finishing, run `npm run lint`. The `i18next/no-literal-string`
rule must pass.
ルールを10個に絞ったのは意図的です。Cursor自身のルールに関するドキュメントは、ルールは頻繁に使うパターンに絞ること、スタイルガイドを丸ごと貼り付けるのではなくlinterを使うことを推奨しています。Claude Codeのガイダンスも同様で、検証できる程度に具体的に書くこと、ファイルが長くなるほど遵守率が下がるので短く保つことを求めています。40項目にもなるi18nセクションは、流し読みされてしまいます。
単に「i18nを使う」ではなく、この10個のルールにしているのはなぜですか?
「i18nを使う」というのは、エージェントが<Button>Save changes</Button>と書いているときでさえ、すでに自分ではやっているつもりでいる内容だからです。曖昧な指示は、エージェント自身の「守っているつもり」で満たされてしまいます。以下の各ルールは、具体的な出力を名指しして禁止しています。
属性内のリテラル。 エージェントはJSXの子要素が翻訳対象だと学習していますが、aria-label="Close"やalt="Company logo"は属性が表示テキストではなく設定値のように見えるため、プレーンな文字列として書いてしまいます。対象の属性を名前で列挙すれば、この抜け穴を塞げます。「すべて翻訳する」という一般的な指示では塞げません。
文字列連結。 t('greeting') + ' ' + name + '!'は英語ならそのまま表示できますが、名前が先頭に来る言語や、名前が語形変化する言語には翻訳できません。変数補間なら、翻訳者が扱うのは、移動すべき変数が1つだけの1本の文字列で済みます。
三項演算子による複数形処理。 count === 1 ? 'item' : 'items'は、i18nのセットアップ後にエージェントが最もよく書いてしまうパターンで、複数形が3つ以上ある言語ではすべて誤りになります。このルールではパターンを名指ししているため、エージェントはそれと照合して回避できます。カタログでICUを使っている場合は、ICU MessageFormatとは何か、AI生成アプリのどこで破綻するのかで構文を解説しています。
生成されたロケールファイル。 このルールが必要なのは、エージェントが親切だからです。ドイツ語の誤字を直すよう頼まれると、エージェントはlocales/de.jsonを開いて直接編集します。次の翻訳ジョブがソースからファイルを再生成するまでは問題なく動きますが、再生成されれば修正は消えます。ソース以外のファイルは生成物であること、そして本来の修正先がどこなのかを伝えておけば、気づきにくいリグレッションを防げます。
同じ変更内でのenの編集。 このルールがないと、エージェントはt('checkout.summary.total')を追加するだけで先に進んでしまい、誰かが気づくまでキー名がそのまま画面に表示されます。カタログのエントリと最初の使用箇所は、1つのdiffにまとめるのが原則です。
各ツールは実際にどこでファイルを読み込むのですか?
同じ内容でも、エージェントごとに置く場所が異なります。置き場所によって、ルールが常にコンテキストに入るのか、必要なときだけ読み込まれるのかが決まります。
リポジトリルートのAGENTS.mdが共通の置き場所です。フォーマットは必須フィールドのないプレーンなMarkdownで、Linux Foundation傘下のAgentic AI Foundationが管理しており、Cursor、Codex、Copilotコーディングエージェントをはじめ、数多くのツールが読み込みます。仕様ではネストされたファイルにも対応しており、編集中のファイルに最も近いものが優先されます。
Cursorは、ネストされたものも含めAGENTS.mdを直接読み込み、独自のスコープ付きフォーマットも備えています。UIファイル向けにスコープを絞ったルールは、該当するファイルを開くまでコンテキストに入りません。
---
description: i18n rules for user-visible strings
globs: src/**/*.tsx, src/**/*.jsx
alwaysApply: false
---
(paste the Internationalization section here)
ファイルの拡張子は.mdcにして、.cursor/rules/に置く必要があります。このフォルダ内のプレーンな.mdは、フロントマターがないため無視されます。
VS CodeのGitHub Copilotには3つの選択肢があります。.github/copilot-instructions.mdはすべてのチャットリクエストに適用されます。フロントマターにapplyTo: "**/*.tsx,**/*.jsx"を指定した.github/instructions/i18n.instructions.mdファイルは、該当ファイルが作成または変更されたときだけ適用されます。そして、リポジトリルートのAGENTS.mdは、chat.useAgentsMdFile設定がオンのときにLocalエージェントが読み込みます。サブフォルダ内のネストされたAGENTS.mdは別の試験的設定chat.useNestedAgentsMdFilesで制御され、デフォルトではオフです。Copilot固有の挙動については、GitHub Copilotで作ったアプリにi18nを導入する方法で解説しています。
Claude CodeはCLAUDE.mdを読み込みます。また、v2.1.277以降は、作業ディレクトリより上の階層にCLAUDE.mdがない場合、ルートのAGENTS.mdを単独で読み込みます。両方を併用する場合、ドキュメントで案内されている方法は、CLAUDE.mdの先頭に@AGENTS.mdインポートを置き、ファイルが1回だけ読み込まれるようにするパターンです。スコープ指定には、フロントマターにpaths:リストを記述した.claude/rules/i18n.mdを使います。このファイルは、Claudeが該当ファイルを読んだときにだけ読み込まれます。Claude Codeのワークフロー全体は、Claude Codeが生成したアプリをローカライズする方法で解説しています。
複数のエージェントが触れるリポジトリでの現実的な構成は、チームが最もよく使うエージェント向けのスコープ付きファイルに10個のルールを置き、AGENTS.mdには2行のポインタ(「UI文字列は.cursor/rules/i18n.mdcのi18nルールを参照し、従うこと」)を置いておく方法です。こうしておけば、スコープ指定に対応していないエージェントにも指示が届きます。
AGENTS.mdを読み込まないコード生成の場面はどこですか?
インライン補完です。これは両ベンダーが明記している事実であり、ルールファイルだけでは対策として不十分な理由でもあります。
Cursorのルールに関するFAQでは、「ルールはCursor Tabやその他のAI機能に影響しますか?」という問いに、はっきり「いいえ」と答えています。ルールが反映されるのはAgentだけで、入力中の行を補完するオートコンプリートであるTabには、ルールが見えていません。VS Codeのカスタムインストラクションのページにも、Copilotについて同様の注記があり、エディタでの入力中のインライン提案では、インストラクションは考慮されないとされています。
つまり、開発者が<p>No results found</p>と入力してゴーストテキストを受け入れる場面こそ、ルールが届かない場面なのです。チャットやエージェントモードなら文字列にキーを付けてくれますが、他のことを考えている間に行を補完してしまうインライン補完は、そうしてくれません。通常の作業を1週間続けるうちに、インストラクションファイルがカバーしていない経路からリテラルが蓄積していきます。開発者は丁寧にルールファイルを書いたつもりなので、エージェントがそれを無視していると思い込んでしまいます。
Claude Codeにはインライン補完の場面がありませんが、ドキュメントは逆の側から同じ点を指摘しています。CLAUDE.mdの内容はコンテキストであって、強制される設定ではありません。モデルの判断にかかわらず特定の操作をブロックしたい場合は、フックを使います。これは、ここで取り上げたすべてのツールに当てはまる正しい捉え方です。インストラクションファイルは確率を傾けるだけで、強制はしません。
ルールを実際に強制するものは何ですか?
CIのlintルールです。ルールファイルでエージェントにlintの実行を指示済みなら、追加は1行で済みます。eslint-plugin-i18nextにはno-literal-stringが同梱されています。コンポーネントのディレクトリで有効にすれば、JSXにリテラルがあるとやんわり注意されるのではなく、ビルドが失敗します。セットアップ方法、ルールのオプション、そしてインストラクションと強制がなぜ別のレイヤーなのかは、i18nセットアップ後もCursorがハードコード文字列を追加し続ける理由で詳しく説明しているので、この記事では繰り返しません。
lintレイヤーには、ルールファイルにはできないことが2つあります。1つは、lintは会話ではなくファイルに対して実行されるため、インライン補完の出力も検出できることです。もう1つは、エージェント自身のループが修正につながることです。上のブロックの最後のルールは、完了前にlintを実行するようエージェントに指示しています。no-literal-stringが失敗するのを見たエージェントは、同じセッション内で自分から文字列にキーを付けます。
Claude Codeの場合は、PreToolUseまたは編集後のフックで、書き込んだばかりのファイルにlinterを実行する方法が、ドキュメントの案内する強制手段です。フックは決まったタイミングでシェルコマンドとして実行されるため、モデルがルールに従うかどうかにかかわらず適用されます。
それでもすり抜けた文字列はどうなりますか?
抽出して既存のネームスペースに沿ったキーを付け、翻訳する必要があります。この部分こそ、プロンプトで指示し直すのではなく、自動化する価値があります。ビルドが赤くなれば、リテラルがあることはわかります。しかし、それをキーに変換し、enに追加して、他のすべてのロケールに反映する作業は、誰かがやらなければなりません。
globalize.nowは、このレイヤーを担います。変換はアプリ内で行う初回のみの作業です。リポジトリを接続すると、コードベースが一度だけ変換され、カタログはレビュー可能なプルリクエストとして届きます。その後は、プッシュジョブが新しいカタログユニットを随時翻訳するため、火曜日のPRで追加したキーの翻訳は、水曜日のPRに入ります。ランタイムライブラリ、カタログのフォーマット、上記のルールファイルは、すべてご自身で管理できます。セットアップ後にCursor、Claude Code、Codex、Copilotのエージェントがどのような役割を担うかは開発者向けリファレンスで解説しています。最短の手順はCursor連携をご覧ください。
関連する2つの失敗パターンについては、それぞれ別の記事があります。すでにキーを付けたコピーをエージェントが書き換えてしまう問題は、AIエージェントにUIコピーを書き換えさせない方法で扱っています。対策は、上のルール9と同じ、ソースロケールを守る運用です。ロケールは用意してあるのに内容がずれていく場合は、翻訳ファイルが同期ずれを起こす理由で、その背景にあるルーティングの失敗を解説しています。
どこから始めればよいのでしょうか?
ブロックを貼り付けてUIのディレクトリに適用範囲を絞り、その日のうちにCIでno-literal-stringを有効にしましょう。そして1週間、lintルールが何を検出するかを観察してください。それが、ルールファイルだけでどこまで防げていたのかを測る指標になります。AIで作ったアプリをリリースしていて、すり抜けた文字列の抽出と翻訳を自前で運用せずに任せたい場合は、概要をVibe Coder向けページで、料金を料金ページでご確認ください。
globalize.nowは、ハードコードされたアプリ内テキストを翻訳可能なロケールファイルに変換し、リリースのたびに自動で最新の状態を保ちます。
globalize.nowを無料で試す