「言語切り替え」を作り込む:文言の分離と切り替え時の保持
遊びをつくる執筆にClaudeを使用しています。

ステージ3の途中でスコアが1,200点、ヒントは残り1回という状況で、右上の「English」ボタンを押す場面を考えてみます。切り替わった瞬間に、タイトル画面へ戻ったりスコアが0に戻ったりしたら、遊んでいる人は二度と言語ボタンに触れなくなります。言語切り替えは機能として小さく見えますが、「押しても何も失わない」ことまで作って初めて完成します。 この記事で確かめるのは、表示言語を切り替えても進行を失わない設計がどう組めるかです。鍵は、ゲームの状態(進行)と画面に出す文言を別の層に分けることにあります。分けておけば、言語の切り替えは「言語の値を変えて、画面を描き直す」だけで済みます。 最後まで進むと、単独で動く40行ほどのHTMLを土台に、文言の辞書、状態の持ち方、切り替え時の確認手順が手元に揃います。ゲームの規模が大きくなっても、同じ考え方で広げられます。
この記事の目次
切り替えで失われるものを先に洗い出す
最初に、言語の切り替えで何が壊れうるかを書き出します。実装より前にこの一覧を作っておくと、あとで確認項目としてそのまま使えます。
- ページの再読み込み:言語ごとに別のURLへ移動する作りだと、メモリ上の進行がすべて消えます。
- 画面の作り直し:画面全体を破棄して作り直すと、入力欄の文字、選択中の項目、スクロール位置、フォーカスが消えます。
- 文言に埋め込まれた状態:「拾いました」という表示済みの文字列を状態として保存していると、切り替え後も日本語のまま残ります。
- 文言で判定する処理:if (answer === "はい") のように、表示文字列で分岐していると、英語にした途端に判定が壊れます。
- 動いているものの停止:タイマーやアニメーションが、描き直しの際に止まったり二重に動いたりします。
アソバースの公式作品13本は、設計として「言語変更とプレイ画面の拡大で進行を保つ」ことを共通の方針にしています。実際にどの作品がどの場面でどう保たれるかは、作品を開いてプレイ中に切り替えて確かめてください。この記事の手順は、その考え方を自作に取り入れるためのものです。
手順1:状態に表示用の文字列を持たせない
最初にやるのは、ゲームの状態として何を持つかの見直しです。原則は、状態には「言語に依存しない値」だけを入れることです。
- スコア(数値)、持たせないもの:「集めた葉:3枚」という完成済みの文
- 直前のメッセージのキー("ok")、持たせないもの:直前のメッセージの本文(「拾いました」)
- 選択中の項目のID("recipe_3")、持たせないもの:選択中の項目の表示名
- 現在の言語コード("ja" / "en")、持たせないもの:言語ごとの並び順に依存した添字
判断に迷ったら、「言語を切り替えたとき、この値も変えたいか」と自問します。変えたいなら表示用なので状態には置かず、描画の時点で作ります。変えたくないなら状態に置きます。
分岐も同じ考え方です。判定は常にIDや数値で行い、表示文字列は比較に使いません。選択肢ボタンを作るときは、ボタンに data-id="yes" のようなIDを持たせ、表示名だけを辞書から引きます。
手順2:文言の辞書と、状態から描く関数を作る
次に、文言を一か所の辞書にまとめ、画面を render() という一つの関数で描く形にします。以下は単独で動く最小構成です。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つあります。
- state.msgKey に本文ではなくキーを入れています。葉を拾ったあとに言語を切り替えると、メッセージも新しい言語で表示されます。ここが「保持」の中心です。
- t() は、現在の言語に訳がなければ日本語、それもなければキー名そのものを返します。訳し忘れても画面が空にならず、どのキーが足りないか見つけやすくなります。
- 言語ボタンの操作は state.lang の書き換えと render() の呼び出しだけです。再読み込みも画面の破棄もないため、state.n は変わりません。
- document.documentElement.lang も更新しています。読み上げ機能や、ブラウザの文字の選び方に影響するためです。
自分のゲームに移すときは、まず今ある文字列をすべて辞書のキーに置き換えます。検索で " や ' で囲まれた日本語を拾い出していくと漏れが見つかります。
手順3:数字・語順・長さに耐える文言にする
辞書に分けただけでは、まだ不十分な場面があります。言語によって、文の組み立てと長さが違うためです。
- 文を部品で組み立てない:"集めた葉:" + n + "枚" のように継ぎ足すと、語順の違う言語で破綻します。上のコードのように、{n} を含む文全体を一つの訳として持ちます。
- 単数と複数を分ける:英語では1枚と2枚以上で語形が変わる場合があります。数によって文を変えるものは leaf_one と leaf_other のように別キーにし、数で選ぶ関数を一つ挟みます。
- 文字数の差を見込む:一般に、日本語から英語にするとボタンの文言が長くなりがちです。ボタンの幅を固定せず、折り返しや余白に余裕を持たせます。
- 画像に埋め込んだ文字を減らす:文字入りの画像は言語ごとに差し替えが必要です。タイトルや説明は、画面上のテキストとして描くほうが管理しやすくなります。
なお、アソバースでは作者が入力した作品名や紹介文は、サイトの表示言語を切り替えても書き換えられません。ゲーム画面の中の文言は作者の側で用意するものなので、辞書を持つ設計は作者自身の仕事になります。
手順4:動いているものを止めずに描き直す
タイマー、アニメーション、入力中の欄がある画面では、render() の作りに一工夫必要です。
- 文字だけを更新する:innerHTML で画面を丸ごと作り直さず、既存の要素の textContent を書き換えます。要素が残るので、フォーカスや入力中の文字も保たれます。
- 動的に増える要素は、IDで差分更新する:一覧やボタン群を描くときは、既存の要素を状態のIDで探して文字だけを変えます。数が変わるときだけ追加・削除します。
- タイマーは状態の外で動かさない:残り時間は state.timeLeft に持ち、タイマー処理は数値を減らして render() を呼ぶだけにします。言語切り替えがタイマーを作り直さない構造にします。
- 一時停止中の切り替えを許す:一時停止中にも言語ボタンが押せるようにし、描き直しても一時停止が解除されないようにします。state.paused は言語と無関係に保たれます。
- ゲームオーバーやリザルト画面でも動かす:画面の種類は state.screen = "result" のように値で持ち、render() が画面の種類に応じて描きます。この構造なら、どの画面でも言語ボタンが効きます。
プレイ画面の拡大やウィンドウの大きさの変化も、同じ考え方で扱えます。大きさが変わったときは表示サイズだけを調整し、状態には触れません。
手順5:進行が保たれるかを自分で確かめる
作りが整ったら、次の順で確認します。結果は自分のゲームで確かめて、記録してください。
- 確認用の状態を作る:スコア、ヒントの残り回数、ラウンド数、アイテムの所持、一時停止など、進行を表す値をできるだけ変えた状態にします。
- 画面ごとに切り替える:タイトル、プレイ中、一時停止中、結果画面、設定画面のそれぞれで言語を切り替えます。回数は各画面で、日本語→英語→日本語と最低1往復ずつです。
- 前後を並べて見る:切り替え前後で、数値、選択中の項目、入力欄の内容、タイマーの進み方が同じかを確認します。変化があれば、その値が状態に入っていない(または描き直しで消えている)ことを意味します。
- 直前のメッセージを見る:メッセージ欄に表示中の文が、新しい言語に切り替わっているかを確認します。切り替わらない場合は、本文を状態に持っている可能性があります。
- 訳漏れを探す:一通り遊んで、片方の言語に日本語や英語が混ざっていないかを確認します。キー名がそのまま表示されている箇所も、訳漏れの目印です。
- 操作の途中で切り替える:ボタンを押す直前、ドラッグの最中、アニメーションの途中など、不自然なタイミングで切り替えます。状態が壊れる場合は、処理の途中で文言を状態に書き込んでいないかを疑います。
起こりやすい誤りは、次の3つです。
- 画面を作り直す関数に、状態の初期化が混ざっている。
- 切り替えボタン自体が、言語ごとに別の関数で処理されている。
- 結果画面など、めったに開かない画面の文言だけ辞書に入っていない。
言語は保存データに入れるべきか
アソバースにアップロードした作品の中の localStorage と sessionStorage は、一時メモリーとして動く互換処理です。言語設定を永続させる手段としては期待しすぎないでください。進行を残したい場合は、作品ごとにJSONで128KiBまで使えるクラウドセーブがあります。アップロード作品ではSDKが自動で追加され、URL投稿の作品では作者がSDKを読み込むと使えます。
保存データに言語を含めるかは、設計上の判断です。言語は進行ではなく環境の設定なので、進行データとは分けるのが基本の考え方です。含めてしまうと、別の言語で開いた端末で読み込んだときに、遊ぶ人が選んだ言語を上書きしかねません。保存データには、手順1で選んだ「言語に依存しない値」だけを入れておけば、どの言語で開いても同じ進行を再現できます。
初期言語を何にするか(サイトの日本語/英語設定にどう合わせるか)は、アソバース上での受け渡し方法を含めて、現在の案内で確認してください。
次の行動
まず上のHTMLを保存し、ブラウザで開いて、葉を3回拾ってから言語を切り替えてみてください。次に自分のゲームの文字列を辞書へ移し、状態に残っている本文があればキーに置き換えます。最後に手順5の確認を画面ごとに行い、進行が変わらないことを確かめたうえで、作品を投稿画面から公開してください。

