UIコピーの一貫性は、ユーザーに表示されるすべての文字列をリポジトリ内の単一のカタログに置き、キーで参照し、レビューを経たdiffでのみ変更することで保たれます。ロジックにすでに適用している規律を、文言にも適用するということです。このページの残りは、それを定着させるためのプラクティスです。

これは実践編のガイドです。その背景にあるアーキテクチャと、なぜコードベースをプロダクトコピーの信頼できる情報源にすべきかという主張は、コードネイティブなプロダクトコピーで解説しています。理由を知りたい方はそちらを、やり方を知りたい方はこのまま読み進めてください。

UIコピーはどこに置くべきか?

ソース言語ごとに1つのカタログファイルとして、リポジトリ内の、それを描画するコードのそばに置きます。 スプレッドシートでも、デザインファイルでも、3月時点では正確だったWikiページでもありません。画面に何と書いてあるかを知りたいとき、開発者もレビュアーも翻訳者もコーディングエージェントも、まず見るべき場所がカタログです。

ここから導かれる実践的なルールは次のとおりです。

  • カタログは1つ、ソースロケールも1つ。 単一のen.json(または.po、.ftl、.stringsなど、使用しているi18nライブラリが読み込む形式)が正本となるコピーです。モノレポに複数のアプリがある場合は各アプリに1つずつ用意し、共通の文字列は両者がインポートする共有カタログに置きます。避けるべきなのは、同じメッセージを保持しうるファイルが2つ存在することです。
  • コンポーネント単位ではなく、画面または機能単位でグループ化する。 checkout.summary.totalのようなキーは、SummaryCard.totalLabelよりも長持ちします。コンポーネントは名前が変わったり分割されたりしますが、画面とメッセージは変わらないからです。
  • キーには文言ではなく、メッセージの意味で名前を付ける。 checkout.submitなら「注文する」から「今すぐ購入」に文言が変わっても使い続けられます。checkout.placeOrderButtonではそうはいかず、キーとテキストのずれが原因で、後からレビュアーが別々の文字列だと思い込んでしまいます。
  • カタログは、それを使うコードと同じプルリクエストに含める。 文字列を追加するPRとそのコンポーネントを追加するPRが別だと、その文字列は見落とされるか、忘れられるか、重複します。

文字列がまだコンポーネント内のリテラルのままなら、最初に作るべきものはカタログです。これを手作業で行うのはそれだけで一大プロジェクトになるため、リポジトリを接続して変換を任せるという選択肢があります(後述します)。

リテラルを検索するより、キーで参照するほうが優れているのはなぜか?

キーは1つの文字列にしか解決されませんが、リテラルは開発者の数だけ異なる書き方ができてしまうからです。 「Sign in」「Sign In」「Log in」「Login」は、検索ツールにとっては4つの文字列ですが、ユーザーにとっては1つの意図です。すべてのコンポーネントがt("auth.signIn")を呼び出していれば、どの文言が正しいかという問いには1か所で一度だけ答えが出て、その答えはキーが使われているあらゆる場所に適用されます。

これを機能させ続けるための習慣が2つあります。

  • 追加する前に再利用を検討する。 キーを作る前に、カタログでそのメッセージを検索します。common.cancelがすでにあるなら、同じテキストの新しいdialog.cancelButtonを作るのは無害な重複ではなく、ドリフトの始まりです。どちらかが変更された途端、2つは別々に編集されていきます。
  • 文字列を連結しない。 t("cart.items") + " " + countは、カタログのどの行にも対応しないテキストを生み出します。ライブラリの補間機能や複数形のフォームを使い、文全体を1つのレビュー可能な単位にしましょう。

よく持ち出される例外が「この文字列は1回しか出てこないから、リテラルで問題ない」というものです。たしかに今日は1回です。しかしコンポーネントはコピーされ、画面には兄弟画面ができ、リテラルも一緒に複製されて、やがて食い違っていきます。

プルリクエストでコピーの変更をレビューするには?

変更されたカタログの行は、変更された関数シグネチャと同じように扱います。つまり、レビュアーが読み、疑問を投げかけ、承認するものです。 このプラクティスによって、一貫性は「記憶力の問題」から「レビューの問題」に変わります。そしてレビューは、誰かが確実に注意を払ってくれる唯一のタイミングです。

カタログレビューを機能させるポイントは次のとおりです。

  • 文言の変更は、diffの中で独立した1行になる。 ただしそれは、文字列がカタログにある場合に限ります。コンポーネント内での文言変更もdiffの行にはなりますが、ロジックの変更に紛れ、「ただのテキストだから」と読み飛ばされてしまいます。
  • カタログの変更はコピーのオーナーにルーティングする。 カタログのパスにCODEOWNERSのエントリを設定すれば、文言の担当者(プロダクト担当、コンテンツデザイナー、あるいは最も気にかけている開発者)が、変更のたびにレビュアーとして自動的にアサインされます。PRのそれ以外の部分は通常のレビューで進みます。
  • 変更された行ごとに3つの問いを投げかける。 これは、そのものを指す承認済みの用語か。周囲の文字列とトーンや表記が揃っているか。キー名は今もメッセージを表しているか。この3つを確認するレビュアーがいれば、スタイルガイドが防ごうとするものの大半を拾えます。
  • 黙ったままの文言変更は差し戻す。 auth.signInの「Sign in」を理由の説明なく「Log in」に変えるPRは、たまたま編集していた人が行ったコピー上の意思決定です。PRの説明に1文で理由を書くか、元に戻す必要があります。

これによって防げる失敗パターンには名前があります。コピードリフトとは、ユーザーに表示されるテキストが、複数の画面や時間の経過のなかで、承認された元の文言から乖離していく現象で、しかも乖離元となる唯一の情報源が存在しない状態を指します。コピードリフトとはでその原因を解説していますが、diffでのカタログレビューは、その大半を防ぐプラクティスです。

新たなハードコード文字列の混入を防ぐには?

新しいリテラルがあればビルドを失敗させます。 ルールやレビューが拾えるのは、人が思い出して確認したものだけです。lintルールは、誰かが思い出す必要もなく、すべてのプルリクエストで残りを検出します。

セットアップはごく簡単です。

  • no-literal-stringルールを有効にする。 eslint-plugin-i18nextがJavaScriptおよびTypeScriptプロジェクト向けに用意しています。適用範囲はJSXのテキストと、入力欄のヒント、title、aria-label、altといったユーザー向け属性に絞り、テストファイルとStorybookのストーリーは対象外にして、ルールの信頼性を保ちましょう。
  • マージが行われる場所で実行する。 pre-commitフックは手軽ですが、実効性があるのはCIのステップです。作成者のエディタが設定済みかどうかに関係なく、プルリクエストで必ず実行されるからです。
  • 未使用キーと不足キーのチェックを追加する。 ほとんどのi18nライブラリには、コードで参照されているのにカタログにないキーと、カタログにあるのにどこからも参照されていないキーを列挙する補助ツールがあります。これもCIで実行しましょう。古い文言が潜んでいるのは、未使用のキーです。
  • 最初は厳格にし、例外はコメントで許可する。 components/フォルダ全体で無効化されているルールは、もはやルールではありません。理由を添えて1行だけ無効化されているルールは、ドキュメントになります。

AIコーディングツールで開発しているチームは、この問題により強く直面します。ツールはローカルのコンテキストからコンポーネントを再生成し、そのたびにリテラルを持ち込むからです。Cursor AIがハードコード文字列を追加し続ける理由では、ルールファイル、リンター、CIの3層構成も含めて、このlintルールを詳しく解説しています。

開発者向けのコピースタイルガイドには何を盛り込むべきか?

1画面に収まる短いファイルをリポジトリに置き、トーンを示し、用語を固定し、文字列の置き場所を定めます。 長いスタイルガイドはWikiに置かれ、一度読まれて終わりです。短いものはコードのそばにあり、文字列が追加されるたびに人にもツールにも読まれます。

実用的なCOPY.md(または既存のコントリビューターガイド内のセクション)には、次の内容を含めます。

  1. トーンを3文で。 プロダクトが誰のような話し方をするのか、どの程度フォーマルか、「you」と呼びかけるのか「the user」と第三者として扱うのか。たいていの議論に決着をつけるには十分です。
  2. 決して揺らさない用語。 各機能のプロダクト内での呼び名、主要なアクションの動詞(「保存」か「更新」か、「削除」か「取り除く」か)、そして使わないと決めた言葉。
  3. 大文字小文字と句読点のルール。 ボタンや見出しをsentence caseにするかtitle caseにするか、ツールチップに句点を付けるかどうか、数値や日付の書き方。
  4. 文字列の置き場所とキーの命名方法。 カタログのパス、グループ化のルール、キーの命名ルール、そして「追加する前に検索する」こと。
  5. 迷ったときの対処。 誰に聞くか、あるいは当面どのキーを再利用するか。

リポジトリ内に置いておけば、ルールファイルから参照でき、レビュアーはコメントでリンクでき、コーディングエージェントはラベルを書く前に読むことができます。

AIコーディングエージェントによる一貫性の崩れを防ぐには?

エージェントには、参照すべき信頼できる情報源と、守られなかったときに検知するゲートの両方を与えます。 フォームを生成するエージェントは、ある画面で「Submit」、次の画面で「Send」と平気で書きます。それぞれの場面で最もありそうな単語だったからです。不注意なのではなく、揃えるべき基準がそもそも手元にないのです。

たいていの問題は、2つの対策で解決します。1つ目はルールファイル(AGENTS.md、.cursor/rules、CLAUDE.mdなど、使用しているツールが読み込むもの)で、ユーザーに表示されるテキストはカタログに置くこと、既存のキーを再利用すること、ラベルを書く前にCOPY.mdを読むことを定めます。2つ目は前のセクションで紹介したlintルールです。ルールファイルは推測を減らし、lintルールはすり抜けた推測を検出します。

このページでエージェントについて述べることは以上です。エージェントがすでにある文字列を書き換えてしまう問題と、その編集をこっそりではなく目に見える形にする方法は、別のテーマとして扱っています。AIコーディングエージェントによるUIコピーの書き換えを防ぐ方法をご覧ください。

ローカライゼーションはどこに関わってくるのか?

一貫性とローカライズはどちらも「合意された単一の情報源」を必要とするため、その土台となるカタログがあれば、両方を実現できます。 3通りの表現のどれが承認済みか言えないチームは、そのどれも正しく翻訳できません。ソースを整えれば、翻訳は独自の「真実のコピー」を持つ別プロジェクトではなく、カタログから派生する成果物になります。

実際には、一貫性のために作ったカタログが、そのまま翻訳者や翻訳ツールの作業対象のファイルになります。各キーに対して対象言語ごとの訳文が付き、同じキー名が適用され、ソースカタログの文言変更は、翻訳側が追随すべき変更として可視化されます。翻訳がソースに遅れてずれていくままだと、翻訳ファイルが同期ずれを起こす理由で解説している種類の問題が起こります。その解決策もやはり同じ規律で、カタログを1つにし、キーで参照し、変更をdiffで確認することです。

globalize.nowの位置づけ

globalize.nowは、ローカライズを組み込んだコードネイティブなコピー管理システムであり、このページで紹介したすべてのプラクティスは、お使いのリポジトリがそうなっていることを前提としています。 アプリでGitHubまたはGitLabのリポジトリを接続すると、スキャンで内容を把握します。文字列がコンポーネント内のリテラルであれば、1回限りの変換でそれらをキー参照に置き換えてカタログに移し、i18nのセットアップがなければ追加します。next-intl、react-i18next、i18next、Linguiをすでに使っている場合は、既存の構成のまま利用できます。結果は専用ブランチのプルリクエストとして届くため、最初のカタログも、上で説明したレビューを通ります。その後、新しく追加したソース文字列はプッシュジョブで翻訳され、結果がプルリクエストとして作成されます。信頼できる情報源は、常にリポジトリだけです。

私たちが目指しているのは、マーケティングサイト、ランディングページ、プロダクトの全体で、同じ規律を単一のソースから保つことです。機能が、ユーザーが触れるあらゆる場面で同じように説明されるようにするためです。これは私たちが向かっている方向であり、今日すぐに購入できるものではありません。料金は料金ページをご覧ください。

今週から導入できるチェックリスト

  1. ソース言語ごとに1つのカタログをリポジトリ内に作成し、キーの命名ルールを決める。
  2. 最もよく触る画面の文字列からカタログに移し、キーで参照する。
  3. ユーザー向けテキストに絞ったno-literal-stringのlintルールを有効にし、CIで実行する。
  4. カタログのパスにCODEOWNERSの行を追加する。
  5. COPY.mdを書く:トーン、固定する用語、大文字小文字のルール、文字列の置き場所。1画面に収める。
  6. エージェントのルールファイルから、カタログとCOPY.mdを参照するように設定しましょう。
  7. PRの説明に理由が書かれていない文言の変更は、レビューで差し戻してください。

2つ目のステップが1か月がかりの作業に見えるなら、リポジトリを接続して、提案されたプランを確認し、作成されたプルリクエストをレビューしましょう。

globalize.nowは、ハードコードされたアプリ内テキストを翻訳可能なロケールファイルに変換し、リリースのたびに自動で最新の状態を保ちます。

globalize.nowを無料で試す