Article

🌐 Reactの翻訳辞書をTypeScriptで型安全にする:キー漏れ・誤字を防ぐ

typescript, react, nextjs, i18n

日本語・英語に対応する画面を実装していると、翻訳辞書のキーがずれたままレビューを通ってしまうことがあります。例えば日本語にはsaveがあるのに英語側にはsaveがないと、画面に空の表示やundefinedが出る原因になります。

翻訳ライブラリの導入前でも、TypeScriptのsatisfiesを使えば辞書キーの不足・誤字をコンパイル時に見つけられます。本記事は一般的なasとsatisfiesの比較ではなく、実際の翻訳辞書を安全に増やす設計に絞って紹介します。確認日:2026年10月14日。

何も工夫しない辞書の問題

const ja = { title: "商品一覧", save: "保存", cancel: "キャンセル" };
const en = { title: "Products", save: "Save" };
// en.cancel が未定義でも、この時点では辞書同士の比較はされない

キーを文字列で受け取るt(locale, key)のような関数を安易に作ると、存在しないキーを指定してもTypeScriptが指摘できない設計になりがちです。

型とは、コード上で取り得る値・構造に付ける制約です。翻訳辞書のキーを型にすれば、未登録の文字列をコンパイル時に排除できます。

日本語辞書を正本にしてキー型を作る

まず日本語の辞書からキー一覧を導出します。

// messages.ts
export const ja = {
  title: "商品一覧",
  save: "保存",
  cancel: "キャンセル",
} as const;

export type MessageKey = keyof typeof ja;
export type Locale = "ja" | "en";

as constは定義された値をできるだけ具体的な型として扱い、プロパティをreadonly(型チェック上、書き換え不可)にします。keyof typeof jaは"title" | "save" | "cancel"というユニオン型(取り得る候補を|で列挙した型)になります。

辞書を更新しても、キー型を手で二重管理する必要がありません。

satisfiesで英語辞書の不足を検出する

// messages.ts
export const en = {
  title: "Products",
  save: "Save",
  cancel: "Cancel",
} as const satisfies Record<MessageKey, string>;

Record<MessageKey, string>は「すべてのメッセージキーに文字列の値を持つ」という型です。satisfiesは与えたオブジェクトがその型に適合するか検査します。

例えばcancelを消すと、TypeScriptの型チェックで不足が指摘されます。canselのような誤字を追加しても、直接書いたオブジェクトリテラルなら余分なキーとして検出されます。

satisfiesは値の型推論を活かすので、単なる強制的な型アサーション(as Record<...>)に比べて誤りを握りつぶしにくくなります。TypeScript 4.9の公式リリースノートでも、プロパティ名の誤字検出が代表例として挙げられています。

言語とキーを両方型で制限する

// messages.ts(上の定義に追記)
export const messages = {
  ja,
  en,
} as const satisfies Record<Locale, Record<MessageKey, string>>;

export function t(locale: Locale, key: MessageKey): string {
  return messages[locale][key];
}

呼び出し側は次のようになります。

t("ja", "save");     // "保存"
t("en", "save");     // "Save"
// t("en", "unknown"); // 型エラー: 定義されていないキー
// t("fr", "save");     // 型エラー: 対応していない言語

Reactコンポーネントの例です。

import { t, type Locale } from "./messages";

export function SaveButton({ locale }: { locale: Locale }) {
  return <button type="button">{t(locale, "save")}</button>;
}

通常のReactだけでなくNext.jsの画面でも使えます。ただし、ここで扱うのは辞書の型安全性です。Next.js側の多言語ルーティング設定そのものは別途行います。

URLやブラウザから来る言語は検証する

URLのパラメータやリクエストヘッダーに入る言語名は、型の保証がない外部入力です。そのままas Localeで決めつけると、実行時にundefinedが出る可能性があります。

import type { Locale } from "./messages";

export function isLocale(value: string): value is Locale {
  return value === "ja" || value === "en";
}

export function resolveLocale(raw: string): Locale {
  const language = raw.toLowerCase().split("-")[0];
  return isLocale(language) ? language : "ja";
}

resolveLocale("en-US"); // "en"
resolveLocale("fr-FR"); // "ja"(例として既定言語へフォールバック)

value is Localeは型ガードといい、条件を通った値の型を絞り込む仕組みです。この例は最小限の実装なので、実運用で地域別ロケールやHTTPのAccept-Languageヘッダーを扱う場合は仕様に沿った解析が必要です。

導入時にハマりやすい点

  1. 翻訳キー自体を途中で変更する:日本語のキー名を変えると全言語・画面で型エラーが出ます。これは不具合を早期発見するための意図した挙動です。
  2. 翻訳済みかどうかは確認できない:英語側に"TODO"が入っていても型は文字列なので通ります。文言の品質はレビューやテストで確認します。
  3. 外部JSONは別問題:JSONやAPIから受け取った辞書はコンパイル時に構造が確定しません。実行時バリデーションが必要です。
  4. 辞書が大規模になる:巨大な1ファイルにまとめず、画面や機能単位で辞書を分割し、それぞれでキー型を導出する方法もあります。

型チェックの実行

TypeScriptを導入済みのプロジェクトなら、次のコマンドで型エラーを確認できます。

npx tsc --noEmit

npxはインストール済みのパッケージなどの実行を補助するnpmのコマンド、tscはTypeScriptコンパイラ、--noEmitはJavaScriptファイルを出力せずに検査する指定です。CIにも組み込むと、翻訳キーの漏れをプルリクエスト時に検出できます。

まとめ

  • 日本語辞書のkeyof typeofからキー型を導出する。
  • 別言語はsatisfies Record<MessageKey, string>で不足・誤字を検出する。
  • 呼び出し関数の言語・キーをユニオン型で制限する。
  • URLやAPIから来る値は別途実行時に検証する。

公式参考資料(2026年10月14日確認)