マガジン

「言語切り替え」を作り込む:文言の分離と切り替え時の保持

遊びをつくる

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

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

ステージ3の途中でスコアが1,200点、ヒントは残り1回という状況で、右上の「English」ボタンを押す場面を考えてみます。切り替わった瞬間に、タイトル画面へ戻ったりスコアが0に戻ったりしたら、遊んでいる人は二度と言語ボタンに触れなくなります。言語切り替えは機能として小さく見えますが、「押しても何も失わない」ことまで作って初めて完成します。 この記事で確かめるのは、表示言語を切り替えても進行を失わない設計がどう組めるかです。鍵は、ゲームの状態(進行)と画面に出す文言を別の層に分けることにあります。分けておけば、言語の切り替えは「言語の値を変えて、画面を描き直す」だけで済みます。 最後まで進むと、単独で動く40行ほどのHTMLを土台に、文言の辞書、状態の持ち方、切り替え時の確認手順が手元に揃います。ゲームの規模が大きくなっても、同じ考え方で広げられます。

この記事の目次
  1. 切り替えで失われるものを先に洗い出す
  2. 手順1:状態に表示用の文字列を持たせない
  3. 手順2:文言の辞書と、状態から描く関数を作る
  4. 手順3:数字・語順・長さに耐える文言にする
  5. 手順4:動いているものを止めずに描き直す
  6. 手順5:進行が保たれるかを自分で確かめる
  7. 次の行動

切り替えで失われるものを先に洗い出す

最初に、言語の切り替えで何が壊れうるかを書き出します。実装より前にこの一覧を作っておくと、あとで確認項目としてそのまま使えます。

  1. ページの再読み込み:言語ごとに別のURLへ移動する作りだと、メモリ上の進行がすべて消えます。
  2. 画面の作り直し:画面全体を破棄して作り直すと、入力欄の文字、選択中の項目、スクロール位置、フォーカスが消えます。
  3. 文言に埋め込まれた状態:「拾いました」という表示済みの文字列を状態として保存していると、切り替え後も日本語のまま残ります。
  4. 文言で判定する処理:if (answer === "はい") のように、表示文字列で分岐していると、英語にした途端に判定が壊れます。
  5. 動いているものの停止:タイマーやアニメーションが、描き直しの際に止まったり二重に動いたりします。

アソバースの公式作品13本は、設計として「言語変更とプレイ画面の拡大で進行を保つ」ことを共通の方針にしています。実際にどの作品がどの場面でどう保たれるかは、作品を開いてプレイ中に切り替えて確かめてください。この記事の手順は、その考え方を自作に取り入れるためのものです。

手順1:状態に表示用の文字列を持たせない

最初にやるのは、ゲームの状態として何を持つかの見直しです。原則は、状態には「言語に依存しない値」だけを入れることです。

  • スコア(数値)、持たせないもの:「集めた葉:3枚」という完成済みの文
  • 直前のメッセージのキー("ok")、持たせないもの:直前のメッセージの本文(「拾いました」)
  • 選択中の項目のID("recipe_3")、持たせないもの:選択中の項目の表示名
  • 現在の言語コード("ja" / "en")、持たせないもの:言語ごとの並び順に依存した添字

判断に迷ったら、「言語を切り替えたとき、この値も変えたいか」と自問します。変えたいなら表示用なので状態には置かず、描画の時点で作ります。変えたくないなら状態に置きます。

分岐も同じ考え方です。判定は常にIDや数値で行い、表示文字列は比較に使いません。選択肢ボタンを作るときは、ボタンに data-id="yes" のようなIDを持たせ、表示名だけを辞書から引きます。

手順2:文言の辞書と、状態から描く関数を作る

次に、文言を一か所の辞書にまとめ、画面を render() という一つの関数で描く形にします。以下は単独で動く最小構成です。HTMLファイルとして保存してブラウザで開くと動きます。

html
<!doctype html>
<html lang="ja">
<meta charset="utf-8">
<body>
<h1 id="title"></h1>
<p id="score"></p>
<button id="tap"></button>
<button id="lang"></button>
<p id="msg" aria-live="polite"></p>
<script>
const I18N = {
  ja: { title:"葉っぱ集め", score:"集めた葉:{n}枚", tap:"葉を拾う",
        lang:"English", full:"かごがいっぱいです", ok:"拾いました" },
  en: { title:"Leaf Picker", score:"Leaves: {n}", tap:"Pick a leaf",
        lang:"日本語", full:"The basket is full", ok:"Picked one" }
};
const state = { lang:"ja", n:0, msgKey:null };
const $ = id => document.getElementById(id);
const t = (k, v = {}) =>
  (I18N[state.lang][k] ?? I18N.ja[k] ?? k)
    .replace(/\{(\w+)\}/g, (_, x) => v[x] ?? "");

function render() {
  document.documentElement.lang = state.lang;
  $("title").textContent = t("title");
  $("score").textContent = t("score", { n: state.n });
  $("tap").textContent   = t("tap");
  $("lang").textContent  = t("lang");
  $("msg").textContent   = state.msgKey ? t(state.msgKey) : "";
}
$("tap").onclick = () => {
  if (state.n < 10) { state.n++; state.msgKey = state.n === 10 ? "full" : "ok"; }
  render();
};
$("lang").onclick = () => {
  state.lang = state.lang === "ja" ? "en" : "ja";
  render();
};
render();
</script>
</body>
</html>

このコードで押さえる点は4つあります。

  1. state.msgKey に本文ではなくキーを入れています。葉を拾ったあとに言語を切り替えると、メッセージも新しい言語で表示されます。ここが「保持」の中心です。
  2. t() は、現在の言語に訳がなければ日本語、それもなければキー名そのものを返します。訳し忘れても画面が空にならず、どのキーが足りないか見つけやすくなります。
  3. 言語ボタンの操作は state.lang の書き換えと render() の呼び出しだけです。再読み込みも画面の破棄もないため、state.n は変わりません。
  4. document.documentElement.lang も更新しています。読み上げ機能や、ブラウザの文字の選び方に影響するためです。

自分のゲームに移すときは、まず今ある文字列をすべて辞書のキーに置き換えます。検索で " や ' で囲まれた日本語を拾い出していくと漏れが見つかります。

手順3:数字・語順・長さに耐える文言にする

辞書に分けただけでは、まだ不十分な場面があります。言語によって、文の組み立てと長さが違うためです。

  • 文を部品で組み立てない:"集めた葉:" + n + "枚" のように継ぎ足すと、語順の違う言語で破綻します。上のコードのように、{n} を含む文全体を一つの訳として持ちます。
  • 単数と複数を分ける:英語では1枚と2枚以上で語形が変わる場合があります。数によって文を変えるものは leaf_one と leaf_other のように別キーにし、数で選ぶ関数を一つ挟みます。
  • 文字数の差を見込む:一般に、日本語から英語にするとボタンの文言が長くなりがちです。ボタンの幅を固定せず、折り返しや余白に余裕を持たせます。
  • 画像に埋め込んだ文字を減らす:文字入りの画像は言語ごとに差し替えが必要です。タイトルや説明は、画面上のテキストとして描くほうが管理しやすくなります。

なお、アソバースでは作者が入力した作品名や紹介文は、サイトの表示言語を切り替えても書き換えられません。ゲーム画面の中の文言は作者の側で用意するものなので、辞書を持つ設計は作者自身の仕事になります。

手順4:動いているものを止めずに描き直す

タイマー、アニメーション、入力中の欄がある画面では、render() の作りに一工夫必要です。

  1. 文字だけを更新する:innerHTML で画面を丸ごと作り直さず、既存の要素の textContent を書き換えます。要素が残るので、フォーカスや入力中の文字も保たれます。
  2. 動的に増える要素は、IDで差分更新する:一覧やボタン群を描くときは、既存の要素を状態のIDで探して文字だけを変えます。数が変わるときだけ追加・削除します。
  3. タイマーは状態の外で動かさない:残り時間は state.timeLeft に持ち、タイマー処理は数値を減らして render() を呼ぶだけにします。言語切り替えがタイマーを作り直さない構造にします。
  4. 一時停止中の切り替えを許す:一時停止中にも言語ボタンが押せるようにし、描き直しても一時停止が解除されないようにします。state.paused は言語と無関係に保たれます。
  5. ゲームオーバーやリザルト画面でも動かす:画面の種類は state.screen = "result" のように値で持ち、render() が画面の種類に応じて描きます。この構造なら、どの画面でも言語ボタンが効きます。

プレイ画面の拡大やウィンドウの大きさの変化も、同じ考え方で扱えます。大きさが変わったときは表示サイズだけを調整し、状態には触れません。

手順5:進行が保たれるかを自分で確かめる

作りが整ったら、次の順で確認します。結果は自分のゲームで確かめて、記録してください。

  1. 確認用の状態を作る:スコア、ヒントの残り回数、ラウンド数、アイテムの所持、一時停止など、進行を表す値をできるだけ変えた状態にします。
  2. 画面ごとに切り替える:タイトル、プレイ中、一時停止中、結果画面、設定画面のそれぞれで言語を切り替えます。回数は各画面で、日本語→英語→日本語と最低1往復ずつです。
  3. 前後を並べて見る:切り替え前後で、数値、選択中の項目、入力欄の内容、タイマーの進み方が同じかを確認します。変化があれば、その値が状態に入っていない(または描き直しで消えている)ことを意味します。
  4. 直前のメッセージを見る:メッセージ欄に表示中の文が、新しい言語に切り替わっているかを確認します。切り替わらない場合は、本文を状態に持っている可能性があります。
  5. 訳漏れを探す:一通り遊んで、片方の言語に日本語や英語が混ざっていないかを確認します。キー名がそのまま表示されている箇所も、訳漏れの目印です。
  6. 操作の途中で切り替える:ボタンを押す直前、ドラッグの最中、アニメーションの途中など、不自然なタイミングで切り替えます。状態が壊れる場合は、処理の途中で文言を状態に書き込んでいないかを疑います。

起こりやすい誤りは、次の3つです。

  • 画面を作り直す関数に、状態の初期化が混ざっている。
  • 切り替えボタン自体が、言語ごとに別の関数で処理されている。
  • 結果画面など、めったに開かない画面の文言だけ辞書に入っていない。

言語は保存データに入れるべきか

アソバースにアップロードした作品の中の localStorage と sessionStorage は、一時メモリーとして動く互換処理です。言語設定を永続させる手段としては期待しすぎないでください。進行を残したい場合は、作品ごとにJSONで128KiBまで使えるクラウドセーブがあります。アップロード作品ではSDKが自動で追加され、URL投稿の作品では作者がSDKを読み込むと使えます。

保存データに言語を含めるかは、設計上の判断です。言語は進行ではなく環境の設定なので、進行データとは分けるのが基本の考え方です。含めてしまうと、別の言語で開いた端末で読み込んだときに、遊ぶ人が選んだ言語を上書きしかねません。保存データには、手順1で選んだ「言語に依存しない値」だけを入れておけば、どの言語で開いても同じ進行を再現できます。

初期言語を何にするか(サイトの日本語/英語設定にどう合わせるか)は、アソバース上での受け渡し方法を含めて、現在の案内で確認してください。

次の行動

まず上のHTMLを保存し、ブラウザで開いて、葉を3回拾ってから言語を切り替えてみてください。次に自分のゲームの文字列を辞書へ移し、状態に残っている本文があればキーに置き換えます。最後に手順5の確認を画面ごとに行い、進行が変わらないことを確かめたうえで、作品を投稿画面から公開してください。