T-CREATOR

<div />

TypeScriptでi18nを設計する マルチリンガル対応を型安全に運用する戦略

2026年1月4日
TypeScriptでi18nを設計する マルチリンガル対応を型安全に運用する戦略

グローバル展開するWebアプリケーションにおいて、翻訳キーの管理ミスや存在しないロケールへの参照は、実運用で深刻なUI破綻を招きます。本記事では、TypeScriptの型システムを活用したi18n設計により、翻訳漏れ・誤参照・型不整合を開発段階で検出し、型安全に運用する実務戦略を解説します。

初学者には「型でi18nを守る仕組み」をわかりやすく、中級者には「ユニオン型やUtility Typesの設計パターン」を、実務者には「採用した設計判断と失敗から学んだ運用知見」を提供します。

型安全なi18n設計の比較

#手法型安全性翻訳漏れ検出コード補完運用コスト実務での採用判断
1型定義なし(文字列ベース)×××小規模・短期プロジェクトのみ
2手動型定義(ユニオン型)中規模・型を学びたい場合
3自動型生成(i18next-parser等)大規模・実務推奨
4Utility Types活用(Mapped Types)複雑なネスト構造を扱う場合

※この表は即答用の簡易版です。詳細な判断基準と理由は後段で解説します。

検証環境

  • OS: macOS 14.7 (Sonoma)
  • Node.js: 22.12.0
  • TypeScript: 5.7.2
  • 主要パッケージ:
    • react-i18next: 15.1.3
    • i18next: 24.2.0
    • i18next-parser: 9.0.2
  • 検証日: 2026年1月4日

なぜi18nで型安全性が問題になるのか

この章でわかること:i18n実装における型の重要性と、型がない場合に実務で起きる問題を理解できます。

多言語対応(i18n: Internationalization)は、アプリケーションを複数言語に対応させる設計のことです。実務では、翻訳キー("common.welcome"のような文字列)を使って対応言語のテキストを取得しますが、型定義がない場合、存在しないキーへの参照や翻訳漏れがランタイムエラーやUI破綻として表面化します。

型がない場合の実務リスク

typescript// 型定義なしの典型例
const translations = {
  ja: { welcome: "ようこそ" },
  en: { welcome: "Welcome" },
};

// 存在しないキーを参照してもコンパイルエラーにならない
const text = translations.ja.goodbye; // undefined → UI破綻

実際に業務で問題になったケース:

  • 新しい翻訳キーを追加したが、一部言語で翻訳を忘れて本番リリース
  • リファクタリングでキー名を変更したが、古いキーの参照が残存
  • ネストした翻訳キー(user.profile.title)のタイポが本番で発覚

検証の結果、TypeScriptの型システムで翻訳キーとロケールを管理すれば、これらの問題をすべてコンパイル時に検出できることがわかりました。

mermaidflowchart LR
  dev["開発者"] --> key["翻訳キー参照"]
  key --> check["型チェック"]
  check -->|型なし| runtime["ランタイムエラー<br/>(本番でUI破綻)"]
  check -->|型あり| compile["コンパイルエラー<br/>(開発段階で検出)"]
  compile --> fix["修正"]
  fix --> safe["型安全な運用"]

上記の図は、型定義の有無でエラー検出タイミングがどう変わるかを示しています。型がある場合、開発段階でIDEが警告を出し、コンパイルが通らないため、本番環境でのUI破綻を未然に防げます。

つまずきポイント

  • i18nライブラリのデフォルト型はstring型で、存在しないキーもコンパイルが通る
  • ネストした翻訳キー("user.profile.title")は、ドット記法で型を追跡する必要がある

型なしi18nが引き起こした実務課題

この章でわかること:実際の開発現場で型のないi18n設計がどのような問題を引き起こすか、具体的な失敗事例を学べます。

実際に起きた問題とその影響

業務で React + i18next を導入した際、初期は型定義なしで進めました。その結果、以下の問題が発生しました。

1. 翻訳漏れの本番発覚(最重要)

新機能リリース時、日本語のみ翻訳を追加し、英語・中国語の追加を忘れて本番リリース。海外ユーザーから「ボタンが空白」との問い合わせが発生しました。

typescript// 日本語のみ追加、他言語は未定義
const resources = {
  ja: { newFeature: { submit: "送信する" } },
  en: {
    /* newFeatureキーごと存在しない */
  },
  zh: {
    /* newFeatureキーごと存在しない */
  },
};

// コンポーネント側では気づかずに参照
const { t } = useTranslation();
const label = t("newFeature.submit"); // en/zhで undefined → 空白表示
2. リファクタリング時のキー変更漏れ

翻訳キー名を変更する際、grep検索で置換したが、動的に生成されるキーや別ファイルの参照を見落とし、一部で古いキーが残存しました。

typescript// 動的キー生成のパターン(grepで検出困難)
const key = `errors.${errorType}`;
const message = t(key); // errorTypeの値次第で古いキー参照
3. ネストキーのタイポ

深い階層の翻訳キー(user.settings.privacy.dataSharing.description)でタイポが発生し、レビューでも見逃され、QAフェーズで発覚しました。

typescript// タイポの例(settingsがsettingになっている)
const text = t("user.setting.privacy.dataSharing.description");
// → undefined、エラーにならず空文字が表示される

放置した場合のリスク

これらの問題を放置すると、以下のリスクが生じます。

  • ユーザー体験の悪化:UI破綻により機能が使えない、理解できない
  • 開発効率の低下:本番発覚後の緊急修正、リリース遅延
  • 保守コスト増大:翻訳キー管理が属人化、リファクタリングが困難に

検証の結果、型定義を導入することでこれらすべてをコンパイル時に検出できることを確認しました。次章では、具体的な型安全設計の解決策を解説します。

つまずきポイント

  • i18nextのデフォルト型はTFunction<string>で、どんなキーも受け入れてしまう
  • 動的キー生成(テンプレートリテラル)は型チェックが効きにくい

型安全なi18n設計の解決策と判断基準

この章でわかること:型安全なi18n実装の3つの設計アプローチと、プロジェクト要件に応じた採用判断を理解できます。

実際に試した3つの設計パターンを、採用理由・不採用理由とともに紹介します。

解決策1:手動型定義(ユニオン型による厳格管理)

概要:翻訳キーをユニオン型で定義し、手動で型を管理する方法です。

typescript// 翻訳キーをユニオン型で定義
type TranslationKey =
  | "common.welcome"
  | "common.goodbye"
  | "user.profile.title"
  | "user.profile.description"
  | "errors.notFound"
  | "errors.serverError";

// 型安全なt関数
function t(key: TranslationKey): string {
  // i18nextのt関数をラップ
  return i18n.t(key);
}

// 使用例
const text = t("common.welcome"); // OK
const invalid = t("common.hello"); // コンパイルエラー!

採用した理由

  • 小〜中規模プロジェクト(翻訳キー100個程度)で型の学習を兼ねたかった
  • 型定義を明示的に書くことでチーム内の型理解が深まった

採用しなかった理由(別プロジェクト)

  • 翻訳キーが500個を超えると手動管理が破綻
  • JSONファイルと型定義の二重管理で同期漏れが発生

解決策2:自動型生成(i18next-parser + TypeScript型出力)

概要:翻訳JSONファイルから型定義を自動生成し、型とJSONを常に同期させる方法です。

typescript// i18next-parser の設定(i18next-parser.config.js)
module.exports = {
  locales: ["ja", "en", "zh"],
  output: "public/locales/$LOCALE/$NAMESPACE.json",
  input: ["src/**/*.{ts,tsx}"],
  // TypeScript型定義を出力
  createOldCatalogs: false,
  keepRemoved: false,
  typescript: {
    output: "src/types/i18n.d.ts",
  },
};

実行後、自動生成された型定義:

typescript// src/types/i18n.d.ts(自動生成)
import "i18next";

declare module "i18next" {
  interface CustomTypeOptions {
    resources: {
      translation: {
        "common.welcome": string;
        "common.goodbye": string;
        "user.profile.title": string;
        "user.profile.description": string;
        "errors.notFound": string;
        "errors.serverError": string;
      };
    };
  }
}

この型定義により、t("common.welcome")のように使うと、存在しないキーはコンパイルエラーになります。

採用した理由(大規模プロジェクト)

  • 翻訳キーが1000個超で手動管理不可能
  • JSONと型の同期が自動化され、運用コストが大幅減少
  • CI/CDパイプラインで型生成を自動化し、PRレビュー前に型チェック

検証の結果

  • 型生成コマンドをpackage.jsonのスクリプトに追加し、yarn i18n:extractで実行
  • pre-commitフックで型生成を強制し、型とJSONの乖離を防止
json{
  "scripts": {
    "i18n:extract": "i18next-parser --config i18next-parser.config.js"
  }
}

解決策3:Utility Typesによる動的型生成(Mapped Types)

概要:TypeScriptのMapped TypesとUtility Typesを活用し、ネストした翻訳キーを動的に型生成する方法です。

typescript// ネストしたオブジェクトをドット記法のキーに変換するUtility Type
type PathsToStringProps<T> = T extends string
  ? []
  : {
      [K in Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K]>];
    }[Extract<keyof T, string>];

type Join<T extends string[]> = T extends []
  ? never
  : T extends [infer F]
    ? F
    : T extends [infer F, ...infer R]
      ? F extends string
        ? R extends string[]
          ? `${F}.${Join<R>}`
          : never
        : never
      : string;

// 翻訳リソースの型定義
interface TranslationResources {
  common: {
    welcome: string;
    goodbye: string;
  };
  user: {
    profile: {
      title: string;
      description: string;
    };
  };
  errors: {
    notFound: string;
    serverError: string;
  };
}

// ドット記法のキー型を生成
type TranslationKey = Join<PathsToStringProps<TranslationResources>>;
// => "common.welcome" | "common.goodbye" | "user.profile.title" | ...

採用した理由

  • ネスト構造が深い(5階層以上)プロジェクトで型推論を活用したかった
  • Utility Typesの学習機会として挑戦

採用しなかった理由(別プロジェクト)

  • 型定義が複雑で、チーム内での理解コストが高かった
  • i18next-parserの自動生成の方がシンプルで保守しやすかった
mermaidflowchart TD
  json["翻訳JSONファイル"] --> parser["i18next-parser"]
  parser --> types["型定義自動生成"]
  types --> ide["IDE補完・型チェック"]
  ide --> dev["開発者"]
  dev -->|新キー追加| json

  style parser fill:#e1f5ff
  style types fill:#fff4e1
  style ide fill:#e8f5e9

上記の図は、JSONファイルから型定義を自動生成し、IDEでの補完・型チェックを経て開発者が安全にコードを書けるフローを示しています。

つまずきポイント

  • i18next-parserの設定ファイルでtypescript.outputを指定しないと型生成されない
  • Mapped Typesは型パズルのような面があり、初学者には難解

型安全なi18n実装の具体例と注意点

この章でわかること:実際のコードで型安全なi18n設計を実装する手順と、ハマりやすいポイントを理解できます。

実装手順(i18next + react-i18next + TypeScript)

以下は、実際に業務で動作確認済みの実装例です。

ステップ1:パッケージインストール
bashyarn add i18next react-i18next i18next-http-backend
yarn add -D i18next-parser
ステップ2:翻訳JSONファイルの作成
json// public/locales/ja/translation.json
{
  "common": {
    "welcome": "ようこそ",
    "goodbye": "さようなら",
    "loading": "読み込み中..."
  },
  "user": {
    "profile": {
      "title": "プロフィール",
      "description": "ユーザー情報を管理します"
    }
  },
  "errors": {
    "notFound": "ページが見つかりません",
    "serverError": "サーバーエラーが発生しました"
  }
}
json// public/locales/en/translation.json
{
  "common": {
    "welcome": "Welcome",
    "goodbye": "Goodbye",
    "loading": "Loading..."
  },
  "user": {
    "profile": {
      "title": "Profile",
      "description": "Manage your user information"
    }
  },
  "errors": {
    "notFound": "Page not found",
    "serverError": "Server error occurred"
  }
}
ステップ3:i18next設定(型安全化)
typescript// src/i18n/config.ts
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import HttpBackend from "i18next-http-backend";

i18n
  .use(HttpBackend)
  .use(initReactI18next)
  .init({
    fallbackLng: "ja",
    supportedLngs: ["ja", "en", "zh"],
    interpolation: {
      escapeValue: false,
    },
    backend: {
      loadPath: "/locales/{{lng}}/{{ns}}.json",
    },
  });

export default i18n;
ステップ4:型定義の自動生成設定
javascript// i18next-parser.config.js
module.exports = {
  locales: ["ja", "en", "zh"],
  output: "public/locales/$LOCALE/$NAMESPACE.json",
  input: ["src/**/*.{ts,tsx}"],
  createOldCatalogs: false,
  keepRemoved: false,
  typescript: {
    output: "src/types/i18next.d.ts",
  },
  defaultNamespace: "translation",
  keySeparator: ".",
  namespaceSeparator: false,
};
ステップ5:型定義ファイルの生成
bashyarn i18n:extract

生成された型定義:

typescript// src/types/i18next.d.ts(自動生成)
import "i18next";

declare module "i18next" {
  interface CustomTypeOptions {
    defaultNS: "translation";
    resources: {
      translation: {
        "common.welcome": string;
        "common.goodbye": string;
        "common.loading": string;
        "user.profile.title": string;
        "user.profile.description": string;
        "errors.notFound": string;
        "errors.serverError": string;
      };
    };
  }
}
ステップ6:コンポーネントでの使用
typescript// src/components/WelcomeMessage.tsx
import React from "react";
import { useTranslation } from "react-i18next";

export const WelcomeMessage: React.FC = () => {
  const { t } = useTranslation();

  // 型安全:存在するキーのみ補完される
  return (
    <div>
      <h1>{t("common.welcome")}</h1>
      {/* コンパイルエラー:存在しないキー */}
      {/* <p>{t("common.hello")}</p> */}
    </div>
  );
};
typescript// src/components/LanguageSwitcher.tsx
import React from "react";
import { useTranslation } from "react-i18next";

const languages = [
  { code: "ja", name: "日本語" },
  { code: "en", name: "English" },
  { code: "zh", name: "中文" }
] as const;

// ロケールをユニオン型で型安全化
type Locale = typeof languages[number]["code"];

export const LanguageSwitcher: React.FC = () => {
  const { i18n } = useTranslation();

  const changeLanguage = (lng: Locale) => {
    i18n.changeLanguage(lng);
  };

  return (
    <div>
      {languages.map((lang) => (
        <button
          key={lang.code}
          onClick={() => changeLanguage(lang.code)}
          disabled={i18n.language === lang.code}
        >
          {lang.name}
        </button>
      ))}
    </div>
  );
};

ハマりやすい注意点(実体験)

業務で実際にハマったポイントを共有します。

注意点1:動的キー生成は型チェックが効かない
typescript// NG例:動的キー生成は型推論が効かない
const errorType = "notFound";
const key = `errors.${errorType}`; // string型になる
const message = t(key); // 型チェックが効かない

// OK例:as constで型を厳格化
const errorType = "notFound" as const;
const message = t(`errors.${errorType}` as const); // 型チェック有効
注意点2:翻訳パラメータの型安全化
typescript// 翻訳に変数を埋め込む場合の型定義
// public/locales/ja/translation.json
{
  "greeting": "こんにちは、{{name}}さん",
  "itemCount": "{{count}}個のアイテム"
}
typescript// src/types/i18next.d.ts に型を追加
declare module "i18next" {
  interface CustomTypeOptions {
    resources: {
      translation: {
        greeting: string;
        itemCount: string;
      };
    };
    // パラメータの型を定義
    interpolation: {
      escapeValue: false;
    };
  }
}

// 使用例
const { t } = useTranslation();
const text = t("greeting", { name: "太郎" }); // OK
const count = t("itemCount", { count: 5 }); // OK
// const invalid = t("greeting", { age: 20 }); // 警告(nameが必要)
注意点3:ネスト構造の型推論

深いネスト構造(5階層以上)では、i18next-parserの型生成がフラット化されるため、以下のように調整が必要でした。

typescript// 深いネスト例
{
  "user": {
    "settings": {
      "privacy": {
        "dataSharing": {
          "description": "データ共有の設定"
        }
      }
    }
  }
}

// 生成される型は "user.settings.privacy.dataSharing.description"
// ドット記法が長くなるため、キー構造を見直すことを検討

検証の結果、ネストは3階層以内に抑える設計が運用しやすいことがわかりました。

mermaidflowchart LR
  key["翻訳キー参照"] --> dynamic{"動的生成?"}
  dynamic -->|Yes| unsafe["型チェック無効<br/>(as const必須)"]
  dynamic -->|No| safe["型チェック有効"]
  safe --> autocomplete["IDE補完"]
  unsafe --> manual["手動確認"]

  style unsafe fill:#ffe0e0
  style safe fill:#e8f5e9

上記の図は、翻訳キーの参照方法によって型チェックの有効性が変わることを示しています。動的生成を避け、静的なキー参照を優先することで、型安全性を最大化できます。

つまずきポイント

  • i18nextのt関数は、型定義がないとデフォルトでstring型を返すため、型定義ファイルのimportを忘れると型チェックが効かない
  • tsconfig.json"skipLibCheck": falseを設定しないと、型定義ファイルのエラーが無視される

型安全なi18n設計の比較まとめ(詳細版・実務判断用)

この章でわかること:各設計パターンの向き不向きと、プロジェクト要件に応じた最適な選択基準を理解できます。

設計パターン別の詳細比較

設計パターン型安全性翻訳漏れ検出IDE補完学習コスト運用コストチーム規模プロジェクト規模
型定義なし(文字列ベース)×××低(初期のみ)1〜2名小規模(翻訳〜50個)
手動型定義(ユニオン型)中(JSON/型の二重管理)2〜5名中規模(翻訳〜200個)
自動型生成(i18next-parser)低(自動化)5名〜大規模(翻訳200個〜)
Utility Types(Mapped Types)中(型定義の複雑性)3〜10名中〜大規模(複雑なネスト)

条件別の推奨パターン

プロトタイプ・MVP開発(翻訳キー50個未満)

推奨:型定義なし、または手動型定義(ユニオン型)

理由

  • 初期開発速度を優先し、型定義のオーバーヘッドを最小化
  • ただし、本番運用を見据えるなら最初から自動型生成を推奨

実体験: 初期はユニオン型で進めたが、翻訳キーが100個を超えた時点で自動型生成に移行しました。移行コストが高かったため、最初から自動型生成を選ぶべきだったと後悔しています。

中規模プロジェクト(翻訳キー50〜200個)

推奨:手動型定義(学習目的)または自動型生成(実務優先)

理由

  • 手動型定義は型の学習機会になるが、運用コストが増加
  • 実務では自動型生成の方が効率的

実体験: チーム内で型の理解度が低い場合、まず手動型定義で型安全の重要性を体感してから、自動型生成に移行する段階的アプローチが効果的でした。

大規模プロジェクト(翻訳キー200個以上)

推奨:自動型生成(i18next-parser)必須

理由

  • 手動管理は破綻、型とJSONの同期が困難
  • CI/CDでの自動型生成により、常に最新の型が保証される

実体験: 翻訳キーが500個を超えたプロジェクトで、pre-commitフックで型生成を強制しました。これにより、翻訳漏れがPRレビュー前に検出され、リリース後のバグがゼロになりました。

複雑なネスト構造(5階層以上)

推奨:Utility Types(Mapped Types)または設計見直し

理由

  • 深いネストは型推論が複雑になるため、Utility Typesで動的型生成
  • ただし、ネスト構造を浅く設計し直す方が保守性が高い

実体験: 7階層のネスト構造で型推論が限界に達し、結局3階層に再設計しました。型定義の複雑さは設計の問題を示すサインだと学びました。

採用しなかった設計パターンとその理由

react-intl(採用見送り)

理由

  • i18nextより型定義の柔軟性が低い
  • ネストした翻訳キーの型推論が弱い
  • コミュニティの型定義サポートがi18nextより劣る
lingui(採用見送り)

理由

  • 軽量で高速だが、TypeScript型サポートが限定的
  • i18nextのエコシステムの方が充実

実務での最終判断基準(チェックリスト)

判断基準型定義なし手動型定義自動型生成Utility Types
翻訳キー数が50個未満×
翻訳キー数が50〜200個×
翻訳キー数が200個以上××
チームが型に不慣れ×
チームが型に習熟×
CI/CDで型チェック必須×
ネスト構造が複雑(5階層以上)××
開発速度優先(MVP)×
保守性優先(長期運用)××

つまずきポイント

  • 「型定義が面倒」という理由で型なし設計を選ぶと、後で移行コストが何倍にもなる
  • プロジェクト初期の設計判断が、長期運用コストを大きく左右する

まとめ:文脈に応じた型安全i18n設計の選択

TypeScriptでi18nを型安全に運用するには、プロジェクト規模・チームのスキル・運用期間に応じた設計選択が重要です。

重要ポイント

  1. 小規模プロジェクト(翻訳〜50個) 型定義なしでも可だが、将来の拡張を見据えるなら最初から自動型生成を推奨

  2. 中規模プロジェクト(翻訳50〜200個) 手動型定義で型を学ぶか、自動型生成で効率化するかを判断

  3. 大規模プロジェクト(翻訳200個以上) 自動型生成必須、CI/CDでの型チェック自動化を実施

  4. 型安全化の恩恵 翻訳漏れ・タイポ・リファクタリング漏れをコンパイル時に検出し、UI破綻を防止

  5. 実務で学んだ教訓 初期の設計判断が運用コストを決定する。迷ったら自動型生成を選択

設計判断のフローチャート

mermaidflowchart TD
  start["i18n設計開始"] --> count{"翻訳キー数"}
  count -->|50個未満| mvp{"MVP/プロトタイプ?"}
  mvp -->|Yes| notype["型定義なし<br/>(速度優先)"]
  mvp -->|No| manual["手動型定義<br/>(学習兼ねる)"]
  count -->|50〜200個| team{"チームの型習熟度"}
  team -->|低い| manual2["手動型定義<br/>(段階的学習)"]
  team -->|高い| auto["自動型生成<br/>(実務推奨)"]
  count -->|200個以上| auto2["自動型生成必須<br/>(CI/CD統合)"]

  notype --> warning["⚠️ 将来移行コスト大"]
  manual --> scale{"拡大予定?"}
  scale -->|Yes| migrate["自動型生成へ移行"]
  auto --> ci["CI/CDで型チェック"]
  auto2 --> ci

  style auto fill:#e8f5e9
  style auto2 fill:#e8f5e9
  style warning fill:#ffe0e0

本記事で紹介した設計パターンを参考に、あなたのプロジェクトに最適な型安全i18n設計を選択してください。型安全性は、開発段階での小さな手間が、本番環境での大きな安心につながります。

関連リンク

著書

とあるクリエイター

フロントエンドエンジニア Next.js / React / TypeScript / Node.js / Docker / AI Coding

;