# Internationalization (i18n) Guide

Guren provides a comprehensive internationalization system with translation management, pluralization support for multiple languages, and both file-based and memory-based loading.

## Core Concepts

- **I18nManager** – Central hub for managing translations and locales.
- **Translator** – Instance for performing translations with context.
- **TranslationLoader** – Interface for loading translations (JSON files or memory).
- **Pluralization** – Built-in rules for 15+ languages with customizable forms.

## Basic Usage

### Quick Start

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

// Translate
t('messages.hello')                    // 'Hello'
t('messages.welcome', { name: 'John' }) // 'Welcome, John!'

// Pluralize
tc('messages.items', 1)  // 'One item'
tc('messages.items', 5)  // '5 items'
```

### Replacement Syntax

Two formats are supported for variable replacement:

```ts
// Laravel-style :variable
const messages = { greeting: 'Hello, :name!' }
t('greeting', { name: 'World' }) // 'Hello, World!'

// Brace-style {variable}
const messages = { greeting: 'Hello, {name}!' }
t('greeting', { name: 'World' }) // 'Hello, World!'
```

### Nested Keys

Use dot notation to access nested translation keys:

```ts
const messages = {
  en: {
    user: {
      profile: {
        title: 'User Profile',
        settings: 'Settings',
      },
    },
  },
}

t('user.profile.title')    // 'User Profile'
t('user.profile.settings') // 'Settings'
```

## Pluralization

### Basic Pluralization

Separate plural forms with `|`:

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

### Supported Languages

Built-in pluralization rules for:

| Language | Forms | Rule |
|----------|-------|------|
| English, German, Spanish, Italian, Portuguese, Dutch | 2 | 1 = singular, else plural |
| French, Brazilian Portuguese | 2 | 0-1 = singular, else plural |
| Japanese, Chinese, Korean, Vietnamese, Thai | 1 | No plural forms |
| Russian, Ukrainian | 3 | one, few (2-4), many (5+) |
| Polish, Czech, Slovak | 3 | Complex rules |
| Arabic | 6 | Most complex pluralization |

### Complex Pluralization

For languages with multiple plural forms:

```ts
const messages = {
  ru: {
    // Russian: one | few | many
    apple: 'яблоко|яблока|яблок',
  },
}

i18n.setLocale('ru')
tc('apple', 1)   // 'яблоко' (one)
tc('apple', 2)   // 'яблока' (few)
tc('apple', 5)   // 'яблок' (many)
tc('apple', 21)  // 'яблоко' (one - special case)
```

### Custom Pluralization Rules

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

const i18n = createI18n({
  locale: 'custom',
  messages: { /* ... */ },
})

// Set custom rule
i18n.setPluralizationRule('custom', (count) => {
  if (count === 0) return 0
  if (count === 1) return 1
  return 2
})
```

## Loading Translations

### JSON Loader

Load translations from JSON files:

```
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, // Enable caching (default: true)
})

const i18n = new I18nManager({
  locale: 'en',
  loader,
})

// Load locale on demand
await i18n.loadLocale('ja')
```

### Memory Loader

For testing or embedded translations:

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

const loader = new MemoryLoader({
  en: {
    messages: { hello: 'Hello' },
  },
  ja: {
    messages: { hello: 'こんにちは' },
  },
})

// Dynamically add messages
loader.addMessages('fr', { messages: { hello: 'Bonjour' } })

// Remove locale
loader.removeLocale('fr')
```

## I18nManager

### Creating the Manager

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

const i18n = new I18nManager({
  locale: 'en',           // Current locale
  fallbackLocale: 'en',   // Fallback when translation missing
  loader: new JsonLoader('./lang'),
  messages: {
    // Optional initial messages
    en: { /* ... */ },
  },
})
```

### Locale Management

```ts
// Get/set current locale
const locale = i18n.getLocale()
i18n.setLocale('ja')

// Get/set fallback locale
const fallback = i18n.getFallbackLocale()
i18n.setFallbackLocale('en')

// Check loaded locales
i18n.isLocaleLoaded('en')      // true
i18n.getAvailableLocales()     // ['en', 'ja']

// Load locale
await i18n.loadLocale('fr')
```

### Translation Methods

```ts
// Simple translation
i18n.t('messages.hello')

// With replacements
i18n.t('messages.welcome', { name: 'John' })

// Pluralization
i18n.tc('messages.items', 5)
i18n.tc('messages.items', 5, { type: 'apple' })

// Check if translation exists
i18n.has('messages.hello')       // true
i18n.has('messages.hello', 'ja') // check specific locale
```

### Scoped Translators

```ts
// Create translator for specific locale
const jaTranslator = i18n.forLocale('ja')
jaTranslator.t('messages.hello') // 'こんにちは'

// Original manager unchanged
i18n.getLocale() // still 'en'
```

## Global Functions

### Setup Global Manager

```ts
import { createI18n, setI18n, getI18n, t, tc } from '@guren/core'

// In bootstrap file
const i18n = createI18n({
  locale: 'en',
  messages: { /* ... */ },
})

setI18n(i18n)

// Anywhere in application
t('messages.hello')
tc('messages.items', 5)

// Access manager
const i18n = getI18n()
```

## Request Middleware

### Per-Request Locale

Use the built-in `detectLocaleMiddleware` to resolve each request's locale from the `?locale=` query parameter, a `locale` cookie, or the `Accept-Language` header (in that order), restricted to your supported locales:

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

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

The middleware sets the `locale` context variable — which Inertia responses use for the root `<html lang>` attribute — and, when an i18n manager is available (the global one from `setI18n()`, or one passed via the `i18n` option), binds request-scoped `t`/`tc` translator helpers:

```ts
export class PostController extends Controller {
  async index() {
    const t = this.ctx.get('t')
    return this.json({ message: t('messages.hello') })
  }
}
```

Options: `fallback` (defaults to the first supported locale), `sources` to change the detection order (e.g. `['header']`), `queryParam`, `cookieName`, and `i18n` to pass a manager explicitly (or `false` to skip the translator binding). `Accept-Language` matching understands region subtags (`ja-JP` matches `ja`) and q-values. Keep `supported` in sync with the locales your translation files actually provide — a locale missing from the list is never detected.

Without the middleware, Inertia responses fall back to the app-wide i18n locale, then `en`. A per-response `lang` option always wins:

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

### Using in Controllers

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

## Configuration

### Full Configuration Example

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

## Testing

```ts
import { describe, test, expect, beforeEach } from 'bun:test'
import { createI18n, MemoryLoader } from '@guren/core'

describe('Translations', () => {
  let i18n

  beforeEach(() => {
    i18n = createI18n({
      locale: 'en',
      fallbackLocale: 'en',
      messages: {
        en: {
          hello: 'Hello',
          welcome: 'Welcome, :name!',
          items: 'One item|:count items',
        },
        ja: {
          hello: 'こんにちは',
        },
      },
    })
  })

  test('translates simple key', () => {
    expect(i18n.t('hello')).toBe('Hello')
  })

  test('translates with replacements', () => {
    expect(i18n.t('welcome', { name: 'John' })).toBe('Welcome, John!')
  })

  test('handles pluralization', () => {
    expect(i18n.tc('items', 1)).toBe('One item')
    expect(i18n.tc('items', 5)).toBe('5 items')
  })

  test('switches locale', () => {
    i18n.setLocale('ja')
    expect(i18n.t('hello')).toBe('こんにちは')
  })

  test('falls back for missing translation', () => {
    i18n.setLocale('ja')
    expect(i18n.t('welcome', { name: 'John' })).toBe('Welcome, John!')
  })

  test('returns key for missing translation', () => {
    expect(i18n.t('missing.key')).toBe('missing.key')
  })
})
```

## Translation File Structure

### Recommended Organization

```
lang/
├── en/
│   ├── messages.json     # General messages
│   ├── validation.json   # Validation messages
│   ├── errors.json       # Error messages
│   └── emails.json       # Email templates
└── ja/
    ├── messages.json
    ├── validation.json
    ├── errors.json
    └── emails.json
```

### Example JSON Files

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

## Best Practices

1. **Use namespaced keys**: Organize translations by feature (`user.profile.title`).

2. **Set fallback locale**: Always configure a fallback for missing translations.

3. **Include count in pluralization**: Use `:count` placeholder for dynamic numbers.

4. **Cache in production**: Enable caching for JSON loader in production.

5. **Keep translations organized**: Use separate files for different concerns.

6. **Handle missing keys gracefully**: Use `onMissingKey` callback for logging.

7. **Test translations**: Write tests to ensure all keys exist in all locales.

8. **Load locales on demand**: Use `loadLocale()` for better performance.
