npm package: @flightdev/i18n
Description: Agnostic internationalization for Flight Framework. Choose your engine: i18next, Paraglide, FormatJS, Lingui, or custom.
Latest version: 0.2.0

# @flightdev/i18n

Internationalization for Flight Framework. Supports multiple translation libraries with a unified API, automatic locale detection, and SSR-friendly design.

## Table of Contents

- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Adapters](#adapters)
- [Locale Detection](#locale-detection)
- [UI Framework Integration](#ui-framework-integration)
- [Formatting Utilities](#formatting-utilities)
- [Pluralization](#pluralization)
- [Interpolation](#interpolation)
- [Namespace Support](#namespace-support)
- [Loading Translations](#loading-translations)
- [SSR and Hydration](#ssr-and-hydration)
- [API Reference](#api-reference)
- [Creating Custom Adapters](#creating-custom-adapters)
- [License](#license)

---

## Features

- Unified API across i18next, Paraglide, FormatJS, and Lingui
- Automatic locale detection (URL, cookie, header, browser)
- Type-safe translations with TypeScript
- SSR support with hydration
- Lazy loading of translation bundles
- Built-in formatting for numbers, dates, and relative time
- Pluralization and gender support
- React, Vue, Svelte, and Solid integrations

---

## Installation

```bash
npm install @flightdev/i18n

# Install your preferred adapter:
npm install i18next         # Most popular, feature-rich
npm install @inlang/paraglide-js  # Compile-time, smallest bundle
npm install @formatjs/intl  # ICU message format
npm install @lingui/core    # Compile-time extraction
```

---

## Quick Start

### 1. Create the i18n Instance

```typescript
// src/i18n.ts
import { createI18n } from '@flightdev/i18n';
import { i18next } from '@flightdev/i18n/i18next';

export const i18n = createI18n(i18next({
  locales: ['en', 'es', 'fr', 'de'],
  defaultLocale: 'en',
  fallbackLocale: 'en',
}));
```

### 2. Add Translation Files

```
src/
  locales/
    en/
      common.json
      errors.json
    es/
      common.json
      errors.json
```

```json
// src/locales/en/common.json
{
  "welcome": "Welcome to our app",
  "greeting": "Hello, {{name}}!",
  "items": "You have {{count}} item",
  "items_plural": "You have {{count}} items"
}
```

### 3. Initialize and Use

```typescript
import { i18n } from './i18n';

await i18n.init();

// Basic translation
i18n.t('welcome');  // "Welcome to our app"

// With interpolation
i18n.t('greeting', { name: 'Maria' });  // "Hello, Maria!"

// With pluralization
i18n.t('items', { count: 1 });  // "You have 1 item"
i18n.t('items', { count: 5 });  // "You have 5 items"

// Change locale
await i18n.setLocale('es');
i18n.t('welcome');  // "Bienvenido a nuestra app"
```

---

## Adapters

### i18next

The most popular option with extensive features.

```typescript
import { i18next } from '@flightdev/i18n/i18next';

const adapter = i18next({
  locales: ['en', 'es', 'fr'],
  defaultLocale: 'en',
  fallbackLocale: 'en',
  
  // i18next-specific options
  detection: {
    order: ['cookie', 'header', 'navigator'],
    caches: ['cookie'],
  },
  
  backend: {
    loadPath: '/locales/{{lng}}/{{ns}}.json',
  },
});
```

### Paraglide

Compile-time translations for the smallest bundle size.

```typescript
import { paraglide } from '@flightdev/i18n/paraglide';
import * as messages from './paraglide/messages';

const adapter = paraglide({
  messages,
  locales: ['en', 'es'],
  defaultLocale: 'en',
});
```

### FormatJS (react-intl)

ICU message format with powerful formatting.

```typescript
import { formatjs } from '@flightdev/i18n/formatjs';

const adapter = formatjs({
  locales: ['en', 'es'],
  defaultLocale: 'en',
  messages: {
    en: { greeting: 'Hello, {name}!' },
    es: { greeting: 'Hola, {name}!' },
  },
});
```

### Lingui

Compile-time extraction with excellent developer experience.

```typescript
import { lingui } from '@flightdev/i18n/lingui';
import { messages as enMessages } from './locales/en/messages';
import { messages as esMessages } from './locales/es/messages';

const adapter = lingui({
  locales: ['en', 'es'],
  defaultLocale: 'en',
  messages: {
    en: enMessages,
    es: esMessages,
  },
});
```

---

## Locale Detection

Automatic detection from multiple sources:

```typescript
import { createI18n, detectLocale } from '@flightdev/i18n';

const i18n = createI18n(adapter, {
  detection: {
    // Detection order (first match wins)
    order: ['path', 'cookie', 'header', 'navigator'],
    
    // URL path detection: /en/about -> locale: 'en'
    pathIndex: 0,
    
    // Cookie name for persistence
    cookieName: 'locale',
    
    // Header to check (Accept-Language)
    headerName: 'accept-language',
  },
});
```

### Server-Side Detection

```typescript
import { getLocaleFromRequest } from '@flightdev/i18n';

export async function loader({ request }) {
  const locale = getLocaleFromRequest(request, {
    supported: ['en', 'es', 'fr'],
    fallback: 'en',
  });
  
  return { locale };
}
```

### URL-Based Routing

```typescript
// Routes: /en/about, /es/about, /fr/about
import { createLocaleRouter } from '@flightdev/i18n';

const router = createLocaleRouter({
  locales: ['en', 'es', 'fr'],
  defaultLocale: 'en',
  strategy: 'prefix',  // 'prefix' | 'prefix-except-default' | 'domain'
});
```

---

## UI Framework Integration

### React

```tsx
import { I18nProvider, useTranslation, Trans } from '@flightdev/i18n/react';

function App() {
  return (
    <I18nProvider i18n={i18n}>
      <Content />
    </I18nProvider>
  );
}

function Content() {
  const { t, locale, setLocale, locales } = useTranslation();
  
  return (
    <div>
      <h1>{t('welcome')}</h1>
      <p>{t('greeting', { name: 'World' })}</p>
      
      {/* Rich text with components */}
      <Trans
        i18nKey="terms"
        components={{
          link: <a href="/terms" />,
          bold: <strong />,
        }}
      />
      
      {/* Locale switcher */}
      <select value={locale} onChange={e => setLocale(e.target.value)}>
        {locales.map(loc => (
          <option key={loc} value={loc}>{loc}</option>
        ))}
      </select>
    </div>
  );
}
```

### Vue

```vue
<script setup>
import { useI18n } from '@flightdev/i18n/vue';

const { t, locale, setLocale, locales } = useI18n();
</script>

<template>
  <h1>{{ t('welcome') }}</h1>
  <p>{{ t('greeting', { name: 'World' }) }}</p>
  
  <select v-model="locale" @change="setLocale(locale)">
    <option v-for="loc in locales" :key="loc" :value="loc">
      {{ loc }}
    </option>
  </select>
</template>
```

### Svelte

```svelte
<script>
  import { getI18n } from '@flightdev/i18n/svelte';
  
  const { t, locale, setLocale, locales } = getI18n();
</script>

<h1>{$t('welcome')}</h1>
<p>{$t('greeting', { name: 'World' })}</p>

<select bind:value={$locale} on:change={() => setLocale($locale)}>
  {#each $locales as loc}
    <option value={loc}>{loc}</option>
  {/each}
</select>
```

### Solid

```tsx
import { useI18n, Trans } from '@flightdev/i18n/solid';

function Content() {
  const { t, locale, setLocale, locales } = useI18n();
  
  return (
    <>
      <h1>{t('welcome')}</h1>
      <p>{t('greeting', { name: 'World' })}</p>
      
      <select value={locale()} onChange={e => setLocale(e.target.value)}>
        <For each={locales()}>
          {loc => <option value={loc}>{loc}</option>}
        </For>
      </select>
    </>
  );
}
```

---

## Formatting Utilities

Built-in formatters using the Intl API:

```typescript
import {
  formatNumber,
  formatCurrency,
  formatDate,
  formatTime,
  formatRelativeTime,
  formatList,
} from '@flightdev/i18n';

// Numbers
formatNumber(1234567.89, 'en-US');        // "1,234,567.89"
formatNumber(1234567.89, 'de-DE');        // "1.234.567,89"
formatNumber(0.75, 'en-US', { style: 'percent' });  // "75%"

// Currency
formatCurrency(99.99, 'USD', 'en-US');    // "$99.99"
formatCurrency(99.99, 'EUR', 'de-DE');    // "99,99 €"
formatCurrency(99.99, 'JPY', 'ja-JP');    // "￥100"

// Dates
formatDate(new Date(), 'en-US');          // "1/15/2026"
formatDate(new Date(), 'de-DE');          // "15.1.2026"
formatDate(new Date(), 'en-US', { dateStyle: 'full' });
// "Thursday, January 15, 2026"

// Time
formatTime(new Date(), 'en-US');          // "3:45 PM"
formatTime(new Date(), 'de-DE');          // "15:45"

// Relative time
formatRelativeTime(Date.now() - 3600000, 'en-US');  // "1 hour ago"
formatRelativeTime(Date.now() + 86400000, 'en-US'); // "in 1 day"

// Lists
formatList(['Apple', 'Banana', 'Cherry'], 'en-US');
// "Apple, Banana, and Cherry"
formatList(['Apple', 'Banana', 'Cherry'], 'es-ES');
// "Apple, Banana y Cherry"
```

---

## Pluralization

Automatic plural form selection based on count:

```json
// English (2 forms: one, other)
{
  "apple": "{{count}} apple",
  "apple_plural": "{{count}} apples"
}

// Russian (3 forms: one, few, many)
{
  "apple_one": "{{count}} яблоко",
  "apple_few": "{{count}} яблока",
  "apple_many": "{{count}} яблок"
}

// Arabic (6 forms)
{
  "apple_zero": "...",
  "apple_one": "...",
  "apple_two": "...",
  "apple_few": "...",
  "apple_many": "...",
  "apple_other": "..."
}
```

```typescript
t('apple', { count: 0 });   // "0 apples"
t('apple', { count: 1 });   // "1 apple"
t('apple', { count: 5 });   // "5 apples"
```

---

## Interpolation

Insert dynamic values into translations:

```json
{
  "greeting": "Hello, {{name}}!",
  "order": "Order #{{orderId}} placed on {{date, datetime}}",
  "price": "Total: {{amount, currency(USD)}}"
}
```

```typescript
t('greeting', { name: 'John' });
// "Hello, John!"

t('order', { orderId: '12345', date: new Date() });
// "Order #12345 placed on Jan 15, 2026"

t('price', { amount: 49.99 });
// "Total: $49.99"
```

---

## Namespace Support

Organize translations by feature:

```
locales/
  en/
    common.json     # Shared translations
    auth.json       # Login, signup, etc.
    dashboard.json  # Dashboard-specific
    errors.json     # Error messages
```

```typescript
const i18n = createI18n(adapter, {
  namespaces: ['common', 'auth', 'dashboard', 'errors'],
  defaultNamespace: 'common',
});

// Use namespace prefix
t('auth:login');           // From auth.json
t('dashboard:welcome');    // From dashboard.json
t('errors:not_found');     // From errors.json
t('greeting');             // From common.json (default)
```

---

## Loading Translations

### Static (Build-time)

Include all translations in the bundle:

```typescript
import en from './locales/en/common.json';
import es from './locales/es/common.json';

const i18n = createI18n(adapter, {
  resources: { en, es },
});
```

### Dynamic (Runtime)

Load translations on demand:

```typescript
const i18n = createI18n(adapter, {
  loadPath: '/locales/{{locale}}/{{namespace}}.json',
  
  // Preload specific locales
  preload: ['en'],
  
  // Load namespace on demand
  partialBundledLanguages: true,
});

// Load additional namespace
await i18n.loadNamespace('dashboard');
```

---

## SSR and Hydration

Avoid content flash by synchronizing server and client:

```typescript
// Server
export async function loader({ request }) {
  const locale = getLocaleFromRequest(request);
  await i18n.setLocale(locale);
  
  return {
    locale,
    translations: i18n.getResourceBundle(locale, 'common'),
  };
}

// Client hydration
const { locale, translations } = useLoaderData();

i18n.addResourceBundle(locale, 'common', translations);
await i18n.setLocale(locale);
```

### With React

```tsx
// entry-server.tsx
const html = renderToString(
  <I18nProvider i18n={i18n} locale={locale}>
    <App />
  </I18nProvider>
);

// entry-client.tsx
hydrateRoot(
  document,
  <I18nProvider i18n={i18n} locale={window.__LOCALE__}>
    <App />
  </I18nProvider>
);
```

---

## API Reference

### createI18n Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `locales` | `string[]` | required | Supported locales |
| `defaultLocale` | `string` | required | Fallback locale |
| `fallbackLocale` | `string` | defaultLocale | Missing key fallback |
| `namespaces` | `string[]` | `['translation']` | Translation namespaces |
| `defaultNamespace` | `string` | `'translation'` | Default namespace |
| `loadPath` | `string` | - | Path pattern for loading |
| `detection` | `object` | - | Locale detection config |

### i18n Instance Methods

| Method | Description |
|--------|-------------|
| `init()` | Initialize the instance |
| `t(key, options?)` | Translate a key |
| `setLocale(locale)` | Change current locale |
| `getLocale()` | Get current locale |
| `hasLocale(locale)` | Check if locale exists |
| `loadNamespace(ns)` | Load a namespace |
| `addResourceBundle(locale, ns, resources)` | Add translations |
| `getResourceBundle(locale, ns)` | Get translations |

---

## Creating Custom Adapters

Implement the `I18nAdapter` interface:

```typescript
import type { I18nAdapter } from '@flightdev/i18n';

export function myAdapter(config: MyConfig): I18nAdapter {
  return {
    name: 'my-adapter',
    
    async init() {
      // Initialize your library
    },
    
    t(key, options) {
      // Return translated string
    },
    
    async setLocale(locale) {
      // Change locale
    },
    
    getLocale() {
      // Return current locale
    },
    
    hasKey(key) {
      // Check if key exists
    },
    
    async loadNamespace(namespace) {
      // Load namespace translations
    },
  };
}
```

---

## License

MIT