# 国際化（i18n）ガイド

Guren は翻訳管理、複数言語の複数形サポート、ファイルベースとメモリベースの両方のローダーを備えた包括的な国際化システムを提供します。

## コアコンセプト

- **I18nManager** – 翻訳とロケールを管理する中央ハブ。
- **Translator** – コンテキスト付きで翻訳を実行するインスタンス。
- **TranslationLoader** – 翻訳を読み込むインターフェース（JSONファイルまたはメモリ）。
- **複数形処理** – 15以上の言語に対応した組み込みルールとカスタマイズ可能な形式。

## 基本的な使い方

### クイックスタート

```ts
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つの形式がサポートされています。

```ts
// 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!'
```

### ネストされたキー

ドット記法でネストされた翻訳キーにアクセスできます。

```ts
const messages = {
  en: {
    user: {
      profile: {
        title: 'ユーザープロフィール',
        settings: '設定',
      },
    },
  },
}

t('user.profile.title')    // 'ユーザープロフィール'
t('user.profile.settings') // '設定'
```

## 複数形処理

### 基本的な複数形処理

複数形を`|`で区切ります。

```ts
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 | 最も複雑な複数形処理 |

### 複雑な複数形処理

複数の複数形を持つ言語の場合は次のように記述します。

```ts
const messages = {
  ru: {
    // ロシア語: 1 | 少数 | 多数
    apple: 'яблоко|яблока|яблок',
  },
}

i18n.setLocale('ru')
tc('apple', 1)   // 'яблоко' (1)
tc('apple', 2)   // 'яблока' (少数)
tc('apple', 5)   // 'яблок' (多数)
tc('apple', 21)  // 'яблоко' (1 - 特殊ケース)
```

### カスタム複数形ルール

```ts
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
```

```ts
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')
```

### メモリローダー

テストや埋め込み翻訳に使用します。

```ts
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

### マネージャーの作成

```ts
import { I18nManager, JsonLoader } from '@guren/core'

const i18n = new I18nManager({
  locale: 'en',           // 現在のロケール
  fallbackLocale: 'en',   // 翻訳がない場合のフォールバック
  loader: new JsonLoader('./lang'),
  messages: {
    // オプションの初期メッセージ
    en: { /* ... */ },
  },
})
```

### ロケール管理

```ts
// 現在のロケールを取得/設定
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')
```

### 翻訳メソッド

```ts
// シンプルな翻訳
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') // 特定のロケールを確認
```

### スコープ付きトランスレーター

```ts
// 特定のロケール用のトランスレーターを作成
const jaTranslator = i18n.forLocale('ja')
jaTranslator.t('messages.hello') // 'こんにちは'

// 元のマネージャーは変更されない
i18n.getLocale() // まだ 'en'
```

## グローバル関数

### グローバルマネージャーのセットアップ

```ts
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` ヘッダーの順でリクエストのロケールを解決できます(サポート対象のロケールに限定されます):

```ts
import { detectLocaleMiddleware } from '@guren/core'

app.use('*', detectLocaleMiddleware({ supported: ['en', 'ja'] }))
```

このミドルウェアは `locale` コンテキスト変数をセットし(Inertia レスポンスのルート `<html lang>` 属性に使われます)、i18n マネージャーが利用可能な場合(`setI18n()` のグローバル、または `i18n` オプションで渡したもの)はリクエストスコープの `t`/`tc` トランスレーターヘルパーもバインドします:

```ts
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` オプションが常に優先されます:

```ts
return this.inertia(pages.posts.Show, { post }, { lang: 'ja' })
```

### コントローラーでの使用

```ts
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'),
    })
  }
}
```

## 設定

### 完全な設定例

```ts
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文字以上で入力してください',
      },
    },
  },
})
```

## テスト

```ts
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ファイルの例

```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"
  }
}
```

## ベストプラクティス

1. **名前空間付きキーを使用**: 機能ごとに翻訳を整理（`user.profile.title`）。

2. **フォールバックロケールを設定**: 存在しない翻訳に備えて常にフォールバックを設定。

3. **複数形に数を含める**: 動的な数値には`:count`プレースホルダーを使用。

4. **本番環境ではキャッシュを有効化**: JSONローダーのキャッシュを本番環境で有効に。

5. **翻訳を整理して保持**: 異なる関心事には別々のファイルを使用。

6. **存在しないキーを適切に処理**: ロギング用に`onMissingKey`コールバックを使用。

7. **翻訳をテスト**: すべてのキーがすべてのロケールに存在することを確認するテストを作成。

8. **ロケールをオンデマンドで読み込み**: パフォーマンス向上のため`loadLocale()`を使用。
