---
id: "packages/active-i18n/getting-started"
title: "Getting started"
description: "Receive active-i18n, supply your dictionaries, and translate in a client component."
url: "/packages/active-i18n/getting-started"
locale: "en"
revision: "d3b30d32c00b2303"
---

# Getting started

## Receive the package

`active-i18n` is a product; `active-format` comes along as its owner. Ask the maintainer of
Foundation for a bundle that contains your whole managed set (a bundle without a package you already
have would remove it), then follow [Receiving packages](/receiving-packages):

```bash
node tools/rps/rps-apply.mjs plan --bundle <bundle-folder> --development
node tools/rps/rps-apply.mjs apply --bundle <bundle-folder> --expect-plan <digest> --development
```

## Declare, install and verify

```json
{
  "dependencies": {
    "@workspace/active-i18n": "workspace:*",
    "react": "^19.0.0"
  }
}
```

```bash
pnpm install
node tools/rps/rps-apply.mjs verify-install --from <app-folder>
```

In Next.js, list `@workspace/active-i18n` and `@workspace/active-format` in `transpilePackages`.

## Supply your dictionaries

A dictionary is a tree per namespace. A key is a dot path whose first segment is the namespace:

```ts
// The host's resources, for example resources/en/inbox.json:
const en = {
  inbox: {
    greeting: 'Hello {{name}}',
    messages: {one: '{{count}} new message', other: '{{count}} new messages'},
  },
};
```

## Translate

Without React, build a translator; in React, mount the provider once and read it with a hook:

```tsx
import {createTranslateFunction} from '@workspace/active-i18n';
import {I18nProvider, useTranslations} from '@workspace/active-i18n/react';

const t = createTranslateFunction(en, 'en');
t('inbox.messages', {count: 2}); // '2 new messages'

function Greeting() {
  const t = useTranslations();
  return <p>{t('inbox.greeting', {name: 'Ada'})}</p>;
}

<I18nProvider
  locale="en"
  translations={en}
  supportedLanguages={[{code: 'en', label: 'English'}]}
>
  <Greeting />
</I18nProvider>;
```

**Example: Interpolation, plurals and a missing key**

Client: Runs entirely in your browser. Source: `examples/i18n/translator.tsx`.

```tsx
// apps/docs/examples/i18n/translator.tsx
'use client';

import {useState} from 'react';

import {
  createTranslateFunction,
  type LocaleTranslations,
} from '@workspace/active-i18n';
import {Button} from '@workspace/active-ui/components/primitives/button';
import {
  Tabs,
  TabsList,
  TabsTrigger,
} from '@workspace/active-ui/components/primitives/tabs';

// The host's dictionaries: one tree per language, keyed by namespace.
const DICTIONARIES: Record<'en' | 'nl' | 'ar', LocaleTranslations> = {
  en: {
    inbox: {
      greeting: 'Hello {{name}}',
      messages: {one: '{{count}} new message', other: '{{count}} new messages'},
    },
  },
  nl: {
    inbox: {
      greeting: 'Hallo {{name}}',
      messages: {
        one: '{{count}} nieuw bericht',
        other: '{{count}} nieuwe berichten',
      },
    },
  },
  ar: {
    inbox: {
      greeting: 'مرحبًا {{name}}',
      messages: {
        zero: 'لا توجد رسائل جديدة',
        one: 'رسالة جديدة واحدة',
        two: 'رسالتان جديدتان',
        few: '{{count}} رسائل جديدة',
        many: '{{count}} رسالة جديدة',
        other: '{{count}} رسالة جديدة',
      },
    },
  },
};

/** A translator per language: interpolation, CLDR plurals and a missing key. */
export default function Translator() {
  const [locale, setLocale] = useState<'en' | 'nl' | 'ar'>('en');
  const [count, setCount] = useState(2);
  // Filled while this render translates: the translator reports a miss
  // through the callback and still returns the raw key.
  const missing: string[] = [];
  const t = createTranslateFunction(DICTIONARIES[locale], locale, {
    onMissingKey: ({key}) => missing.push(key),
  });
  return (
    <div className="flex flex-col gap-4">
      <Tabs
        value={locale}
        onValueChange={(value) => setLocale(value as 'en' | 'nl' | 'ar')}
      >
        <TabsList aria-label="Language">
          <TabsTrigger value="en">English</TabsTrigger>
          <TabsTrigger value="nl">Nederlands</TabsTrigger>
          <TabsTrigger value="ar">العربية</TabsTrigger>
        </TabsList>
      </Tabs>
      <div
        lang={locale}
        dir={locale === 'ar' ? 'rtl' : 'ltr'}
        className="flex flex-col gap-1"
      >
        <p data-i18n-greeting>{t('inbox.greeting', {name: 'Ada'})}</p>
        <p data-i18n-count={count}>{t('inbox.messages', {count})}</p>
        <p className="text-muted-foreground font-mono text-sm">
          {t('inbox.archived')}
        </p>
      </div>
      <div className="flex flex-wrap items-center gap-2">
        <Button
          variant="outline"
          size="sm"
          onClick={() => setCount((n) => Math.max(0, n - 1))}
        >
          Fewer
        </Button>
        <Button
          variant="outline"
          size="sm"
          onClick={() => setCount((n) => n + 1)}
        >
          More
        </Button>
        <span className="text-muted-foreground text-sm">count = {count}</span>
      </div>
      <p className="text-muted-foreground text-sm">
        Missing keys reported:{' '}
        {missing.length > 0 ? missing.join(', ') : 'none'}
      </p>
    </div>
  );
}
```

## Next

- [Language choice](/packages/active-i18n/language-choice): who decides the reader's language.
- [Server](/packages/active-i18n/server): load dictionaries on the server in Next.
