🐉 がおー console.log("Hello,World!!")だぜ!
トグルホールディングス株式会社のプロダクトエンジニア ドラゴン🐉です。
トグルホールディングスエンジニアアドベントカレンダーの7日目の記事です!
■ はじめに
i18nの定義と目的
現代のウェブ開発において、グローバルなユーザーベースへの対応は不可欠です。Internationalization(国際化)、略してi18nは、アプリケーションを多言語対応させるための設計・実装プロセスを指します。これにより、異なる言語や文化に適応できるアプリケーションを効率的に構築できます。
ローカリゼーションとの違い
時折混同されてしまいがちなのですが、
Localization(ローカリゼーション、略してl10n) は、i18nと密接に関連していますが、役割が異なります。
i18n(Internationalization: 国際化)
- 目的: アプリケーションが多言語対応できるよう、設計・構築するプロセス。
-
特徴:
- Unicodeの使用により、さまざまな言語の文字を正確に表示。
- テキストをソースコードから分離し、翻訳が容易になる仕組みを整備。
- UIを柔軟に設計して、異なる言語でのテキスト長の違いに対応。
- 日付、時間、通貨、数値フォーマットなど、文化的な違いに対応する基盤を構築。
l10n(Localization: 地域化)
- 目的: i18nの基盤を活用し、特定の地域や言語に最適化するプロセス。
-
特徴:
- テキストの翻訳(例: 英語から日本語への翻訳)。
- 地域特有の文化や規制への適応(例: 規約やコンテンツの調整)。
- 日付、時間、通貨、単位などのローカルフォーマットへの変換。
i18nとl10nの関係
- i18nは、l10nを効率的に行えるようにするための基盤作りです。
- l10nは、i18nで構築した基盤を活用して、具体的な言語や地域に適応させます。
たとえば、eコマースアプリケーションでは、i18nで「翻訳可能な構造」を準備し、l10nで「日本向けに価格を円で表示」といった具体的な適応を行います。
react-i18nextとは
react-i18nextは、強力な国際化ライブラリであるi18nextをベースにした、Reactアプリケーション向けの国際化ライブラリです。
-
i18nextをベースにしたReact用の強力な国際化ライブラリ:
- 簡単な統合:Reactのフックやコンポーネントを活用して、手軽に国際化機能を組み込めます。
- 高度な機能:名前空間や遅延ロードなど、大規模アプリケーションにも対応可能な機能を提供します。
本記事では、react-i18nextを使用してReactアプリを国際化する方法を解説していきます。
■ React-i18nextの基礎
ライブラリのインストール方法
react-i18next
とi18next
を使用するにはライブラリをインストールする必要があります。
詳細は公式ドキュメントを参照してください!
基本的な設定と利用 (TypeScript版)
基本的な設定
以下は、i18nextインスタンスの初期化と基本的な言語リソースの定義の簡単なサンプルを掲載します。
// i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
// 言語リソースの定義
const resources: Record<string, any> = {
en: {
translation: {
welcome: "Welcome",
description: "This is an example."
}
},
ja: {
translation: {
welcome: "ようこそ",
description: "これは例です。"
}
}
};
// i18nの初期化
i18n
.use(initReactI18next) // react-i18nextを使用
.init({
resources, // 言語リソースを設定
lng: 'ja', // デフォルトの言語を設定
fallbackLng: 'en', // フォールバック言語(指定した言語使えない時これ使うよってやつです。)
interpolation: {
escapeValue: false // ReactではXSS対策が自動で行われるため、エスケープ不要
}
});
export default i18n;
この設定をアプリケーションのエントリーポイント(例:index.tsx
)でインポートします。
// index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './i18n'; // i18nの設定をインポート
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(<App />);
補足:ReactのXSS対策
interpolation: { escapeValue: false // ReactではXSS対策が自動で行われるため、エスケープ不要 }
翻訳文字列内で変数を挿入する際のエスケープ処理を制御します。
Reactはデフォルトでレンダリング時に自動的にエスケープ処理を行い、XSS(クロスサイトスクリプティング)攻撃から保護します。
また、i18nextとReactの両方でエスケープを行うと、文字列が二重にエスケープされてしまう可能性があるため、適切に設定を行うと安全です。
コンポーネントでの使用例
useTranslation
フックを使用して、コンポーネント内で翻訳を利用できます。
// App.tsx
import React from 'react';
import { useTranslation } from 'react-i18next';
const App: React.FC = () => {
const { t, i18n } = useTranslation();
// 言語を切り替える関数
const changeLanguage = (lng: string): void => {
i18n.changeLanguage(lng);
};
return (
<div>
<h1>{t('welcome')}</h1> {/* 翻訳キー "welcome" の表示 */}
<p>{t('description')}</p> {/* 翻訳キー "description" の表示 */}
<button onClick={() => changeLanguage('en')}>English</button>
<button onClick={() => changeLanguage('ja')}>日本語</button>
</div>
);
};
export default App;
■ 言語リソースの構築
実際に運用していくにあたって、言語リソースを分離して外部のモジュールとして管理することになります。
国際化(i18n)対応を円滑に運用・継続し、その品質を保証するには、文化的背景の理解と分離した言語リソースである辞書ファイルの効果的な管理・メンテナンスが不可欠です。
翻訳用の辞書ファイルは、アプリケーション内のすべての文言を網羅する必要があります。このファイルが適切に設計とおよび継続的な見直しがされていない場合、ユーザー体験が損なわれたり、運用の負担が増大するリスクがあります。
// locales/ja/translation.ts
const jaTranslation = {
welcome: "ようこそ",
description: "これは例です。",
};
export default jaTranslation;
// locales/en/translation.ts
const enTranslation = {
welcome: "Welcome",
description: "This is an example.",
};
export default enTranslation;
// i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
// 各言語の翻訳をインポート
import jaTranslation from './locales/ja/translation';
import enTranslation from './locales/en/translation';
// 言語リソースの定義
const resources = {
ja: {
translation: jaTranslation,
},
en: {
translation: enTranslation,
},
};
// i18nの初期化
i18n
.use(initReactI18next) // react-i18nextを使用
.init({
resources, // 言語リソースを設定
lng: 'ja', // デフォルトの言語
fallbackLng: 'en', // フォールバック言語
interpolation: {
escapeValue: false, // ReactではXSS対策が自動で行われるためエスケープ不要
},
});
export default i18n;
業務ドメイン領域の言葉の定義について
業務ドメインに特化した言葉の正確な定義を理解し、それを辞書ファイルやコードに反映することは、国際化対応における品質を左右する重要なプロセスです。この作業は一見、技術的ではないように思えるかもしれませんが、実際にはプロジェクト全体の精度と効率に直結する重要な要素です。
たとえば、弊社がターゲットとする不動産業界の機能開発において、言葉の定義に迷った経験があります。「測量図」という言葉について、辞書ファイルのキーや変数名をどう定義するか悩みました。
例: 測量図の英訳候補
- 測量図は、土地の測量結果を示す図を指します(厳密な定義は本記事では割愛します)。
これを英訳する際、まず「図」という観点からmap
やdiagram
が考えられます。一方で、「測量」という文脈を考慮すると、survey
やmeasurement
が候補に上がります。
実際、開発者の間ではmeasure map
とsurvey map
という2つの用語で議論が分かれていましたが、公的文書を参考にして統一を図りました(最終的にはsurvey map
を選定)。
正確な定義を導き出すためには、業務ドメインの知識や文化的背景の深い理解が必要です。これらの知識は学習や経験の蓄積によって得られることは言うまでもありません。しかし、それだけに依存せず、実践的な手法を活用することで補完することも可能です。本稿では、その具体例を後述します。
公的な定義を基にした言葉の選定
公的な定義や業界標準を活用して言葉を選定することで、辞書ファイルのキーを直感的かつ業務ドメインに即したものにすることができます。このアプローチには以下のようなメリットがあります。
-
表記揺れの防止
チーム全体で統一されたキーや変数名を使用することで、コードの可読性と開発効率が向上します。 -
コミュニケーションコストの削減
業務知識とコードが対応付けられるため、仕様の共有やレビューのプロセスがスムーズになります。
本稿で述べる内容がすべての読者に完全に適用できるとは限りません。しかし、類似した課題やニーズがある場合は、本記事の内容を参考にすることで、効果的な解決策が見つかる可能性があります。
● 日本国憲法や条例を参照する
法令や条例が英語で公開されていることをご存知ですか?
公式な法令文書は、専門用語の正確な翻訳や使用例を提供してくれる信頼性の高いリソースです。これを活用することで、用語選定における精度を大幅に向上させることができます。
● 外務省など省庁の公式英語版ウェブサイトを利用する
政府機関の公式英語版ウェブサイトでは、統一された専門用語が使用されており、辞書ファイルのキーや変数名を設計する際の有益なリファレンスとなります。一貫性のある辞書ファイルを構築するためにも、こうした公式資料を積極的に活用することをお勧めします。
● 学術論文や専門書の活用
学術論文や専門書は、業務ドメインにおける正確な用語選定に役立つ重要な情報源です。特に、業界特化型の専門用語や他言語での標準的な訳語を確認する際に非常に有用です。
■ 肥大化する辞書ファイルの管理について
プロジェクトが成長するにつれて、辞書ファイルのサイズも比例して増大します。
これはメニューやボタンだけでなく、ツールチップ、通知、エラーメッセージ、成功メッセージ、警告、フォームラベル、モーダルダイアログなど、あらゆるユーザーインターフェース要素やシステムメッセージの翻訳が必要になるためです。
辞書ファイルに含まれる要素の例
-
メニュー
- ナビゲーション項目や設定セクションの名称
- ユーザーロールに応じたカスタムメニュー
-
ボタン
- 操作ボタンのラベル(例: 保存、削除)
- 状態に応じた動的ラベル(例: 「送信中...」)
-
ツールチップ
- 補助的な説明文(例: 入力フォームのヒント、テーブルヘルプ)
-
メッセージ
- エラーメッセージ(例: 「入力値が無効です」)
- 成功メッセージ(例: 「保存が完了しました」)
- 警告メッセージ(例: 「操作は元に戻せません」)
-
フォームラベル
- 入力フィールド名やドロップダウン選択肢
-
モーダルダイアログ
- 確認や説明文、操作ボタンのラベル
-
その他
- 日付フォーマットやカスタマイズ可能な項目名
- アクセス制限に関するメッセージ(例: 「アクセスが拒否されました」)
辞書ファイルの適切な管理は、国際化対応の効率性や品質を大きく左右します。以下では、辞書ファイルを効果的に管理するための具体的な手法を紹介します。
■ 名前空間の活用による管理
名前空間(namespace)を利用して翻訳キーを論理的に分類することで、辞書ファイルの構造を整理できます。特に、大規模なプロジェクトでは、UIセクションごとに名前空間を分割することで、管理の効率性が大幅に向上します。
名前空間を用いた辞書構造
- 変更前: フラット構造
{
"headerTitle": "Welcome",
"headerSubtitle": "Please sign in",
"footerContact": "Contact Us",
"footerPrivacy": "Privacy Policy"
}
- 変更後: 分離した名前空間構造
{
"header": {
"title": "Welcome",
"subtitle": "Please sign in"
},
"footer": {
"contact": "Contact Us",
"privacy": "Privacy Policy"
}
}
名前空間とヘルパー関数の活用
プロジェクトの規模が大きくなるにつれて、辞書ファイルの名前空間が深くネストし、翻訳キーが複雑化しがちです。
たとえば、features.aapage.bbmode.ccform.ddbutton.eetooltip
ように長いキーが乱立することになります。こうした状況では、翻訳キーを扱いやすくするために ヘルパー関数 を導入することをお勧めします。
辞書ファイルを複数に分割して管理する方法もありますが、特にコンポーネント単位で分離されたコードにおいては、セクションごとにヘルパー関数を用意するのがシンプルで強力な手法です。
例: TypeScriptでのヘルパー関数
import { useTranslation } from 'react-i18next';
/**
* 深い名前空間を扱いやすくするヘルパーファンクション
* @param namespace 名前空間(階層構造を想定)
* @returns 指定された名前空間に限定した翻訳キーを処理する関数
*/
const useScopedTranslation = (namespace: string) => {
const { t } = useTranslation();
return (key: string) => t(`${namespace}.${key}`);
};
// 使用例
const trans = useScopedTranslation('app.pages.dashboard.settings.notifications');
console.log(trans('emailAlerts'));
// -> app.pages.dashboard.settings.notifications.emailAlerts
console.log(trans('pushNotifications'));
// -> app.pages.dashboard.settings.notifications.pushNotifications
■ 未使用フィールドの検出とダイエット術
プロジェクトが継続するにつれて未使用のフィールドが翻訳辞書に蓄積されることはよくある問題です。
これらのフィールドは、コードベースで直接参照されていないため、通常のlinterでは検知されません。
さらに、ほぼ全ての文言を網羅する辞書ファイル全体を1つ1つ手動でチェックするのは非常に手間がかかり、見落としが発生しやすいという課題があります。
i18next-scannerの使用
i18next-scannerは、i18n対応プロジェクトにおいて未使用の翻訳キーを自動で検出し、削除するためのツールです。これにより、翻訳ファイルが常に最新の状態に保たれ、不要なキーを削除する手間が省けます。
ライブラリのインストール方法
i18next-scanner
を使用するにはライブラリをインストールする必要があります。
詳細は公式ドキュメントを参照してください!
設定ファイルの構築
// i18next-scanner.config.cjs
/**
* @file i18next-parser Configuration File
* @description
*/
module.exports = {
input: ["src/**/*.{js,jsx,ts,tsx}"], // 対象ファイルを指定
output: "./src/locales", // 翻訳ファイルの出力先
options: {
lngs: ["ja"], // 使用言語
resource: {
loadPath: "./src/locales/{{lng}}/translation.json", // 既存翻訳ファイルのパス
savePath: "./src/locales/{{lng}}/translation.json", // 保存パス
},
keySeparator: ".", // ドットをキー階層として認識
namespaceSeparator: false, // ネームスペースセパレータを無効化
removeUnusedKeys: true, // 未使用キーを削除
func: {
list: ["t"], // 翻訳関数の指定
extensions: [".js", ".jsx", ".ts", ".tsx"], // 対象ファイル拡張子
},
},
};
i18next-scannerの難しさ
i18next-scannerは、ほぼ全ての言語リソースのメンテナンスを補助してくれる強力なツールですが、使用する際にはいくつかの難しい点があります。以下に主な課題とその詳細を説明します。
● 動的なフィールドの弱さ
i18next-scannerは静的なコードに強い一方で、動的に生成される翻訳キーには弱い部分があります。例えば、以下のようなケースでは正確にキーを検出できない可能性があります
//例1: 動的なキーの生成
const key = `user_${status}`;
t(key);
この場合、statusがどのような値を取るかによって生成されるキーが変わるため、i18next-scannerは具体的なキー名を検出は難しいです。
//例2: テンプレートリテラルを使用したキー
t(`button.${action}.label`);
こちらもactionが動的に変わるため、スキャナーはbutton.${action}.label
というキーを正確に認識できず、結果として適切な翻訳が行われないことがあります。
- 対応策:
-
静的なキーの使用を推奨
可能な限り、動的なキーの生成を避け、静的なキーを使用することでスキャナーによる検出精度を向上させます。 -
カスタムパターンの設定
i18next-scannerの設定ファイルで正規表現をカスタマイズし、特定の動的キーのパターンを認識できるように調整します。 -
手動でキーを追加: 動的キーが多用される場合、翻訳ファイルに手動でキーを追加し、スキャナーに認識させる方法もあります。
-
● Pluralsとのミスマッチ
i18nextは複数形(Pluralization)をサポートしており、count
パラメータを使用して単数形と複数形を切り替えることができます。しかし、i18next-scannerとの連携において、複数形のキーが正しく検出・管理されない場合があります。
問題点の例
- キーの自動生成の不一致: i18next-scannerは
count
を含むキーを正しく処理しないことがあり、複数形のキー(例:key_plural
)が正しく生成されない場合があります。 - 設定の不足: 設定ファイルで複数形の区切り文字やルールが正しく設定されていないと、スキャナーが期待通りに動作しません。
// ソースコードでの使用
t('notification.message', { count: messageCount });
{
"notification": {
"message": "You have {{count}} new message",
"message_plural": "You have {{count}} new messages"
}
}
上記のように、countに応じて単数形と複数形のキーを用意する必要がありますが、i18next-scannerがこれを正しく扱えない場合があります。
対応策:
• 設定ファイルの調整を行う
pluralSeparatorやcontextSeparatorなどのオプションを設定ファイルで適切に設定します。
// i18next-scanner.config.cjs
module.exports = {
input: ["src/**/*.{js,jsx,ts,tsx}"],
output: "./src/locales",
options: {
lngs: ["ja"],
resource: {
loadPath: "./src/locales/{{lng}}/translation.json",
savePath: "./{{lng}}/translation.json",
},
keySeparator: ".",
namespaceSeparator: false,
removeUnusedKeys: true,
func: {
list: ["t"],
extensions: [".js", ".jsx", ".ts", ".tsx"],
},
interpolation: {
prefix: "{{",
suffix: "}}",
},
pluralSeparator: "_",
contextSeparator: "_",
},
};
- コンテキスト(Context)の扱い
contextを使用して、性別や状況に応じた異なる翻訳を提供する場合、i18next-scannerがこれを正しく処理しないことがあります。
t('user.profile', { context: 'male' });
t('user.profile', { context: 'female' });
{
"user": {
"profile_male": "彼のプロフィール",
"profile_female": "彼女のプロフィール"
}
}
■ エラーメッセージの国際化について
エラーメッセージの国際化は、開発プロセスにおいてつい後回しにされがちです。想定ケースを網羅する労力に対して、致命的ではない上に手間がかかるからです。
しかし、これを適切に実装することで、ユーザー体験の向上にもつながります。多言語対応は、グローバルなユーザーベースを持つプロジェクトにおいて、地味ながら重要です。
スキーマライブラリとの併用
エラーメッセージの国際化を実現する際には、スキーマライブラリ(当社ではzodを使用しています👀)を活用することで、効率的な実装が可能です。スキーマライブラリを使用することで、入力データのバリデーションロジックを簡潔に記述できるだけでなく、カスタマイズ可能なエラーメッセージを設定することができます。
以下では、zodを使用してエラーメッセージを国際化対応する方法を紹介します。バリデーションエラー時に特定の言語のエラーメッセージを動的に表示できます。
設定ファイルの構築
z.setErrorMap(zodI18nMap) を設定することで、Zod のバリデーションエラーメッセージを zod-i18n-map によって置き換えることができます。
以下にサンプルを示します。
英語と日本語の両方をリソースに追加し、ユーザーの言語設定に応じて切り替えられるようにします。
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
// ローカルファイル
import ja from '../locales/ja/translation.json'
import en from '../locales/en/translation.json'
// zod-i18n
import { z } from 'zod'
import { zodI18nMap } from 'zod-i18n-map'
import jaZodTranslation from 'zod-i18n-map/locales/ja/zod.json'
import enZodTranslation from 'zod-i18n-map/locales/en/zod.json'
// https://react.i18next.com/
i18n
.use(initReactI18next) // i18nをreact-i18nextに渡す
.init({
resources: {
ja: {
translation: ja,
zod: jaZodTranslation,
},
en: {
translation: en,
zod: enZodTranslation,
},
},
lng: 'ja', // 初期言語を日本語に設定
fallbackLng: 'ja',
interpolation: {
escapeValue: false, // ReactはすでにXSSから保護されているため無効化 => https://www.i18next.com/translation-function/interpolation#unescape
},
})
z.setErrorMap(zodI18nMap)
export default i18n
スキーマバリデーションのエラーメッセージ定義
辞書ファイルに以下のような翻訳が含まれているとします。
- zod-i18n-map/locales/en/zod.json
{
"USERNAME_TOO_SHORT": "Username must be at least 3 characters.",
"AGE_TOO_YOUNG": "Age must be at least 18."
}
- ../locales/en/translation.json
{
"USERNAME_TOO_SHORT": "ユーザー名は最低3文字必要です。",
"AGE_TOO_YOUNG": "年齢は18歳以上でなければなりません。",
}
import { z } from 'zod'
const userSchema = z.object({
username: z.string().min(3, { message: 'USERNAME_TOO_SHORT' }),
age: z.number().min(18, { message: 'AGE_TOO_YOUNG' }),
})
バリデーションの実行とエラーメッセージの取得利用例
バリデーションエラーメッセージは、選択された言語に応じて表示されます。
例えば、以下のようにエラーメッセージを取得します。
import { userSchema } from './path-to-schema'
import i18n from './path-to-i18n'
const validateUser = (data) => {
const result = userSchema.safeParse(data)
if (!result.success) {
// エラーメッセージを取得
const errors = result.error.errors.map(err => i18n.t(`zod:${err.message}`))
console.log(errors)
} else {
console.log('バリデーションが成功しました')
}
}
// サンプルデータ
const userData = {
username: 'ab', // 3文字未満
age: 16, // 18未満
}
validateUser(userData)
■ まとめ
本記事では、i18n(国際化)およびl10n(ローカリゼーション)の定義とその目的について解説し、Reactアプリケーションにおける国際化の実装手法としてreact-i18next
ライブラリを紹介しました。
react-i18next
は、React環境へのシームレスな統合を可能にし、強力な機能セットを提供する一方で、関連記事では基本的な導入程度の内容が多く、実際のプロダクト開発における翻訳管理の効率化に関する具体的なベストプラクティスやツールの活用方法については十分にカバーされていない印象を受けました。翻訳リソースのスケーラブルな管理や未使用キーの自動検出など、実運用における課題解決に向けたさらなる情報提供があればぜひコメントでも共有していただけたらと思います。(`・ω・´)
本記事が、国際化対応を進める開発者やチームの参考となり、実際の開発現場での課題解決に役立つことを期待しています。