マガジン

ゲームの多言語対応の実装:切り替えと保存

遊びを届ける

執筆にClaudeを使用しています。

活版のような抽象的な字形とローラーを並べ、太さや間隔を検討する写真風イメージ。
記事のテーマをもとに制作したAI生成イラスト。

ブラウザゲームで多言語対応を考えるとき、最初に直面するのは「どこでテキストを管理するか」と「選んだ言語をどう記憶させるか」という二つの問いです。翻訳テキストをどのファイルに置いて、どのタイミングで切り替え、次回起動時にも同じ言語を保てるか——この仕組みを最初に設計しておかないと、テキストが増えるたびにコード全体を掘り返すことになります。 この記事では、HTMLとJavaScriptで書かれたブラウザゲームを前提に、言語切り替えの実装パターンと、選択状態の保存方法を具体的に示します。どのゲームエンジンやAIツールで雛形を生成した場合でも応用できるよう、考え方を中心に説明します。

この記事の目次
  1. テキストを一か所に集める「ロケールオブジェクト」
  2. 切り替え処理の実装と「再描画」の考え方
  3. 言語選択を保存する:`localStorage` の使い方
  4. 動的に生成されるテキストへの対応
  5. RTL(右から左)言語を追加するときの注意点
  6. 次の行動
  7. 参考資料

テキストを一か所に集める「ロケールオブジェクト」

多言語対応の土台は、すべての表示テキストをコードの外に出すことです。ボタンのラベルや説明文をHTMLやJavaScriptの中に直書きしていると、言語を追加するたびにソース全体を検索・置換しなければなりません。

代わりに、次のような構造のオブジェクトを一つのファイル(例:locales.js)にまとめます。

js
const LOCALES = {
  ja: {
    start: "スタート",
    score: "スコア",
    gameover: "ゲームオーバー",
    restart: "もう一度遊ぶ",
  },
  en: {
    start: "Start",
    score: "Score",
    gameover: "Game Over",
    restart: "Play Again",
  },
};

キーはすべての言語で共通にします。ゲームロジック側は常に LOCALES[currentLang].start のように参照するだけで、どの言語が選ばれているかを意識しません。テキストの追加・修正もこのファイルだけを開けば済みます。

ゲーム規模が大きくなる場合は、言語ごとにJSONファイルを分けて fetch で読み込む方法もあります。その場合は非同期処理(async/await)が必要になりますが、初期ロード時に全言語分をまとめて取得するのか、切り替え時だけ必要な言語を取得するのかで実装の複雑さが変わります。テキスト量が少ないうちはオブジェクトをまとめて持つ方が単純です。

切り替え処理の実装と「再描画」の考え方

言語を切り替えるとき、単に変数を変えるだけでは画面には反映されません。現在表示されているすべてのテキストをDOMに再反映する処理が必要です。

基本的な構造は次のようになります。

js
let currentLang = "ja";

function setLang(lang) {
  currentLang = lang;
  renderUI();
}

function renderUI() {
  const t = LOCALES[currentLang];
  document.getElementById("btn-start").textContent = t.start;
  document.getElementById("label-score").textContent = t.score;
  // ...
}

renderUI を「UIをまるごと描き直す関数」として設計しておくのがポイントです。言語切り替えだけでなく、スコアの更新や画面遷移のたびにこの関数を呼ぶようにすると、「切り替えたのに一部だけ変わらなかった」というバグが起きにくくなります。

一方でよくある失敗は、テキスト設定の処理があちこちに散らばっているパターンです。たとえばゲームスタート時に document.getElementById("btn-start").textContent = "スタート" と直書きしている箇所が残っていると、言語切り替え後にその画面を開き直したとき日本語に戻ってしまいます。テキストを出力する経路を renderUI に一本化することで、このような見落としを防げます。

言語選択を保存する:`localStorage` の使い方

ページをリロードするたびに言語がリセットされると、プレイヤーは毎回設定しなければなりません。ブラウザの localStorage を使うと、同じブラウザ・同じオリジン(ドメインとポートの組み合わせ)であれば設定を保持できます。

保存と読み込みは次の形が基本です。

js
// 保存
function setLang(lang) {
  currentLang = lang;
  localStorage.setItem("gameLang", lang);
  renderUI();
}

// 読み込み(ゲーム起動時)
function initLang() {
  const saved = localStorage.getItem("gameLang");
  currentLang = saved && LOCALES[saved] ? saved : "ja";
  renderUI();
}

LOCALES[saved] の確認が重要です。保存されていた値が存在しない言語コードだった場合(たとえば以前の実装で使っていたキー名を変えたとき)に、フォールバックとしてデフォルト言語に戻せます。

localStorage はブラウザごと・デバイスごとに別々に保存されます。スマートフォンで設定した言語はPCには引き継がれません。アカウントに紐づけてサーバー側に保存したい場合は別途APIが必要ですが、手軽に始めるには localStorage で十分な場面がほとんどです。

また、プライベートブラウズモードでは localStorage への書き込みが制限されたり、ブラウザを閉じると消えることがあります。そのため setItem を呼ぶ際は try/catch で囲んでおくと安心です。書き込みに失敗してもゲームが壊れないようにするためです。

動的に生成されるテキストへの対応

スコア表示や「〇〇面クリア」のように、数値や変数を含む文字列は単純な置き換えでは対応できません。日本語と英語では語順が異なるため、文字列の連結で作ると翻訳が難しくなります。

テンプレート関数を用意する方法が実用的です。

js
const LOCALES = {
  ja: {
    stageCleared: (n) => `${n}面クリア!`,
    highScore: (n) => `ハイスコア:${n}`,
  },
  en: {
    stageCleared: (n) => `Stage ${n} Cleared!`,
    highScore: (n) => `High Score: ${n}`,
  },
};

// 使う側
LOCALES[currentLang].stageCleared(3); // "3面クリア!" または "Stage 3 Cleared!"

値が一つの場合はこれで十分ですが、複数の変数が混じる文では {player}が{score}点獲得 のようなプレースホルダー形式にしておくと可読性が上がります。プレースホルダーの置換は数行の関数で実装できます。

RTL(右から左)言語を追加するときの注意点

アラビア語やヘブライ語のように右から左に読む言語(RTL)は、テキスト切り替えだけでなく、レイアウト全体の向きも変える必要があります。HTMLの <html> タグや <body> に dir="rtl" を付けると多くの要素が自動で反転しますが、CSSの margin や padding で左右を固定している箇所は崩れることがあります。

RTL対応を本格的に行う場合は、CSSの論理プロパティ(margin-inline-start など、左右ではなく「文章の流れる方向の先頭」を指定するプロパティ)を使うと対応が楽になります。ただし、対象言語が日本語と英語だけであれば考慮不要です。将来的にRTL言語を追加する可能性があるなら、最初からレイアウトをフレックスボックスや論理プロパティで組んでおくと後から変更が少なくて済みます。

次の行動

まずは locales.js に日本語と英語のテキストをまとめ、initLang() と renderUI() の二つの関数を実装するところから始めてみてください。動的テキストが出てきた時点でテンプレート関数を追加し、localStorage の読み書きは最後に組み込むと、段階的に完成度を上げられます。アソバースへ投稿する際はZIPにまとめることで複数ファイルの構成をそのまま公開できます。

参考資料

  • MDN Web Docs「Window: localStorage プロパティ」
  • MDN Web Docs「CSS 論理プロパティと値」
  • Unicode Common Locale Data Repository(CLDR)