npm package: @sejhey/react-i18n
Description: The SejHey i18n SDK for React
Author: Patrik Starck
Latest version: 0.7.0

# SejHey i18n for React

SejHey is a powerful and cost effective alternative to Crowdin, Phrase, Lokalise and other i18n platforms.

This library provides a full i18n solution for React applications, allowing you to easily manage translations and support multiple languages. Push translations to SejHey and fetch them dynamically in your React app. Collaborate with your team to manage translations efficiently. Publish changes instantly without redeploying your app.

To learn more about SejHey, visit [SejHey Docs](https://sejhey.com/docs/docs).

## ✨ Features

- React context provider via `SejHeyProvider`
- Translation hook `useTranslate`
- Component-based translation with `<T />`
- In-context editing support (Even in production)
- Static or CDN-based file loading
- Lazy language loading
- Custom loader component support
- SSR Support for Next.js

---

## 🚀 Installation

```bash
npm install @sejhey/react-i18n
```

---

## 🧩 Usage

### 1. Wrap your app in the `SejHeyProvider`

Minimal setup with CDN Loader and In Context Editor.

```tsx
import { SejheyI18n, SejHeyProvider, useTranslate, T } from '@sejhey/react-i18n'

/* Create the i18n instance */
const i18n = new SejheyI18n({ defaultLanguage: 'en' })
  //Fetch translations from SejHey CDN, always up to date. This will use cdn.sejhey.com/projects/**sejheyProjectId**/**otaEnvName**/manifest.json. Replace with your projectId. 
  .useCdnLoader({ sejheyProjectId: 'XXXX-XXXX' })
  //This enables in-context editing by adding ?in_context=true. In-context means that your project members can edit translations directly on the web page.
  .useInContextEditor({ sejheyProjectId: 'XXXX-XXXX' })
  //Place a language picker on your site automatically
  .useLanguagePicker()

/* Wrap you app with <SejHeyProvider> */
export default function MyApp() {
  return (
    <SejHeyProvider i18n={i18n}>
      <AnotherComponent />
    </SejHeyProvider>
  )
}

/* Now use translations like this */
const AnotherComponent = () => {
  const { t } = useTranslate()

  return (
    <>
      <h1>{t('welcome_message')}</h1>

      {/* Or if you prefer a component */}

      <T keyName='welcome_message' />
    </>
  )
}
```

You can also use static files as a backup or alternative to the CDN

```tsx
const en = () => import('../locales/en.json')
const fr = () => import('../locales/fr.json')

const i18n = new SejheyI18n({ defaultLanguage: 'en' })
...
  .useStaticLoader({ files: { en: en, fr: fr } }) //Treated as fallback if CdnLoader would fail
```

---

### 2. Pluralization and parameters

SejHey supports pluralization and parameters both for `useTranslate` and the `<T />` component.

```tsx
import { useTranslate } from '@sejhey/react-i18n'

export function Welcome() {
  const { t } = useTranslate()

  return (
    <>
      {/* Will be outputted as Patrik has one new message */}
      {t('welcome_message_plural', { count: 1, name: 'Patrik' })}

      {/* Same as above, but with T */}
      <T
        keyName='welcome_message_plural'
        params={{ count: 1, name: 'Patrik' }}
      />

      {/* Will be outputted as Anders has five new messages */}
      {t('welcome_message_plural', { count: 5, name: 'Anders' })}

      {/* Same as above, but with T */}
      <T
        keyName='welcome_message_plural'
        params={{ count: 5, name: 'Anders' }}
      />
    </>
  )
}
```

### 3. Next.js and SSR Support

SejHey provides full support for server-side rendering (SSR) in Next.js applications. This enables translations to be fetched and cached on your server during the initial render. This means that users will see the correct translations immediately, without any flickering or loading states.

SejHey also caches translations locally on your server for concurrent requests. This is implemented using a SWR (Stale While Revalidate) approach. This means that translation updates are propagated within a few seconds to your application without compromising any caching performance.

#### Understanding Next.js Routing

Next.js offers two routing systems:

- **Page Router** (Pages Directory): The original routing system using a `pages/` directory. Each file in `pages/` becomes a route. Uses `_app.tsx` for app-wide configuration and `getInitialProps` for SSR data fetching.
  
- **App Router** (App Directory): The newer routing system introduced in Next.js 13+ using an `app/` directory. Provides better performance with React Server Components, improved layouts, and built-in loading/error states. Uses `layout.tsx` for app-wide configuration and supports both Server and Client Components.

**Which one should you use?**
- Use **App Router** for new projects (Next.js 13.4+) as it's the recommended approach with better performance and features.
- Use **Page Router** if you're working with an existing Next.js project or need features that aren't yet available in App Router.

---

#### Page Router Setup

The Page Router uses the `pages/` directory structure and requires setup in `pages/_app.tsx`.

**Recommended File Structure:**
```
your-nextjs-app/
├── pages/
│   ├── _app.tsx          # Main app wrapper (setup SejHey here)
│   ├── index.tsx         # Home page
│   └── about/
│       └── index.tsx     # About page
└── package.json
```

**Setup Instructions:**

1. Create or update `pages/_app.tsx`:

```tsx
import type { AppContext, AppProps } from 'next/app'
import {
  SejheyI18n,
  SejHeyProvider,
  getSejHeyInitialProps
} from '@sejhey/react-i18n'

// Create the i18n instance
const i18n = new SejheyI18n({
  defaultLanguage: 'en'
})
  .useCdnLoader({ 
    sejheyProjectId: 'XXXX-XXXX', 
    otaEnvName: 'production' 
  })
  .useInContextEditor({ sejheyProjectId: 'XXXX-XXXX' })
  .useLanguagePicker()

// Define your App component
export default function MyApp({
  Component,
  pageProps,
  serialized
}: AppProps & { serialized?: any }) {
  return (
    <SejHeyProvider i18n={i18n} serialized={{serialized}}>
      <Component {...pageProps} />
    </SejHeyProvider>
  )
}

// Enable SSR - this fetches and caches translations on the server
MyApp.getInitialProps = async (appContext: AppContext) =>
  getSejHeyInitialProps(MyApp, appContext, i18n)
```

2. Use translations in your pages:

```tsx
// pages/index.tsx
import { useTranslate, T } from '@sejhey/react-i18n'

export default function Home() {
  const { t, changeLanguage } = useTranslate()

  return (
    <div>
      <h1>{t('welcome_message')}</h1>
      <T keyName='greeting' params={{ name: 'John' }} />
      <button onClick={() => changeLanguage('fr')}>
        Switch to French
      </button>
    </div>
  )
}
```

---

#### App Router Setup

The App Router uses the `app/` directory structure and requires a more modular setup with separate client and server components.

**Recommended File Structure:**
```
your-nextjs-app/
├── app/
│   ├── layout.tsx         # Root layout (uses SejHey server setup)
│   ├── page.tsx           # Home page
│   └── about/
│       └── page.tsx       # About page
├── sejhey/
│   ├── client.tsx         # Client-side SejHey wrapper
│   ├── server.tsx         # Server-side SejHey instance
│   ├── shared.tsx         # Shared i18n configuration
└── package.json
```

**Setup Instructions:**

1. Create `sejhey/shared.tsx` to define your i18n configuration:

```tsx
import { SejheyI18n } from '@sejhey/react-i18n'

export const getI18n = () => {
  return new SejheyI18n({
    defaultLanguage: 'en'
  })
    .useCdnLoader({ 
      sejheyProjectId: 'XXXX-XXXX', 
      otaEnvName: 'production' 
    })
    .useInContextEditor({ sejheyProjectId: 'XXXX-XXXX' })
    .useLanguagePicker({ position: 'bottom-right' })
}
```

2. Create `sejhey/server.tsx` for server-side usage:

```tsx
import { createServerInstance } from '@sejhey/react-i18n'
import { getI18n } from './shared'
import { headers } from 'next/headers'

const i18n = getI18n()

export const { T, translate, sejheyInstance } = await createServerInstance(
  i18n,
  { headers } //By providing headers to the server instance, the users preffered language will automatically be fetched from cookie or header.
)
```

3. Create `sejhey/client.tsx` for client-side provider:

```tsx
'use client' //This tells nextjs this is a "client component", see nextjs documentation for more info

import { SejHeyProvider, SejHeyServerSerializedInstance } from '@sejhey/react-i18n'
import { getI18n } from './shared'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'

const i18n = getI18n()

export default function ClientWrapper({ 
  children, 
  serialized 
}: { 
  children: React.ReactNode
  serialized: SejHeyServerSerializedInstance 
}) {
  const router = useRouter()

  useEffect(() => {
    // Ensure server components refresh after language change
    const unsubscribe = i18n.on('language_change_complete', () => {
      router.refresh()
    })
    return () => unsubscribe()
  }, [i18n, router])

  return (
    <SejHeyProvider i18n={i18n} serialized={serialized}>
      {children}
    </SejHeyProvider>
  )
}
```

4. Update `app/layout.tsx` (root layout):

```tsx
import { ReactNode } from 'react'
import ClientWrapper from '@/sejhey/client'
import { sejheyInstance } from '@/sejhey/server'
import { headers } from 'next/headers'

export default async function RootLayout({ 
  children 
}: { 
  children: ReactNode 
}) {
  // Initialize SejHey on the server with request headers
  await sejheyInstance.init(headers)
  const serialized = sejheyInstance.serialized()

  return (
    <html>
      <body>
        <ClientWrapper serialized={serialized}>
          {children}
        </ClientWrapper>
      </body>
    </html>
  )
}
```

5. Use translations in your pages:

**For Server Components** (default in App Router):

```tsx
// app/page.tsx
import { T, translate } from '@/sejhey/server'

export default async function Home() {
  // Server-side translation
  const welcomeText = translate('welcome_message')

  return (
    <div>
      <h1>{welcomeText}</h1>
      <T keyName='greeting' params={{ name: 'John' }} />
    </div>
  )
}
```

**For Client Components**:

```tsx
// app/client-page.tsx
'use client'

import { useTranslate, T } from '@sejhey/react-i18n'

export default function ClientPage() {
  const { t, changeLanguage } = useTranslate()

  return (
    <div>
      <h1>{t('welcome_message')}</h1>
      <T keyName='greeting' params={{ name: 'John' }} />
      <button onClick={() => changeLanguage('fr')}>
        Switch to French
      </button>
    </div>
  )
}
```


### 4. Changing language

You can change language easily by using the hook `useTranslate`. It can be done like this

```tsx
const MyComponent = () => {
  const { t, changeLanguage, availableLanguages, currentLanguage } =
    useTranslate()

  return <div onClick={() => changeLanguage('fr')}>Change to french</div>
}
```

## 📦 CDN Loader

The CDN Loader allows you to fetch translations from the SejHey CDN dynamically. This ensures that your translations are always up to date without requiring a full redeploy of your application. Translations from the CDN is hosted using Cloudflare edge servers. This means that they always are delivered with low latency and high availability.

You can set which `Environment` that the application should point towards. This enables you to have different environments for development, staging, and production. This is set by using the `otaEnvName` option in the CDN loader.

```tsx
const i18n = new SejheyI18n({ defaultLanguage: 'en' }).useCdnLoader({
  sejheyProjectId: 'XXXX-XXXX',
  otaEnvName: 'staging'
}) // Specific environment. Defined in SejHey dashboard.
```

To create a new environment, follow this instruction from within the SejHey dashboard. Note that the format must be i18n-JSON.

[![Creating key](https://sejhey.com/docs/assets/images/create_environment-6073178d71175921f552dd1174224fce.gif)](https://sejhey.com/docs/assets/images/create_environment-6073178d71175921f552dd1174224fce.gif)

---

## ✏️ In-Context Editor

You easily enable in the in-context editor by adding the `.useInContextEditor()` in the configuration. This enables in-context editing by adding `?in_context=true` to the URL. The in-context editor allow translations to be edited on the web-page directly.

**Note:** When In-Context is enabled, the translators must still authenticate themselves towards SejHey and translations can only be edited if the user has the appropriate permissions.

You can customize the query parameter to enable in-context by providing your own parameter name:

```tsx
.useInContextEditor({ sejheyProjectId: 'XXXX-XXXX', enableByQueryParam: 'my_custom_param' })
```

This would enable in_context by adding `?my_custom_param=true` to the URL.

### Creating keys

If the logged in user has the appropriate permissions, they can create new translation keys directly from the in-context editor. This allows for a more streamlined workflow, as translators can add missing keys on the fly without needing to switch back to the main application.

---

## Language detection

By default, language detection is enabled in this plugin. The default order is defined as. `"querystring"`,`"nextjs"`, `"cookie"`, `"localStorage"`, `"path"`, `"subdomain"`, `"htmlTag"`, `"navigator"`. This can be customized or disabled, see `languageDetectionSettings` in the [API documentation](#new-sejheyi18nconfig) for reference.

---

## 📚 Namespaces

Namespaces allow you to organize your translations into logical groups. This is useful for large applications where you want to split translations by feature, module, or component. Namespaces help reduce bundle size by loading only the translations you need.

### Setting namespaces at initialization

You can specify which namespaces to load when creating the i18n instance:

```tsx
const i18n = new SejheyI18n({
  defaultLanguage: 'en',
  loadedNamespaces: ['common', 'dashboard', 'settings']
})
  .useCdnLoader({ sejheyProjectId: 'XXXX-XXXX' })
```

All specified namespaces will be loaded when the i18n instance is initialized or when the language changes.

### Adding and removing namespaces dynamically

You can add or remove namespaces at runtime using the i18n instance methods:

```tsx
// Add a namespace (will be loaded immediately)
await i18n.addNamespace('notifications')

// Remove a namespace
i18n.removeNamespace('settings')
```

**Note:** When using namespaces with React, you'll need to access the i18n instance. You can do this by storing the instance in a variable that's accessible to your components, or by using a context if needed.

### Using namespaced translation keys

There are two ways to use namespaces with translation keys:

#### Option 1: Pass namespace to `useTranslate` hook

You can pass a namespace directly to the `useTranslate` hook. This will automatically prefix all translation keys with that namespace:

```tsx
// All keys that will belongs to the namespace common will be available
const { t } = useTranslate('common')

// No need to prefix the key - it's automatically added
{t('welcome_message')}  // Translates to 'common:welcome_message'
{t('greeting', { name: 'John' })}  // Translates to 'common:greeting'

```


### Example: Lazy loading namespaces

You can load namespaces on-demand for better performance. The `useTranslate` hook will automatically add the namespace when you pass it:

```tsx
// Create i18n instance in a separate file or at module level
const i18n = new SejheyI18n({
  defaultLanguage: 'en',
  loadedNamespaces: ['common'] // Load common namespace initially
})
  .useCdnLoader({ sejheyProjectId: 'XXXX-XXXX' })

// In your component - namespace is automatically loaded when used
const MyComponent = () => {
  // Passing 'notifications' will automatically add it to namespaces to load
  const { t } = useTranslate('notifications')

  return (
    <div>
      {/* No need to prefix with 'notifications:' - it's automatic */}
      <p>{t('new_message')}</p>
      <p>{t('unread_count', { count: 5 })}</p>
    </div>
  )
}
```

Alternatively, you can manually manage namespaces:

```tsx
// Create i18n instance in a separate file or at module level
const i18n = new SejheyI18n({
  defaultLanguage: 'en',
  loadedNamespaces: ['common']
})
  .useCdnLoader({ sejheyProjectId: 'XXXX-XXXX' })

// In your component
const MyComponent = () => {
  const { t } = useTranslate()
  const [namespaceLoaded, setNamespaceLoaded] = useState(false)

  const loadNotifications = async () => {
    await i18n.addNamespace('notifications')
    setNamespaceLoaded(true)
  }

  return (
    <div>
      {!namespaceLoaded && (
        <button onClick={loadNotifications}>Load notifications</button>
      )}
      {namespaceLoaded && (
        <p>{t('new_message')}</p>
      )}
    </div>
  )
}
```

---

## 🗂 Static Translations File Format

If you provide static translation files, they must follow flat JSON-i18n format, e.g.:

```json
{
  "welcome_message": "Welcome to SejHey!",

  //If params are present
  "greeting_message": "Hello, {{name}}!",

  //If plural use suffix _one, _other etc, and the variable {{count}}
  "welcome_message_plural_one": "You have one message",
  "welcome_message_plural_other": "You have {{count}} messages"
}
```

If you are exporting files from SejHey, this option format is called i18n JSON.

**Note:** When using namespaces, translation keys in your JSON files should be prefixed with the namespace and a colon (e.g., `"common:welcome_message"`). However, when using the CDN loader, SejHey will automatically organize translations by namespace, so you don't need to prefix keys in the exported files.

---

## 📘 API Reference: SejheyI18n

### `new SejheyI18n(config)`

Creates a new SejHey i18n instance.

**Parameters:**

- **`config: DefaultConfig`**
  - `defaultLanguage` (**string**, required): The initial language to use (e.g., `"en"`).
  - `loadedNamespaces` (**string[]**, optional): Array of namespace names to load initially. These namespaces will be fetched when the i18n instance is initialized or when the language changes. Example: `['common', 'dashboard']`.
  - `languageDetectionSettings` (**LanguageDetectorConfig**, optional):
    - `detectionOrder?: DetectorAlternative[]` — Detection order: `"querystring"`,`"nextjs"`, `"cookie"`, `"localStorage"`, `"path"`, `"subdomain"`, `"htmlTag"`, `"navigator"`. You can customize which ways the plugin uses to detect the current language of their choice.
    - `queryParamName?: string` — Name of query parameter to detect language if **querystring** is used. Defaults to **locale**. Eg `?locale=de`.
    - `disable?: boolean` — Disable detection entirely.
  - `fallbackSettings` (**FallbackConfig**, optional):
    - `show`: `'key' | 'empty' | 'fallback_language' ` — What to display if translation is missing.
    - `fallbackLanguage?: string` — Language to use as fallback.

---

### **Chainable Instance Methods**

The `SejheyI18n` instance implements these methods to configure the functionality:

#### `.useCdnLoader(options?: CdnLoaderOptions)`

Fetch translations dynamically from **SejHey CDN**.

**Options:**

- `sejheyProjectId` (**string**, required if using SejHey CDN): Your SejHey project ID (found in your SejHey project under Settings & More -> Settings).
- `otaEnvName` (**string**, optional): Environment name (e.g., `"production"`, `"staging"`). This is defined from the SejHey project under `Export & Download -> OTA -> Environments`. If not provided, the `default` env will be used.
- `baseUrl` (**string**, optional): Custom base URL for CDN. If provided, `sejheyProjectId` and `otaEnvName` should not be used.

```ts
// Using SejHey CDN
i18n.useCdnLoader({ sejheyProjectId: 'XXXX-XXXX', otaEnvName: 'production' })

// Using custom base URL
i18n.useCdnLoader({ baseUrl: 'https://cdn.example.com/translations/' })
```

---

#### `.useStaticLoader(options: { files: StaticFileLoader })`

Define fallback static translations (bundled with your app).

- `files`: Object where keys are language codes and values are:

  - A function returning `Promise<FlatJson>` (lazy import)
  - Or a `Promise<FlatJson>` directly.

```ts
i18n.useStaticLoader({
  files: {
    en: () => import('../locales/en.json'),
    fr: () => import('../locales/fr.json')
  }
})
```

---

#### `.useInContextEditor(options: InContextEditorSettings)`

Enable in-context editing for translators.

**Note**: By enabling this, you do not increase the bundle size. This part of the bundle is dynamically loaded only when needed.

**Options:**

- `sejheyProjectId` (**string**, required): Your SejHey project ID (found in your SejHey project under Settings & More -> Settings).
- `enableByQueryParam?: string` — Query parameter that activates the editor (defaults to `"in_context"`). E.g., `?in_context=true`.

```ts
i18n.useInContextEditor({
  sejheyProjectId: 'XXXX-XXXX',
  enableByQueryParam: 'in_context'
})
```

---

#### `.useLanguagePicker(options?: LanguagePickerSettings)`

Automatically render a floating **language picker** on your site.

- `position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'`
- `onChangeLanguage?: (lang: string) => void`
- `fixedLocales?: string[]` — Restrict available locales.

```ts
i18n.useLanguagePicker({ position: 'bottom-right' })
```

---

#### `.addNamespace(namespace: string): Promise<void>`

Dynamically add a namespace to be loaded. The namespace will be fetched immediately for the current language.

**Parameters:**
- `namespace` (**string**, required): The namespace name to add (e.g., `"notifications"`).

```ts
await i18n.addNamespace('notifications')
```

---

#### `.removeNamespace(namespace: string): void`

Remove a namespace from the list of namespaces to fetch. This only removes one occurrence if the namespace was added multiple times.

**Parameters:**
- `namespace` (**string**, required): The namespace name to remove.

```ts
i18n.removeNamespace('settings')
```

---

### `<SejHeyProvider />`

React provider to wrap your app.

**Props:**

- `i18n` (**ISejHeyCoreWrapper**, required): Instance created via `new SejheyI18n()`.
- `serialized?: any`: Preloaded translations (e.g., from SSR). See more under **getSejHeyInitialProps**.
- `children: React.ReactNode`: Your app components.
- `suspenseComponent?: React.ReactNode`: Optional loading fallback (e.g., spinner) that is shown before translations are loaded.

Example:

```tsx
<SejHeyProvider i18n={i18n}>
  <App />
</SejHeyProvider>
```

---

### `useTranslate(namespace?: string)`

React hook returning translation utilities.

**Parameters:**

- `namespace` (**string**, optional): If provided, all translation keys will be automatically prefixed with this namespace. The namespace will also be added to the list of namespaces to load if not already loaded.

**Returns:**

- `t(key: string, params?: OptionsParams, defaultValue?: string, context?: string): string` — Translation function. If a namespace was provided to `useTranslate`, keys without a `:` will be automatically prefixed with that namespace.
- `changeLanguage(lang: string): void` — Change language dynamically.
- `currentLanguage: string` — Active language code.
- `availableLanguages: string[]` — All available languages.

Examples:

```tsx
// Without namespace
const { t, changeLanguage } = useTranslate()
return <p>{t('welcome_message', { name: 'John' })}</p>

// With namespace - keys are automatically prefixed
const { t } = useTranslate('common')
return <p>{t('welcome_message')}</p>  // Translates 'common:welcome_message'

// You can still use fully qualified keys
const { t } = useTranslate('common')
return <p>{t('dashboard:user_count', { count: 5 })}</p>  // Uses 'dashboard:user_count' as-is
```

---

### `<T />` Component

JSX component alternative to `t()` function.

**Props:**

- `keyName: string` — Translation key.
- `params?: OptionsParams` — Dynamic variables `{ name: 'John' }`.
- `defaultValue?: string` — Fallback if missing.

Example:

```tsx
<T keyName='welcome_message' params={{ name: 'John' }} />
```

---

### `getSejHeyInitialProps(App, appContext, i18n)`

**Note:** This function is specifically for Next.js **Page Router** (Pages Directory). For App Router setups, use `createServerInstance` as shown in the [App Router Setup](#app-router-setup) section above.

For SSR in Page Router, preloads translations on the server.

**Parameters:**

- `App` (**Component**, required): The Next.js App component.
- `appContext` (**AppContext**, required): The Next.js app context.
- `i18n` (**SejheyI18n**, required): The SejHey i18n instance.

```ts
const i18n = new SejheyI18n({ defaultLanguage: 'en' })
  .useCdnLoader({ sejheyProjectId: 'XXXX-XXXX', otaEnvName: 'production' })

export default function MyApp({ Component, pageProps, serialized }: { Component: any, pageProps: any, serialized?: any }) {
  return (
    <SejHeyProvider i18n={i18n} serialized={serialized}>
      <Component {...pageProps} />
    </SejHeyProvider>
  );
}
MyApp.getInitialProps = async (appContext: import('next/app').AppContext) => 
  getSejHeyInitialProps(MyApp, appContext, i18n);
```

See the [Page Router Setup](#page-router-setup) section above for complete setup instructions.

---

### `createServerInstance(i18n, options)`

**Note:** This function is specifically for Next.js **App Router** (App Directory). For Page Router setups, use `getSejHeyInitialProps` as shown in the [Page Router Setup](#page-router-setup) section above.

Creates a server-side instance for App Router that enables SSR support. This should be used in Server Components and the root layout.

**Parameters:**

- `i18n` (**SejheyI18n**, required): The SejHey i18n instance created with `new SejheyI18n()`.
- `options` (**object**, required):
  - `headers`: Next.js headers function (from `next/headers`). Used for language detection from request headers.

**Returns:**

An object with:
- `T`: Server Component for translations
- `translate(key: string, params?: OptionsParams): string`: Server-side translation function
- `sejheyInstance`: Server instance with methods:
  - `init(headers)`: Initialize and fetch translations (call in layout)
  - `serialized()`: Get serialized translations for client hydration

```ts
// sejhey/server.tsx
import { createServerInstance } from '@sejhey/react-i18n'
import { getI18n } from './shared'
import { headers } from 'next/headers'

const i18n = getI18n()

export const { T, translate, sejheyInstance } = await createServerInstance(
  i18n,
  { headers }
)
```

See the [App Router Setup](#app-router-setup) section above for complete setup instructions.

---

## 🔧 Example Usage

For complete Next.js setup instructions, see the [Next.js and SSR Support](#3-nextjs-and-ssr-support) section above, which includes detailed guides for both:
- [Page Router Setup](#page-router-setup) - For Next.js projects using the `pages/` directory
- [App Router Setup](#app-router-setup) - For Next.js 13+ projects using the `app/` directory

**Quick Example (Page Router):**

```ts
import {
  SejheyI18n,
  SejHeyProvider,
  getSejHeyInitialProps
} from '@sejhey/react-i18n'

const en = () => import('../locales/en.json')
const fr = () => import('../locales/fr.json')

const i18n = new SejheyI18n({
  defaultLanguage: 'en'
})
  .useCdnLoader({ sejheyProjectId: 'XXXX-XXXX', otaEnvName: 'production' })
  .useStaticLoader({ files: { en, fr } })
  .useInContextEditor({ sejheyProjectId: 'XXXX-XXXX' })
  .useLanguagePicker()

export default function MyApp({
  Component,
  pageProps,
  serialized
}: {
  Component: any
  pageProps: any
  serialized?: any
}) {
  return (
    <SejHeyProvider i18n={i18n} serialized={serialized}>
      <Component {...pageProps} />
    </SejHeyProvider>
  )
}

MyApp.getInitialProps = async (appContext: import('next/app').AppContext) =>
  getSejHeyInitialProps(MyApp, appContext, i18n)
```

For App Router examples, see the [App Router Setup](#app-router-setup) section above.

---

## License

This project is licensed under the MIT License.