zukucode
主にWEB関連の情報を技術メモとして発信しています。

TypeScript i18nextの動的な翻訳キーを抽出対象に含める

i18nextでステータスに応じた文言を表示する場合、翻訳キーを動的に切り替えることがあります。

ただし、実行時に正しく表示できることと、翻訳キーの抽出ツールが使用箇所を検出できることは別です。

今回は、翻訳キーの対応表から抽出用ファイルを生成する方法を紹介します。例では、i18nextとTypeScriptを使用する環境に、対応表と生成処理を追加します。

抽出設定で広いパターンを残す方法もありますが、実際には使われなくなったキーまで保持しやすくなります。反対に、抽出のためだけにt()の呼び出しを手書きすると、実行時の対応表と二重管理になります。そこで、有限な候補を持つ対応表を正とし、抽出ツールが読めるコードをそこから生成します。

文字列を組み立てる場合の問題

以下のようにキーを組み立てると、コードは短くなります。

動的にキーを組み立てる例
t(`status.${status}`);

しかし、この式だけではstatusにどの値が入るのか分かりません。

抽出ツールが候補を把握できないと、使用中のキーを未使用と判定する可能性があります。型チェックが通る場合でも、抽出結果まで正しいとは限りません。

候補が決まっている場合は、翻訳キーを対応表に列挙します。

翻訳リソースの型を定義する

まず、例で使用する翻訳リソースを定義します。

src/resources.ts
export const resources = {
  ja: {
    common: {
      status: {
        pending: '処理待ち',
        completed: '完了',
        canceled: 'キャンセル',
      },
    },
  },
  en: {
    common: {
      status: {
        pending: 'Pending',
        completed: 'Completed',
        canceled: 'Canceled',
      },
    },
  },
} as const;

このリソースをi18next.init({ resources, lng: 'ja', defaultNS: 'common' })などで登録して使用します。Reactから使用する場合は、通常どおりreact-i18nextとの連携も設定します。

さらに、CustomTypeOptionsへリソースの型を登録します。

src/i18next.d.ts
import 'i18next';
import type { resources } from './resources';

declare module 'i18next' {
  interface CustomTypeOptions {
    defaultNS: 'common';
    resources: typeof resources['ja'];
  }
}

この宣言ファイルはTypeScriptのコンパイル対象に含めます。リソースの型から翻訳キーを検査する仕組みは、i18nextのTypeScriptドキュメントでも説明されています。

実行時に使う対応表を定義する

ステータスと翻訳キーの対応表を作成します。

src/statusKeys.ts
import type { ParseKeys } from 'i18next';

export type Status = 'pending' | 'completed' | 'canceled';

export const statusKeys = {
  pending: 'status.pending',
  completed: 'status.completed',
  canceled: 'status.canceled',
} as const satisfies Record<Status, ParseKeys<'common'>>;

Recordでステータスに対応する項目がそろっているか確認し、ParseKeysで翻訳キーを検査します。

例えば、Statusに新しい状態を追加したのに対応表を更新しなかった場合や、翻訳キーをstatus.compeletedと書き間違えた場合に、型エラーとして検出できます。

表示時は、この対応表からキーを選びます。

StatusLabel.tsx
import { useTranslation } from 'react-i18next';
import { statusKeys, type Status } from './statusKeys';

export const StatusLabel = ({ status }: { status: Status }) => {
  const { t } = useTranslation('common');
  return <span>{t(statusKeys[status])}</span>;
};

APIから任意の文字列を受け取る場合は、対応している状態か確認してから渡します。型アサーションだけでは、実行時の値を検証できません。

対応表から抽出用ファイルを生成する

t(statusKeys[status])という呼び出しを、抽出ツールが対応表まで追跡するとは限りません。

そこで、対応表の値から固定キーのt()呼び出しを生成します。以下では、Node.js上で生成スクリプトを実行します。

generate-i18n-keys.ts
import { mkdirSync, writeFileSync } from 'node:fs';
import { statusKeys } from './src/statusKeys';

const keys = [...new Set(Object.values(statusKeys))].sort();
const calls = keys.map((key) => `t(${JSON.stringify(`common:${key}`)});`);

mkdirSync('src/generated', { recursive: true });
writeFileSync(
  'src/generated/i18n-keys.ts',
  [
    '// 自動生成ファイル。直接編集しないでください。',
    "import { t } from 'i18next';",
    '',
    ...calls,
    '',
  ].join('\n'),
  'utf8',
);

生成スクリプトをTypeScriptのまま実行するため、ここではtsxを使用します。Node.jsの型定義と一緒に開発用依存関係へ追加します。

npm install --save-dev tsx @types/node
npx tsx generate-i18n-keys.ts

コマンドはgenerate-i18n-keys.tsを置いたディレクトリで実行します。

出力されるファイルは以下のようになります。

src/generated/i18n-keys.ts
// 自動生成ファイル。直接編集しないでください。
import { t } from 'i18next';

t("common:status.canceled");
t("common:status.completed");
t("common:status.pending");

抽出ツールから見ると、名前空間付きの固定キーを渡す通常のt()呼び出しになります。

このファイルは抽出用なので、画面からimportして実行する必要はありません。使用する抽出ツールの入力対象へ、生成先のsrc/generated/i18n-keys.tsを含めます。

型チェックと静的抽出を分けて確認する

確認対象確認する内容
対応表の型チェックステータスと翻訳キーが正しいか
生成ファイル対応表のキーが固定のt()呼び出しになっているか
抽出結果生成ファイルにあるキーが抽出対象へ含まれているか

tsxによるスクリプトの実行だけでは、TypeScriptの型チェックは行われません。型チェックはtsc --noEmitなど、アプリケーション側のチェック手順で別途実行します。

また、対応表を更新したあとに生成し忘れると、抽出ツールへ渡す内容が古いままになります。

翻訳の抽出前に生成処理を実行するよう、npm scriptsやCIの手順へ組み込みます。

対応表が増えた場合

この例は1つの対応表を明示的にimportするため、生成処理を短く記述できます。

対応表が増えた場合も、各対応表と名前空間を生成スクリプトに登録し、同じ形式へ変換できます。

ただし、対応表をimportすると、そのモジュールのトップレベルの処理も実行されます。対応表のファイルには定数と型だけを置き、画面の初期化や通信処理を含めないようにします。

大量の対応表を自動で探したい場合は、専用の関数で囲んだ定義をTypeScriptの構文木から収集する方法もあります。その場合は、文字列リテラルや配列など、収集できる書き方を決める必要があります。

翻訳キーを削除するときの注意点

サーバー側で選ばれるキーや通知用のキーは、画面内に固定のt()呼び出しがない場合があります。

未使用候補が出たら、別の生成処理や外部データから参照されていないか確認します。

今回の方法は、候補が有限の動的キーを対象にしています。

実行時の表示と抽出用ファイルを同じ対応表から作ることで、抽出のためだけにキーを手書きで二重管理する必要がなくなります。


関連記事

  • React useStateで入力フォームを作成する

    Reactでテキストボックスに入力した値を使用するには、useStateで値を保持します。今回は名前とメールアドレスを入力し、送信ボタンを押すと入力内容を表示するフォームを作成します。この例では入力値...


  • React useStateで一覧の追加と削除を行う

    Reactで一覧を表示するときは、配列をuseStateへ保存できます。今回は簡単な買い物リストを作り、項目の追加と削除を行います。配列のpushやspliceで既存のstateを直接変更するのではな...


  • React useRefで入力欄にフォーカスを当てる

    入力フォームを開いた直後や、入力エラーが出たときに、特定の入力欄へフォーカスしたいことがあります。DOMの要素自体を参照するにはuseRefを使います。currentは初回描画中などにnullになり得...


  • React useMemoで重い計算結果を再利用する

    一覧の絞り込みや並べ替えを描画のたびに行うと、データ量や計算内容によっては画面操作が遅くなります。useMemoは、依存する値が変わらない間、以前の計算結果を再利用します。productsまたはque...


  • React useEffectで無限ループが発生するときに確認すること

    ReactのuseEffectを利用したときに無限ループが発生してしまうことがあります。特に注意したいのが、ESLintのreact-hooks/exhaustive-depsで表示された警告をUpd...


  • React useEffectの後片付けでイベント登録を解除する

    画面の幅が変わったときに表示を更新したい場合、ブラウザのresizeイベントを登録できます。登録したままにすると、コンポーネントが不要になったあとも処理が残るため、useEffectの後片付けを用意し...


  • React 関数コンポーネントにプロパティを設定する

    React+TypeScriptで作成した関数コンポーネントにプロパティを設定する方法を紹介します。以下は送信ボタンのコンポーネントを作成しています。isSubmittingというプロパティを用意して...


  • Reactで兄弟コンポーネントの状態を共有する

    検索欄に入力した文字列を、別の一覧コンポーネントでも使いたい場合があります。兄弟同士で別々にstateを持つと値がずれるため、共通の親にstateを置きます。入力欄は値を受け取り、変更を親へ通知します...


  • React useStateでselectの選択値を取得する

    Reactでプルダウンの選択値を扱うには、selectのvalueとonChangeを使用します。DOMから取得するvalueは、数値の選択肢を表示していても文字列です。選択値をそのまま識別子として扱...


  • React react-router-domの基本的な使い方

    React+TypeScriptのプロジェクトにreact-router-domを導入してSPAのアプリケーションを作成する方法を紹介します。まずはreact-router-domのパッケージをインス...