ストリングキーとは? 成長に耐える命名設計のルール
ストリングキー(文字列キー、ストリングID)とは、画面に出すテキストを呼び出すためにコード側が付ける名前です。コードは名前だけを持ち、文言は翻訳ファイルが持ちます。
結論から書くと、キーは小文字・階層構造・安定したIDにします。どこで使われ、どんな役割かを名前で表し、いま入っている文言は名前に入れません。以下では理由、リンタに渡せる書式ルール、4つのエンジンの同一性、改名の手順を書きます。
キーは、それを書いたコードより長く生き残ります。翻訳メモリもレビュー履歴も下流のツールも、すべてキーを軸に紐づいているからです。規則はハードコードされたテキストを文字列テーブルに追い出す作業と同時に決めるのが一番安いです。
キーの3方式と、それぞれが払う代償
階層的なIDは、ドット区切りでパスのように読めます。前方一致で検索すれば1画面分の文字列だけが取れ、レビュー担当はゲームを起動せずに場所が分かります。代償は階層を早く決める必要があることと、後から別画面へ移った文字列が場所について嘘をつくキーを持つことです。
フラットなIDは階層を持ちません。最初の問題は衝突で、2つ目の画面が title や confirm を欲しがった時点で起きます。対処として接頭辞の慣習が生まれますが、settings_audio_master_volume は区切り文字のない階層で、ツールから見れば階層ではなく、グループ化はすべて前方一致の当て推量になります。
3つ目は原文そのものをキーにする方式です。gettext系がこれで、msgid が英語の文になります。導入コストはゼロですが、壊れ方が4つあります。
- 原文を直すとキーも変わるので、既存の翻訳が全部宙に浮きます。誤字修正で意味が動いていなくても同じです。
- 同じ原文が2箇所で使われていると訳は1つしか入りません。英語は使い回しに寛容ですが訳し先はそうではなく、2つを分ける別のフィールドが必要です。gettextでは msgctxt です。
- 長い文はそのまま長いキーになり、キー一覧の差分が文章の差分になります。
- キーを見ても文字列がどこに出るか分かりません。場所の情報はコメントやソース参照に置くことになり、表計算ソフト経由の往復で失われがちです。
階層的 ui.settings.audio.master_volume
フラット settings_audio_master_volume
原文キー msgid "Master Volume"
msgctxt "settings/audio"1分で再現できるキーの不具合3つ
1つ目は並び順です。キーを並べ替えるは曖昧な指示で、どちらも並べ替えたと主張する2つのツールが別のファイルを出します。単純なコード単位のソート(JavaScriptの配列のソートの既定)は大文字を小文字より前に置き、アンダースコアは大文字より後、小文字より前に置きます。アンダースコアがコードポイント95で、Zの90とaの97の間だからです。実際に並べると audioMaster、audio_2、audiob の順になります。このルールは大文字を禁じているので、実際のキー一覧ではアンダースコアは数字より後、全英字より前に来ます。ロケール対応の比較は記号を先に置き小文字を大文字より前にし、数値オプションで数字の並びも変わります。下の3つはどれも並べ替え済みで一致しません。
実害は見た目ではありません。書き出しツールとエディタが別の順で並べ直すと、コミットごとに数百行の移動が出て本当の変更が埋もれます。順序を1つ決めて書き出しスクリプトに実装します。
2つ目はキーの重複で、これを隠すのはJSONです。ui.menu.continue に Continue を入れた後で同じキーに Resume を入れたファイルは、値が Resume の1キーになります。警告は出ず、JavaScriptのパーサでもPythonのjsonモジュールでも同じです。重複チェックはパース前の生テキストに走らせます。
3つ目は大文字小文字だけが違うキーです。ui.Settings.audio.master_volume と ui.settings.audio.master_volume はJSONでも多くのエンジンでも別のキーで、片方を引いてももう片方は見つかりません。それでも大文字小文字を区別しないものが読んだ瞬間に1つに潰れます。大文字小文字を無視する表計算の照合、照合順序が大文字小文字非区別のデータベース列、キーから生成する音声ファイル名です。対処はキーに大文字を使わないことです。
対象: ui.Settings.audio ui.settings.audio ui.settings.audio2
ui.settings.audio10 ui.settings.audio_2 ui.settings.audioMaster
コード単位: Settings.audio settings.audio audio10 audio2
audioMaster audio_2
ロケール対応: settings.audio Settings.audio audio_2 audio10
audio2 audioMaster
ロケール+数値: settings.audio Settings.audio audio_2 audio2
audio10 audioMaster書き出してリンタに渡せる命名ルール
書式を正規表現で書き、テストと同じ工程で走るチェックに入れれば、規約は誰がその文字列を追加したかに依存しなくなります。
- 場所と役割を名前にし、文言は入れません。ui.settings.audio.master_volume は英語を書き直しても正しいままですが、continue_button_text はボタンが Resume になった瞬間に嘘です。
- セグメントは広いものから狭いものへ並べ、最初のセグメントの語彙は小さく保ちます。
- 最終セグメントで役割が自明な場合(start、hint、greeting など)を除き、末尾に役割の接尾辞を1つ付け、閉じた一覧から選びます。title、body、button、tooltip、placeholder、error、label。翻訳者が4語で書くのか文で書くのかの判断材料はここです。
- 数を含むテキストは、形式に複数形の仕組みがあればそれを使い、1キーに全バリアントを持たせます。書き方はICU MessageFormatの複数形とselectの解説にあります。無い形式ならCLDRのカテゴリ名を接尾辞にします。英語は one と other、日本語は other だけ、フランス語は3つ、ロシア語とポーランド語は4つ、アラビア語は6つ必要です。
- プレースホルダは位置ではなく名前で持ち、どの言語でも同じ名前にします。不一致を機械的に検出できるからです。arg0 や arg1 ではなく player_name や item_count にします。
- 禁止は4つ。数字だけのキー、原文の一部から作ったキー、日付やチケット番号を含むキー、temp や new や final のような作業の状態を表す語です。
^[a-z][a-z0-9]*(_[a-z0-9]+)*(\.[a-z][a-z0-9]*(_[a-z0-9]+)*){1,4}$
PASS ui.settings.audio.master_volume
PASS shop.item.buy_confirm_body
PASS tutorial.hint_1
FAIL ui.Settings.Audio.MasterVolume 大文字
FAIL ui.settings.audio.master-volume ハイフン
FAIL ui.settings.audio.master volume 空白
FAIL 0012 数字だけ
FAIL Are you sure you want to quit? 原文がキー
FAIL ui.settings.2ndPlayer 数字で始まるセグメント
FAIL a.b.c.d.e.f 6セグメント(上限5)
FAIL ui..settings.audio 空のセグメント長さと深さに標準規格はないので、基準点を1つ置きます。小規模なゲームのメニュー・設定・ショップ・戦闘HUD・チュートリアル・会話をカバーする下の25キーを上のルールで命名すると、長さは中央値26文字・平均24文字、最長37文字でした。深さは2から4セグメント、平均3.4で、3セグメントが最多です。
これは目標値ではなく点検用の基準点です。深さが6以上なら、別テーブルかファイル分割が持つべき情報を階層が抱えています。
ui.menu.start battle.hud.hp_label
ui.menu.continue battle.hud.mp_label
ui.menu.options battle.result.victory_title
ui.menu.quit battle.result.defeat_title
ui.settings.audio.master_volume tutorial.movement.hint
ui.settings.audio.music_volume tutorial.combat.hint
ui.settings.audio.sfx_volume dialogue.ch01.innkeeper.greeting
ui.settings.audio.mute_button dialogue.ch01.innkeeper.farewell
ui.settings.display.resolution_label shop.title
ui.settings.display.fullscreen_toggle shop.item.buy_button
ui.settings.language.title shop.item.buy_confirm_title
ui.settings.language.hint shop.item.buy_confirm_body
shop.item.insufficient_fundsUnity・Unreal・Godot・gettextは何を同一性にしているか
エンジンが何を同一性として扱うかで、改名がテキスト編集なのかデータ移行なのかが決まります。細部は各公式ドキュメントの、以下に挙げた章で確認してください。
UnityのLocalizationパッケージは、String Tableのエントリにキーと数値のIdの両方を持ち、対応はコレクションの共有テーブルデータ側にあります。参照はIdを経由して解決されるので、エディタでキーを改名しても既存の翻訳は付いたままです。パッケージのドキュメントのString Tablesの章で確認してください。キーの出し入れはUnityのCSV入出力の手順にあります。
Unrealはテキストを名前空間とキーの組で識別し、原文も一緒に記録します。同じキーでも名前空間が違えば別エントリです。アセット側のテキストのキーは自動生成されるため、命名規則が効くのは名前空間と、コードで宣言するテキストです。書き出しの形式はドキュメントのローカライズの章にあり、自分で名前を付けるテーブルはUnrealのString Table CSVの扱いで扱っています。
Godot 4の tr 関数はキーをそのまま受け取り、2用途を分ける文脈引数を任意で取れます。CSVの翻訳ファイルは1列目をキーとして使います。同一性が自分で打った文字列なので、改名はコードと全翻訳ファイルを一度に置換する作業です。
gettext系は原文を同一性そのものにします。msgid が原文、msgctxt が2用途を分け、msgid_plural が数に応じた形を持ちます。構造はPOファイルの構造の解説にあります。命名の判断はキー名から msgctxt の値へ移ります。
翻訳を失わずにキーを改名する手順
履歴を引き継ぐ仕組みがない限り、改名は旧翻訳と新キーの結びつきを断ちます。単純な書き出しと取り込みのパイプラインにそれはありません。旧キーの翻訳が消え、再翻訳まで各言語が原文にフォールバックします。表計算ソフトのレビューでは見えず、ゲーム内では目立ちます。一括置換ではなく、成果物を残す移行作業としてやります。キーは文字列と一緒に外へ出ていくので、翻訳者に渡すローカライズキットにはキー一覧とそのキーが表す文脈も入れてください。
- そのリリースのキー一覧を凍結します。翻訳の依頼が開いている間は改名しません。
- 改名を2列の対応表ファイルとして書きます。1キー1行です。これが成果物でリポジトリに入り、変更がレビュー可能で再実行可能になります。
- 対応表をスクリプトで適用し、コードと全言語ファイルを同じ実行で書き換えます。コードだけ改名されて日本語のファイルが取り残されると黙ってフォールバックし、これが気づけない失敗です。
- 旧キーを1リリース分だけ翻訳ファイルに残し、同じ文言を指させます。拾い漏らした参照があっても見つけるまでは解決します。
- 終わったら両方向を報告します。対応表に無いのに使われているキーと、言語ファイルにあってコードに出てこないキーです。削除するのは後者で、それも前者が空になってからです。
old_key,new_key settings_audio_master,ui.settings.audio.master_volume settings_audio_mute,ui.settings.audio.mute_button buyConfirm,shop.item.buy_confirm_body