home / skills / jonathanbelolo / composable-svelte / composable-svelte-i18n

composable-svelte-i18n skill

/.claude/skills/composable-svelte-i18n

This skill helps implement multilingual interfaces in Composable Svelte by providing centralized i18n state, formatters, and SSR-friendly locale handling.

npx playbooks add skill jonathanbelolo/composable-svelte --skill composable-svelte-i18n

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
21.7 KB
---
name: composable-svelte-i18n
description: Internationalization (i18n) system for Composable Svelte. Use when implementing multi-language support, translations, date/number formatting, locale detection, or localizing applications. Covers translation loading, ICU MessageFormat, formatters (dates/numbers/currency), locale detection, SSR integration, and the i18n reducer.
---

# Composable Svelte Internationalization (i18n)

Complete i18n solution integrated with the Composable Architecture. Handles translations, formatting (dates, numbers, currency), locale detection, and works seamlessly with SSR/SSG.

---

## CRITICAL RULES

### Rule 1: i18n State Lives in Store

**Principle**: i18n state is part of your application state, integrated via the i18n reducer.

#### ✅ CORRECT - i18n in Store State
```typescript
import { createInitialI18nState, i18nReducer } from '@composable-svelte/core/i18n';

interface AppState {
  todos: Todo[];
  i18n: I18nState;  // ✅ i18n integrated with app state
}

type AppAction =
  | { type: 'addTodo'; text: string }
  | I18nAction;  // ✅ i18n actions part of app actions

const appReducer: Reducer<AppState, AppAction> = (state, action, deps) => {
  // Handle i18n actions
  if (typeof action.type === 'string' && action.type.startsWith('i18n/')) {
    const [newI18nState, i18nEffect] = i18nReducer(state.i18n, action as any, deps);
    return [
      { ...state, i18n: newI18nState },
      i18nEffect as Effect<AppAction>
    ];
  }

  // Handle other actions...
  return [state, Effect.none()];
};
```

#### ❌ WRONG - Separate i18n Store
```typescript
// ❌ Don't create a separate store for i18n
const i18nStore = createStore({
  initialState: i18nState,
  reducer: i18nReducer
});

const appStore = createStore({
  initialState: appState,
  reducer: appReducer
});
```

**WHY**: Single source of truth. i18n changes should flow through the same reducer pipeline as other state changes.

---

### Rule 2: Use Framework Formatters, Not Manual Formatting

**Principle**: The framework provides locale-aware formatters. Never manually format dates, numbers, or currency.

#### ✅ CORRECT - Framework Formatters
```svelte
<script lang="ts">
  import { createTranslator, createFormatters } from '@composable-svelte/core/i18n';

  const t = $derived(createTranslator($store.i18n, 'common'));
  const formatters = $derived(createFormatters($store.i18n));
</script>

<div>
  <p>{t('welcome', { name: 'Alice' })}</p>
  <p>{formatters.date(post.date)}</p>
  <p>{formatters.number(1234.56)}</p>
  <p>{formatters.currency(99.99, 'USD')}</p>
</div>
```

#### ❌ WRONG - Manual Formatting
```svelte
<!-- ❌ Don't manually format dates -->
<p>{new Date(post.date).toLocaleDateString()}</p>

<!-- ❌ Don't manually format numbers -->
<p>{price.toFixed(2)}</p>

<!-- ❌ Don't manually construct currency -->
<p>${price}</p>
```

**WHY**: Manual formatting doesn't respect locale conventions. Formatters automatically handle regional differences (e.g., "January 5, 2025" vs "5 janvier 2025", "1,234.56" vs "1 234,56").

---

## CORE CONCEPTS

### 1. Translation System

Use `createTranslator()` for accessing translations. It handles interpolation, ICU MessageFormat, and fallback chains automatically.

```svelte
<script lang="ts">
  import { createTranslator } from '@composable-svelte/core/i18n';

  const t = $derived(createTranslator($store.i18n, 'common'));
</script>

<div>
  <!-- Simple translation -->
  <h1>{t('app.title')}</h1>

  <!-- With interpolation -->
  <p>{t('welcome', { name: 'Alice' })}</p>

  <!-- ICU MessageFormat (pluralization) -->
  <p>{t('items', { count: 5 })}</p>
  <!-- Translation: "{count, plural, one {# item} other {# items}}" -->
  <!-- Output: "5 items" -->
</div>
```

**Translation File Example** (`locales/en/common.json`):
```json
{
  "app.title": "My Application",
  "welcome": "Welcome, {name}!",
  "items": "{count, plural, one {# item} other {# items}}",
  "greeting": "{gender, select, male {Hello Mr. {name}} female {Hello Ms. {name}} other {Hello {name}}}"
}
```

---

### 2. Formatters (Dates, Numbers, Currency)

Use `createFormatters()` to get locale-bound formatters. All formatters automatically use the current locale.

```svelte
<script lang="ts">
  import { createFormatters, DateFormats, NumberFormats } from '@composable-svelte/core/i18n';

  const formatters = $derived(createFormatters($store.i18n));
</script>

<div>
  <!-- Date formatting -->
  <p>{formatters.date(post.date)}</p>
  <!-- en: "January 5, 2025" -->
  <!-- fr: "5 janvier 2025" -->
  <!-- es: "5 de enero de 2025" -->

  <!-- Date with custom options -->
  <p>{formatters.date(post.date, DateFormats.short)}</p>
  <!-- en: "1/5/25" -->
  <!-- fr: "05/01/2025" -->

  <!-- Number formatting -->
  <p>{formatters.number(1234.56)}</p>
  <!-- en: "1,234.56" -->
  <!-- fr: "1 234,56" -->
  <!-- de: "1.234,56" -->

  <!-- Currency formatting -->
  <p>{formatters.currency(99.99, 'USD')}</p>
  <!-- en-US: "$99.99" -->
  <!-- fr: "99,99 $US" -->
  <!-- de: "99,99 $" -->

  <!-- Relative time -->
  <p>{formatters.relativeTime(yesterday)}</p>
  <!-- en: "yesterday" -->
  <!-- fr: "hier" -->
  <!-- es: "ayer" -->
</div>
```

**Available DateFormats**:
- `DateFormats.short` - "1/5/25"
- `DateFormats.medium` - "Jan 5, 2025"
- `DateFormats.long` - "January 5, 2025" (default)
- `DateFormats.full` - "Monday, January 5, 2025"

---

### 3. Locale Detection

The framework provides three locale detectors for different environments.

#### Browser (Client-Side)
```typescript
import { createBrowserLocaleDetector } from '@composable-svelte/core/i18n';

const localeDetector = createBrowserLocaleDetector({
  supportedLocales: ['en', 'fr', 'es'],
  defaultLocale: 'en',
  cookieName: 'locale',  // Optional: persist preference
  storageKey: 'user_locale'  // Optional: localStorage key
});

const locale = localeDetector.detect();
// Checks: 1. localStorage, 2. cookie, 3. navigator.language, 4. default
```

#### SSR (Server-Side Rendering)
```typescript
import { createSSRLocaleDetector } from '@composable-svelte/core/i18n';

// In your server handler
const localeDetector = createSSRLocaleDetector({
  supportedLocales: ['en', 'fr', 'es'],
  defaultLocale: 'en',
  url: request.url,  // For ?lang=fr query param
  cookies: request.headers.get('cookie') ?? '',
  acceptLanguage: request.headers.get('accept-language') ?? ''
});

const locale = localeDetector.detect();
// Checks: 1. URL (?lang=fr), 2. cookie, 3. Accept-Language, 4. default
```

#### SSG (Static Site Generation)
```typescript
import { createStaticLocaleDetector } from '@composable-svelte/core/i18n';

// For pre-rendering specific locale
const localeDetector = createStaticLocaleDetector('fr', ['en', 'fr', 'es']);
const locale = localeDetector.detect();  // Always returns 'fr'
```

---

### 4. Translation Loading

Three strategies for loading translations based on your use case.

#### Fetch Loader (Client-Side, Lazy Loading)
```typescript
import { FetchTranslationLoader } from '@composable-svelte/core/i18n';

const translationLoader = new FetchTranslationLoader({
  baseUrl: '/locales',  // Fetches from /locales/{locale}/{namespace}.json
  supportedLocales: ['en', 'fr', 'es']
});
```

**File Structure:**
```
public/
  locales/
    en/
      common.json
      products.json
    fr/
      common.json
      products.json
```

#### Bundled Loader (SSR, Build-Time)
```typescript
import { BundledTranslationLoader } from '@composable-svelte/core/i18n';

// Import translations at build time
import enCommon from '../locales/en/common.json';
import frCommon from '../locales/fr/common.json';

const translationLoader = new BundledTranslationLoader({
  bundles: {
    'en': { common: enCommon },
    'fr': { common: frCommon }
  }
});
```

#### Glob Loader (Vite-Specific, Auto-Discovery)
```typescript
import { createGlobLoader } from '@composable-svelte/core/i18n';

const translationLoader = createGlobLoader(
  import.meta.glob('/src/locales/*/*.json')
);
```

---

## COMPLETE SETUP EXAMPLES

### Client-Only Application

```typescript
// src/store.ts
import { createStore } from '@composable-svelte/core';
import {
  createInitialI18nState,
  i18nReducer,
  createBrowserLocaleDetector,
  FetchTranslationLoader,
  browserDOM
} from '@composable-svelte/core/i18n';

// Detect locale
const localeDetector = createBrowserLocaleDetector({
  supportedLocales: ['en', 'fr', 'es'],
  defaultLocale: 'en',
  storageKey: 'app_locale'
});

const initialLocale = localeDetector.detect();

// Initialize i18n state
const i18nState = createInitialI18nState(initialLocale, ['en', 'fr', 'es'], 'en');

const initialState: AppState = {
  todos: [],
  i18n: i18nState
};

// Create store with i18n dependencies
const store = createStore({
  initialState,
  reducer: appReducer,
  dependencies: {
    translationLoader: new FetchTranslationLoader({
      baseUrl: '/locales',
      supportedLocales: ['en', 'fr', 'es']
    }),
    localeDetector,
    storage: localStorage,
    dom: browserDOM
  }
});

// Load initial translations
store.dispatch({
  type: 'i18n/loadNamespace',
  namespace: 'common'
});
```

```svelte
<!-- src/App.svelte -->
<script lang="ts">
  import { createTranslator, createFormatters } from '@composable-svelte/core/i18n';

  const t = $derived(createTranslator($store.i18n, 'common'));
  const formatters = $derived(createFormatters($store.i18n));

  function switchLanguage(locale: string) {
    store.dispatch({
      type: 'i18n/setLocale',
      locale,
      preloadNamespaces: ['common']
    });
  }
</script>

<div>
  <h1>{t('app.title')}</h1>

  <select value={$store.i18n.currentLocale} onchange={(e) => switchLanguage(e.currentTarget.value)}>
    <option value="en">English</option>
    <option value="fr">Français</option>
    <option value="es">Español</option>
  </select>

  <p>{t('welcome', { name: 'User' })}</p>
  <p>{formatters.date(new Date())}</p>
</div>
```

---

### SSR Application

**Server Setup:**
```typescript
// src/server/index.ts
import { renderToHTML } from '@composable-svelte/core/ssr';
import {
  createInitialI18nState,
  BundledTranslationLoader,
  createSSRLocaleDetector,
  serverDOM
} from '@composable-svelte/core/i18n';

// Import translations
import enCommon from '../locales/en/common.json';
import frCommon from '../locales/fr/common.json';

app.get('*', async (request, reply) => {
  // Detect locale from request
  const localeDetector = createSSRLocaleDetector({
    supportedLocales: ['en', 'fr'],
    defaultLocale: 'en',
    url: request.url,
    cookies: request.headers.cookie ?? '',
    acceptLanguage: request.headers['accept-language'] ?? ''
  });

  const locale = localeDetector.detect();

  // Initialize i18n for this request
  const i18nState = createInitialI18nState(locale, ['en', 'fr'], 'en');

  const initialState: AppState = {
    todos: [],
    i18n: i18nState
  };

  // Render with i18n dependencies
  const html = await renderToHTML(App, {
    initialState,
    reducer: appReducer,
    dependencies: {
      translationLoader: new BundledTranslationLoader({
        bundles: {
          'en': { common: enCommon },
          'fr': { common: frCommon }
        }
      }),
      localeDetector,
      storage: new Map(),  // SSR-safe storage
      dom: serverDOM
    }
  });

  reply.type('text/html').send(html);
});
```

**Client Hydration:**
```typescript
// src/client/index.ts
import { hydrateStore } from '@composable-svelte/core/ssr';
import {
  FetchTranslationLoader,
  createBrowserLocaleDetector,
  browserDOM
} from '@composable-svelte/core/i18n';

const store = hydrateStore(
  document.getElementById('__COMPOSABLE_SVELTE_STATE__')!.textContent,
  {
    reducer: appReducer,
    dependencies: {
      translationLoader: new FetchTranslationLoader({
        baseUrl: '/locales',
        supportedLocales: ['en', 'fr']
      }),
      localeDetector: createBrowserLocaleDetector({
        supportedLocales: ['en', 'fr'],
        defaultLocale: 'en'
      }),
      storage: localStorage,
      dom: browserDOM
    }
  }
);
```

---

## ICU MessageFormat

The framework automatically detects and compiles ICU MessageFormat syntax for advanced translations.

### Pluralization
```json
{
  "items": "{count, plural, =0 {No items} one {# item} other {# items}}"
}
```

```typescript
t('items', { count: 0 });   // "No items"
t('items', { count: 1 });   // "1 item"
t('items', { count: 5 });   // "5 items"
```

### Selection (Gender, etc.)
```json
{
  "greeting": "{gender, select, male {Hello Mr. {name}} female {Hello Ms. {name}} other {Hello {name}}}"
}
```

```typescript
t('greeting', { gender: 'male', name: 'Smith' });    // "Hello Mr. Smith"
t('greeting', { gender: 'female', name: 'Johnson' }); // "Hello Ms. Johnson"
t('greeting', { gender: 'other', name: 'Taylor' });  // "Hello Taylor"
```

### Nested Plurals
```json
{
  "cart": "{itemCount, plural, one {You have # item} other {You have # items}} in {cartCount, plural, one {# cart} other {# carts}}"
}
```

```typescript
t('cart', { itemCount: 1, cartCount: 1 });  // "You have 1 item in 1 cart"
t('cart', { itemCount: 5, cartCount: 2 });  // "You have 5 items in 2 carts"
```

---

## COMMON PATTERNS

### Pattern 1: Language Switcher

```svelte
<script lang="ts">
  const availableLocales = $derived($store.i18n.availableLocales);
  const currentLocale = $derived($store.i18n.currentLocale);

  const languageNames: Record<string, string> = {
    en: 'English',
    fr: 'Français',
    es: 'Español'
  };

  function switchLanguage(locale: string) {
    store.dispatch({
      type: 'i18n/setLocale',
      locale,
      preloadNamespaces: ['common']  // Load common translations immediately
    });
  }
</script>

<div class="language-switcher">
  {#each availableLocales as locale}
    <button
      class:active={locale === currentLocale}
      onclick={() => switchLanguage(locale)}
    >
      {languageNames[locale]}
    </button>
  {/each}
</div>
```

### Pattern 2: Lazy Loading Namespaces

Load translations on-demand to reduce initial bundle size.

```typescript
// Load translations when user navigates to products page
function navigateToProducts() {
  store.dispatch({
    type: 'i18n/loadNamespace',
    namespace: 'products'
  });

  // Navigate after translations loaded (via effect)
  store.dispatch({
    type: 'navigate',
    destination: { type: 'products' }
  });
}
```

**In Reducer:**
```typescript
case 'i18n/namespaceLoaded': {
  // Translation loaded, safe to navigate
  if (action.namespace === 'products') {
    return [state, Effect.none()];
  }
  return [state, Effect.none()];
}
```

### Pattern 3: Formatted Lists

```svelte
<script lang="ts">
  const authors = ['Alice', 'Bob', 'Carol'];

  // The framework uses Intl.ListFormat automatically
  const formattedAuthors = $derived(
    new Intl.ListFormat($store.i18n.currentLocale, {
      style: 'long',
      type: 'conjunction'
    }).format(authors)
  );
</script>

<p>Authors: {formattedAuthors}</p>
<!-- en: "Authors: Alice, Bob, and Carol" -->
<!-- fr: "Authors: Alice, Bob et Carol" -->
<!-- es: "Authors: Alice, Bob y Carol" -->
```

---

## BEST PRACTICES

### 1. Organize Translation Files by Namespace

```
locales/
  en/
    common.json         # UI labels, buttons, navigation
    products.json       # Product-related translations
    errors.json         # Error messages
    validation.json     # Form validation messages
  fr/
    common.json
    products.json
    ...
```

**Benefits:**
- Lazy load translations per feature
- Easier maintenance
- Smaller initial bundle

### 2. Use Descriptive Keys

```json
{
  "nav.home": "Home",
  "nav.products": "Products",
  "products.addToCart": "Add to Cart",
  "products.outOfStock": "Out of Stock",
  "validation.required": "This field is required"
}
```

**WHY**: Namespaced keys make it clear where translations are used and prevent conflicts.

### 3. Provide Context in Plurals

```json
{
  "cart.items": "{count, plural, =0 {Your cart is empty} one {# item in cart} other {# items in cart}}"
}
```

**WHY**: The `=0` case lets you provide a more natural message than "0 items".

### 4. Keep Formatters as Derived Values

```svelte
<script lang="ts">
  // ✅ CORRECT - Reactive to locale changes
  const formatters = $derived(createFormatters($store.i18n));

  // ❌ WRONG - Won't update when locale changes
  const formatters = createFormatters($store.i18n);
</script>
```

### 5. Test with Multiple Locales

```typescript
import { TestStore } from '@composable-svelte/core/test';

test('displays localized date', async () => {
  const store = new TestStore({
    initialState: {
      post: { date: '2025-01-05' },
      i18n: createInitialI18nState('fr', ['en', 'fr'])
    },
    reducer: appReducer,
    dependencies: { /* ... */ }
  });

  const formatters = createFormatters(store.state.i18n);
  expect(formatters.date(store.state.post.date)).toBe('5 janvier 2025');
});
```

---

## COMMON GOTCHAS

### Gotcha 1: Not Awaiting Translation Load

```typescript
// ❌ WRONG - Translations not loaded yet
store.dispatch({ type: 'i18n/setLocale', locale: 'fr' });
const t = createTranslator(store.state.i18n, 'common');  // May not have French translations!

// ✅ CORRECT - Wait for load or preload
store.dispatch({
  type: 'i18n/setLocale',
  locale: 'fr',
  preloadNamespaces: ['common']  // Loads before completing
});
```

### Gotcha 2: Forgetting to Handle i18n Actions in Reducer

```typescript
// ❌ WRONG - i18n actions not handled
const appReducer: Reducer<AppState, AppAction> = (state, action, deps) => {
  switch (action.type) {
    case 'addTodo':
      return [/* ... */];
    // Missing i18n handling!
  }
};

// ✅ CORRECT - Handle i18n actions
const appReducer: Reducer<AppState, AppAction> = (state, action, deps) => {
  // Handle i18n actions first
  if (typeof action.type === 'string' && action.type.startsWith('i18n/')) {
    const [newI18nState, i18nEffect] = i18nReducer(state.i18n, action as any, deps);
    return [
      { ...state, i18n: newI18nState },
      i18nEffect as Effect<AppAction>
    ];
  }

  switch (action.type) {
    case 'addTodo':
      return [/* ... */];
  }
};
```

### Gotcha 3: Using Wrong Storage on Server

```typescript
// ❌ WRONG - localStorage doesn't exist on server
const deps = {
  storage: localStorage,  // Crashes in SSR!
  dom: serverDOM
};

// ✅ CORRECT - Use SSR-safe storage
const deps = {
  storage: new Map(),  // Or mock storage
  dom: serverDOM
};
```

### Gotcha 4: Not Binding Formatters to Locale

```typescript
// ❌ WRONG - Formatters created once, won't update
const formatters = createFormatters($store.i18n);

// ✅ CORRECT - Derived, updates when locale changes
const formatters = $derived(createFormatters($store.i18n));
```

---

## PERFORMANCE TIPS

### 1. Cache Formatters Per Locale

The framework already caches `Intl.DateTimeFormat` and `Intl.NumberFormat` instances. Creating formatters is cheap.

### 2. Lazy Load Namespaces

```typescript
// Load translations only when needed
store.dispatch({
  type: 'i18n/loadNamespace',
  namespace: 'products'
});
```

### 3. Use Bundled Loader for SSR

Bundled translations are faster than fetch on every request:
```typescript
const translationLoader = new BundledTranslationLoader({
  bundles: {
    'en': { common: enCommon },
    'fr': { common: frCommon }
  }
});
```

### 4. Preload Critical Namespaces

```typescript
store.dispatch({
  type: 'i18n/setLocale',
  locale: 'fr',
  preloadNamespaces: ['common', 'navigation']  // Load multiple at once
});
```

---

## SSR/SSG INTEGRATION

For complete SSR/SSG patterns, see the **composable-svelte-ssr** skill. Key points:

### SSR
- Server: Use `BundledTranslationLoader`, `createSSRLocaleDetector`, `serverDOM`
- Client: Use `FetchTranslationLoader`, `createBrowserLocaleDetector`, `browserDOM`
- State serializes automatically via `renderToHTML`

### SSG (Static Sites)
- Use `createStaticLocaleDetector` with fixed locale
- Generate separate HTML files per locale (`/en/`, `/fr/`, `/es/`)
- Language switcher uses links to pre-generated pages

**Example**: See `examples/ssr-server/src/build/ssg.ts` for complete SSG setup.

---

## TYPESCRIPT SUPPORT

All types are fully typed and exported:

```typescript
import type {
  I18nState,
  I18nAction,
  I18nDependencies,
  Translator,
  BoundFormatters,
  TranslationLoader,
  LocaleDetector
} from '@composable-svelte/core/i18n';
```

---

## TESTING

Use `TestStore` to test translations and locale switching:

```typescript
import { TestStore } from '@composable-svelte/core/test';
import { createTranslator } from '@composable-svelte/core/i18n';

test('switches locale', async () => {
  const store = new TestStore({
    initialState: {
      i18n: createInitialI18nState('en', ['en', 'fr'])
    },
    reducer: appReducer,
    dependencies: mockI18nDependencies
  });

  await store.send({ type: 'i18n/setLocale', locale: 'fr', preloadNamespaces: ['common'] });
  await store.receive({ type: 'i18n/namespaceLoaded', namespace: 'common' }, (state) => {
    expect(state.i18n.currentLocale).toBe('fr');
  });
});
```

---

## SUMMARY

**Core Principles:**
1. i18n state lives in your app store via `i18nReducer`
2. Use `createTranslator()` for translations, `createFormatters()` for dates/numbers/currency
3. Framework handles all locale-specific formatting automatically
4. ICU MessageFormat for pluralization and selection
5. Three loaders: Fetch (client), Bundled (SSR), Glob (Vite)
6. Three detectors: Browser, SSR, Static (SSG)

**Most Common Pattern:**
```svelte
<script lang="ts">
  import { createTranslator, createFormatters } from '@composable-svelte/core/i18n';

  const t = $derived(createTranslator($store.i18n, 'common'));
  const formatters = $derived(createFormatters($store.i18n));
</script>

<div>
  <h1>{t('app.title')}</h1>
  <p>{t('welcome', { name: user.name })}</p>
  <time>{formatters.date(post.date)}</time>
  <span>{formatters.currency(product.price, 'USD')}</span>
</div>
```

The i18n system is fully integrated with the Composable Architecture, providing a complete, type-safe, and performant solution for building multi-language applications.

Overview

This skill provides a complete internationalization (i18n) system designed for Composable Svelte applications. It integrates translations, ICU MessageFormat, locale-aware formatters, locale detection, translation loading strategies, SSR/SSG support, and an i18n reducer. Use it to centralize locale state and deliver consistent localized UI across client and server rendering. The implementation is TypeScript-first and works with the Composable Architecture store model.

How this skill works

i18n state lives inside your global store and flows through the i18n reducer so locale changes and translation loads are first-class actions. createTranslator() exposes translation lookup with interpolation and ICU MessageFormat support, while createFormatters() returns locale-bound date, number, currency and relative-time formatters. Locale detectors handle browser, SSR, and static scenarios. Translation loaders support fetch-on-demand, build-time bundles, and Vite glob discovery.

When to use it

  • Adding multi-language support to a Composable Svelte app with single-source-of-truth state
  • Needing ICU MessageFormat features like pluralization, selection, and nested messages
  • Formatting dates, numbers, currencies, or relative times in a locale-aware way
  • Implementing server-side rendering or static site generation with correct initial locale
  • Lazy-loading translation namespaces to reduce initial bundle size

Best practices

  • Keep i18n state inside your app store and handle i18n actions via the i18n reducer
  • Use framework formatters (createFormatters) instead of manual toLocaleString or string concatenation
  • Preload commonly used namespaces (e.g., common) and lazy-load large feature namespaces
  • Choose the appropriate locale detector for environment: browser, SSR, or static
  • Provide a clear fallback locale and supportedLocales list to avoid missing keys

Example use cases

  • Client-only app: detect user locale in browser, preload 'common' namespace, and switch languages at runtime
  • SSR app: detect locale from request (query/cookie/Accept-Language), render with bundled translations, hydrate on client
  • Static site generation: pre-render pages per locale using a static locale detector
  • E-commerce: format prices and currency per user locale and load product translations lazily
  • Dashboard: use ICU pluralization and selection to produce grammatically correct messages

FAQ

Where should i18n state live?

Inside your global store. Integrate i18n via the i18n reducer so locale changes follow the same reducer and effect pipeline as other state.

How do I format dates and currency correctly?

Use createFormatters() from the framework. It returns locale-bound formatters for dates, numbers, currency, and relative time; never format manually.