国際化(i18n)ガイド
Guren は翻訳管理、複数言語の複数形サポート、ファイルベースとメモリベースの両方のローダーを備えた包括的な国際化システムを提供します。
国際化(i18n)ガイド
Guren は翻訳管理、複数言語の複数形サポート、ファイルベースとメモリベースの両方のローダーを備えた包括的な国際化システムを提供します。
コアコンセプト
- I18nManager – 翻訳とロケールを管理する中央ハブ。
- Translator – コンテキスト付きで翻訳を実行するインスタンス。
- TranslationLoader – 翻訳を読み込むインターフェース(JSONファイルまたはメモリ)。
- 複数形処理 – 15以上の言語に対応した組み込みルールとカスタマイズ可能な形式。
基本的な使い方
クイックスタート
import { createI18n, setI18n, t, tc } from '@guren/core'
const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
messages: {
en: {
messages: {
hello: 'Hello',
welcome: 'Welcome, :name!',
items: 'One item|:count items',
},
},
ja: {
messages: {
hello: 'こんにちは',
welcome: 'ようこそ、:nameさん!',
},
},
},
})
setI18n(i18n)
// 翻訳
t('messages.hello') // 'Hello'
t('messages.welcome', { name: 'John' }) // 'Welcome, John!'
// 複数形
tc('messages.items', 1) // 'One item'
tc('messages.items', 5) // '5 items'
置換構文
変数置換には2つの形式がサポートされています。
// Laravel形式 :variable
const messages = { greeting: 'Hello, :name!' }
t('greeting', { name: 'World' }) // 'Hello, World!'
// ブレース形式 {variable}
const messages = { greeting: 'Hello, {name}!' }
t('greeting', { name: 'World' }) // 'Hello, World!'
ネストされたキー
ドット記法でネストされた翻訳キーにアクセスできます。
const messages = {
en: {
user: {
profile: {
title: 'ユーザープロフィール',
settings: '設定',
},
},
},
}
t('user.profile.title') // 'ユーザープロフィール'
t('user.profile.settings') // '設定'
複数形処理
基本的な複数形処理
複数形を|で区切ります。
const messages = {
en: {
apple: 'apple|apples',
item: 'One item|:count items',
},
}
tc('apple', 1) // 'apple'
tc('apple', 5) // 'apples'
tc('item', 1) // 'One item'
tc('item', 10) // '10 items'
サポートされている言語
組み込みの複数形ルールは以下のとおりです。
| 言語 | 形式数 | ルール |
|---|---|---|
| 英語、ドイツ語、スペイン語、イタリア語、ポルトガル語、オランダ語 | 2 | 1 = 単数、それ以外 = 複数 |
| フランス語、ブラジルポルトガル語 | 2 | 0-1 = 単数、それ以外 = 複数 |
| 日本語、中国語、韓国語、ベトナム語、タイ語 | 1 | 複数形なし |
| ロシア語、ウクライナ語 | 3 | 1、少数(2-4)、多数(5+) |
| ポーランド語、チェコ語、スロバキア語 | 3 | 複雑なルール |
| アラビア語 | 6 | 最も複雑な複数形処理 |
複雑な複数形処理
複数の複数形を持つ言語の場合は次のように記述します。
const messages = {
ru: {
// ロシア語: 1 | 少数 | 多数
apple: 'яблоко|яблока|яблок',
},
}
i18n.setLocale('ru')
tc('apple', 1) // 'яблоко' (1)
tc('apple', 2) // 'яблока' (少数)
tc('apple', 5) // 'яблок' (多数)
tc('apple', 21) // 'яблоко' (1 - 特殊ケース)
カスタム複数形ルール
import { createI18n } from '@guren/core'
const i18n = createI18n({
locale: 'custom',
messages: { /* ... */ },
})
// カスタムルールを設定
i18n.setPluralizationRule('custom', (count) => {
if (count === 0) return 0
if (count === 1) return 1
return 2
})
翻訳の読み込み
JSONローダー
JSONファイルから翻訳を読み込めます。
lang/
├── en/
│ ├── messages.json
│ └── validation.json
└── ja/
├── messages.json
└── validation.json
import { I18nManager, JsonLoader } from '@guren/core'
const loader = new JsonLoader('./lang', {
cache: true, // キャッシュを有効化(デフォルト: true)
})
const i18n = new I18nManager({
locale: 'en',
loader,
})
// オンデマンドでロケールを読み込み
await i18n.loadLocale('ja')
メモリローダー
テストや埋め込み翻訳に使用します。
import { MemoryLoader, I18nManager } from '@guren/core'
const loader = new MemoryLoader({
en: {
messages: { hello: 'Hello' },
},
ja: {
messages: { hello: 'こんにちは' },
},
})
// 動的にメッセージを追加
loader.addMessages('fr', { messages: { hello: 'Bonjour' } })
// ロケールを削除
loader.removeLocale('fr')
I18nManager
マネージャーの作成
import { I18nManager, JsonLoader } from '@guren/core'
const i18n = new I18nManager({
locale: 'en', // 現在のロケール
fallbackLocale: 'en', // 翻訳がない場合のフォールバック
loader: new JsonLoader('./lang'),
messages: {
// オプションの初期メッセージ
en: { /* ... */ },
},
})
ロケール管理
// 現在のロケールを取得/設定
const locale = i18n.getLocale()
i18n.setLocale('ja')
// フォールバックロケールを取得/設定
const fallback = i18n.getFallbackLocale()
i18n.setFallbackLocale('en')
// 読み込まれたロケールを確認
i18n.isLocaleLoaded('en') // true
i18n.getAvailableLocales() // ['en', 'ja']
// ロケールを読み込み
await i18n.loadLocale('fr')
翻訳メソッド
// シンプルな翻訳
i18n.t('messages.hello')
// 置換付き
i18n.t('messages.welcome', { name: 'John' })
// 複数形
i18n.tc('messages.items', 5)
i18n.tc('messages.items', 5, { type: 'apple' })
// 翻訳が存在するか確認
i18n.has('messages.hello') // true
i18n.has('messages.hello', 'ja') // 特定のロケールを確認
スコープ付きトランスレーター
// 特定のロケール用のトランスレーターを作成
const jaTranslator = i18n.forLocale('ja')
jaTranslator.t('messages.hello') // 'こんにちは'
// 元のマネージャーは変更されない
i18n.getLocale() // まだ 'en'
グローバル関数
グローバルマネージャーのセットアップ
import { createI18n, setI18n, getI18n, t, tc } from '@guren/core'
// ブートストラップファイルで
const i18n = createI18n({
locale: 'en',
messages: { /* ... */ },
})
setI18n(i18n)
// アプリケーションのどこでも
t('messages.hello')
tc('messages.items', 5)
// マネージャーにアクセス
const i18n = getI18n()
リクエストミドルウェア
リクエストごとのロケール
組み込みの detectLocaleMiddleware を使うと、?locale= クエリパラメータ・locale Cookie・Accept-Language ヘッダーの順でリクエストのロケールを解決できます(サポート対象のロケールに限定されます):
import { detectLocaleMiddleware } from '@guren/core'
app.use('*', detectLocaleMiddleware({ supported: ['en', 'ja'] }))
このミドルウェアは locale コンテキスト変数をセットし(Inertia レスポンスのルート <html lang> 属性に使われます)、i18n マネージャーが利用可能な場合(setI18n() のグローバル、または i18n オプションで渡したもの)はリクエストスコープの t/tc トランスレーターヘルパーもバインドします:
export class PostController extends Controller {
async index() {
const t = this.ctx.get('t')
return this.json({ message: t('messages.hello') })
}
}
オプション: fallback(デフォルトはサポート対象の先頭)、検出順を変える sources(例: ['header'])、queryParam、cookieName、マネージャーを明示的に渡す i18n(false でトランスレーターのバインドを停止)。Accept-Language のマッチングはリージョンサブタグ(ja-JP は ja にマッチ)と q 値を理解します。supported は翻訳ファイルが実際に提供するロケールと同期させてください — リストに無いロケールは検出されません。
ミドルウェアを使わない場合、Inertia レスポンスはアプリ全体の i18n ロケール、それも無ければ en にフォールバックします。レスポンスごとの lang オプションが常に優先されます:
return this.inertia(pages.posts.Show, { post }, { lang: 'ja' })
コントローラーでの使用
import { Controller } from '@guren/core'
export class UserController extends Controller {
async index() {
const t = this.ctx.get('t')
return this.json({
message: t('messages.hello'),
})
}
}
設定
完全な設定例
import { I18nManager, JsonLoader } from '@guren/core'
const i18n = new I18nManager({
locale: 'en',
fallbackLocale: 'en',
loader: new JsonLoader('./lang', { cache: true }),
messages: {
en: {
messages: {
hello: 'Hello',
goodbye: 'Goodbye',
},
validation: {
required: 'The :field field is required',
email: 'Please enter a valid email address',
min: 'Must be at least :min characters',
},
errors: {
not_found: 'Resource not found',
unauthorized: 'Please log in to continue',
},
},
ja: {
messages: {
hello: 'こんにちは',
goodbye: 'さようなら',
},
validation: {
required: ':fieldは必須です',
email: '有効なメールアドレスを入力してください',
min: ':min文字以上で入力してください',
},
},
},
})
テスト
import { describe, test, expect, beforeEach } from 'bun:test'
import { createI18n, MemoryLoader } from '@guren/core'
describe('翻訳', () => {
let i18n
beforeEach(() => {
i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
messages: {
en: {
hello: 'Hello',
welcome: 'Welcome, :name!',
items: 'One item|:count items',
},
ja: {
hello: 'こんにちは',
},
},
})
})
test('シンプルなキーを翻訳する', () => {
expect(i18n.t('hello')).toBe('Hello')
})
test('置換付きで翻訳する', () => {
expect(i18n.t('welcome', { name: 'John' })).toBe('Welcome, John!')
})
test('複数形を処理する', () => {
expect(i18n.tc('items', 1)).toBe('One item')
expect(i18n.tc('items', 5)).toBe('5 items')
})
test('ロケールを切り替える', () => {
i18n.setLocale('ja')
expect(i18n.t('hello')).toBe('こんにちは')
})
test('存在しない翻訳にフォールバックする', () => {
i18n.setLocale('ja')
expect(i18n.t('welcome', { name: 'John' })).toBe('Welcome, John!')
})
test('存在しない翻訳はキーを返す', () => {
expect(i18n.t('missing.key')).toBe('missing.key')
})
})
翻訳ファイル構造
推奨される構成
lang/
├── en/
│ ├── messages.json # 一般的なメッセージ
│ ├── validation.json # バリデーションメッセージ
│ ├── errors.json # エラーメッセージ
│ └── emails.json # メールテンプレート
└── ja/
├── messages.json
├── validation.json
├── errors.json
└── emails.json
JSONファイルの例
// lang/en/messages.json
{
"hello": "Hello",
"welcome": "Welcome, :name!",
"items": "One item|:count items",
"user": {
"profile": "Profile",
"settings": "Settings"
}
}
// lang/en/validation.json
{
"required": "The :field field is required",
"email": "Please enter a valid email",
"min": {
"string": "Must be at least :min characters",
"numeric": "Must be at least :min"
}
}
ベストプラクティス
名前空間付きキーを使用: 機能ごとに翻訳を整理(
user.profile.title)。フォールバックロケールを設定: 存在しない翻訳に備えて常にフォールバックを設定。
複数形に数を含める: 動的な数値には
:countプレースホルダーを使用。本番環境ではキャッシュを有効化: JSONローダーのキャッシュを本番環境で有効に。
翻訳を整理して保持: 異なる関心事には別々のファイルを使用。
存在しないキーを適切に処理: ロギング用に
onMissingKeyコールバックを使用。翻訳をテスト: すべてのキーがすべてのロケールに存在することを確認するテストを作成。
ロケールをオンデマンドで読み込み: パフォーマンス向上のため
loadLocale()を使用。