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

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

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

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

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

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

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

動的にキーを組み立てる例
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()呼び出しがない場合があります。

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

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

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


関連記事