npm package: @polingo/react
Description: React bindings for the Polingo translation engine
Author: Reinier Hernández Avila
Latest version: 0.0.4

# @polingo/react

[![npm version](https://img.shields.io/npm/v/%40polingo%2Freact.svg)](https://www.npmjs.com/package/@polingo/react) ![npm bundle size](https://img.shields.io/bundlephobia/min/%40polingo%2Freact)

React bindings for the Polingo translation engine.

This package exposes a context provider, data hooks, and a rich-text `<Trans />` component that wrap the framework-agnostic `Translator` from `@polingo/core`.

## Contents

1. [Installation](#installation)
2. [Quick start](#quick-start)
3. [Step-by-step setup guide](#step-by-step-setup-guide)
4. [Translation workflow](#translation-workflow)
5. [API reference](#api-reference)
6. [Advanced patterns](#advanced-patterns)
7. [Related packages](#related-packages)
8. [License](#license)

## Installation

```bash
npm install @polingo/core @polingo/web @polingo/react
# or
pnpm add @polingo/core @polingo/web @polingo/react
# or
yarn add @polingo/core @polingo/web @polingo/react
```

For development, you'll also want the CLI tooling:

```bash
npm install -D @polingo/cli
# or
pnpm add -D @polingo/cli
```

## Quick start

```tsx
import { PolingoProvider, Trans, useTranslation } from '@polingo/react';

function Example(): JSX.Element {
  const { t, tn, setLocale, locale } = useTranslation();

  return (
    <div>
      <p>{t('Hello {name}', { name: 'Polingo' })}</p>
      <p>{tn('You have {n} message', 'You have {n} messages', 3)}</p>

      <button type="button" onClick={() => setLocale(locale === 'en' ? 'es' : 'en')}>
        <Trans message="Switch to <0>Spanish</0>" components={[<strong />]} />
      </button>
    </div>
  );
}

export function App(): JSX.Element {
  return (
    <PolingoProvider
      create={{
        locale: 'en',
        locales: ['en', 'es'],
        loader: { baseUrl: '/i18n' },
      }}
    >
      <Example />
    </PolingoProvider>
  );
}
```

## Step-by-step setup guide

### 1. Create your project structure

First, create a directory structure for your locale files. The recommended structure is:

```
your-app/
├── public/
│   └── i18n/              # Translation catalogs for web loader
│       ├── en/
│       │   └── messages.json
│       └── es/
│           └── messages.json
├── locales/               # Source .po files
│   ├── messages.pot       # Template (generated by CLI)
│   ├── en/
│   │   └── messages.po
│   └── es/
│       └── messages.po
└── src/
    ├── App.tsx
    └── components/
```

### 2. Install dependencies

Install the required packages:

```bash
pnpm add @polingo/core @polingo/web @polingo/react
pnpm add -D @polingo/cli
```

### 3. Wrap your app with PolingoProvider

Update your root component (e.g., `src/App.tsx` or `src/main.tsx`):

```tsx
import { PolingoProvider } from '@polingo/react';

export function App() {
  return (
    <PolingoProvider
      create={{
        locale: 'en', // Default locale
        locales: ['en', 'es', 'fr'], // All supported locales
        loader: {
          baseUrl: '/i18n', // URL where catalogs are served
        },
        cache: true, // Enable localStorage caching
        cacheOptions: {
          prefix: 'my-app', // Namespace for cache keys
          ttlMs: 86_400_000, // 24 hours cache lifetime
        },
      }}
    >
      <YourApp />
    </PolingoProvider>
  );
}
```

### 4. Use translations in your components

Use the `useTranslation` hook to access translation methods:

```tsx
import { useTranslation } from '@polingo/react';

function MyComponent() {
  const { t, tp, tn, tnp, locale, setLocale } = useTranslation();

  return (
    <div>
      {/* Basic translation */}
      <h1>{t('Welcome')}</h1>

      {/* Translation with variables */}
      <p>{t('Hello {name}!', { name: 'Alice' })}</p>

      {/* Translation with context (disambiguates homonyms) */}
      <button>{tp('menu', 'File')}</button>

      {/* Pluralization */}
      <p>{tn('You have {n} message', 'You have {n} messages', count, { n: count })}</p>

      {/* Pluralization with context */}
      <p>{tnp('inbox', '{n} new email', '{n} new emails', count, { n: count })}</p>

      {/* Switch locale */}
      <button onClick={() => setLocale('es')}>Español</button>
      <button onClick={() => setLocale('en')}>English</button>
    </div>
  );
}
```

### 5. Use the Trans component for rich-text translations

The `Trans` component allows you to embed React components inside translations:

```tsx
import { Trans } from '@polingo/react';

function RichTextExample() {
  return (
    <div>
      {/* Embed components with <0>, <1>, etc. placeholders */}
      <Trans
        message="I agree to the <0>terms of service</0> and <1>privacy policy</1>"
        components={[<a href="/terms" />, <a href="/privacy" />]}
      />

      {/* Combine with variables */}
      <Trans message="Welcome <0>{name}</0>!" components={[<strong />]} vars={{ name: 'Alice' }} />
    </div>
  );
}
```

### 6. Extract translatable strings from your code

Use the CLI to scan your source code and update your locale catalogs:

```bash
# Extract from src, sync locale catalogs, and auto-clean the temporary POT template
pnpm polingo extract src --locales locales --languages en,es --default-locale en

# Or use npx
npx @polingo/cli extract src --locales locales --languages en,es --default-locale en
```

This command finds all calls to `t()`, `tp()`, `tn()`, `tnp()`, and `<Trans>` in your codebase and updates (or creates) `locales/<lang>/messages.po` for every locale you list. The default locale copies the source strings into `msgstr`, while other locales get empty placeholders ready for translators. Add `--keep-template` if you need the generated `locales/messages.pot` for manual review.

### 7. Create locale-specific .po files

For each language you want to support, create a `.po` file based on the template (if you ran `polingo extract` with `--locales`, these files were created for you automatically):

```bash
# Create directories
mkdir -p locales/en locales/es

# Copy template to create new locale files
cp locales/messages.pot locales/en/messages.po
cp locales/messages.pot locales/es/messages.po
```

Edit each `.po` file to add translations. For example, `locales/es/messages.po`:

```po
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

msgid "Welcome"
msgstr "Bienvenido"

msgid "Hello {name}!"
msgstr "¡Hola {name}!"

msgid "You have {n} message"
msgid_plural "You have {n} messages"
msgstr[0] "Tienes {n} mensaje"
msgstr[1] "Tienes {n} mensajes"
```

**Pro tip:** Use translation tools like [Poedit](https://poedit.net/), [Lokalize](https://apps.kde.org/lokalize/), or web platforms like [Weblate](https://weblate.org/) to edit `.po` files more easily.

### 8. Compile .po files to JSON for the web loader

Convert your `.po` files to JSON format for the browser:

```bash
# Compile all .po files in locales/ directory to public/i18n/
pnpm polingo compile locales -o public/i18n --format json

# This creates:
# public/i18n/en/messages.json
# public/i18n/es/messages.json
```

The `--format json` option generates JSON catalogs that `@polingo/web` can fetch. If you were using `@polingo/node` instead, you could use `--format mo` to generate binary `.mo` files.

### 9. Validate your translations (optional)

Before deploying, validate that all translations are complete:

```bash
# Basic validation
pnpm polingo validate locales

# Strict mode (fails on fuzzy or missing translations)
pnpm polingo validate locales --strict
```

### 10. Add to your build process

Add these commands to your `package.json`:

```json
{
  "scripts": {
    "extract": "polingo extract src",
    "compile": "polingo compile locales -o public/i18n --format json",
    "validate": "polingo validate locales --strict",
    "prebuild": "pnpm extract && pnpm compile && pnpm validate",
    "build": "vite build"
  }
}
```

Now running `pnpm build` will automatically extract, compile, and validate translations before building your app.

## Translation workflow

Here's the recommended workflow for managing translations:

1. **Development**: Write code using `t()`, `tn()`, `tp()`, `tnp()`, and `<Trans>` components
2. **Extract**: Run `pnpm extract` to update your locale catalogs (add `--keep-template` if you need the `messages.pot` artifact)
3. **Update .po files**: Update each locale's `.po` file with new strings (manually or with tools)
4. **Translate**: Add translations for new strings in each `.po` file
5. **Validate**: Run `pnpm validate --strict` to check for missing translations
6. **Compile**: Run `pnpm compile` to generate JSON catalogs for the web
7. **Test**: Test your app with different locales
8. **Commit**: Commit both `.po` files and compiled JSON files to version control

### Updating translations

When you add new translatable strings to your code:

```bash
# Extract new strings (this updates messages.pot)
pnpm extract

# Merge new strings into existing .po files using msgmerge (gettext tool)
msgmerge --update locales/en/messages.po locales/messages.pot
msgmerge --update locales/es/messages.po locales/messages.pot

# Or simply copy and manually merge if you don't have gettext installed
cp locales/messages.pot locales/es/messages.po  # Then manually add translations

# Compile to JSON
pnpm compile

# Validate
pnpm validate --strict
```

## API reference

### PolingoProvider

Props:

- `translator?: Translator` – Pre-initialized translator instance (useful for SSR)
- `create?: CreatePolingoOptions | (() => Promise<WebPolingoInstance>)` – Configuration object or factory function to create translator
- `children: ReactNode` – Your app components
- `loadingFallback?: ReactNode` – Optional UI to display while loading
- `onError?: (error: unknown) => void` – Error callback for loading or locale switching failures

The provider accepts either:

1. A configuration object (recommended for most cases)
2. A factory function that returns a Polingo instance (for advanced initialization)

The provider exposes loading and error states while catalogs are being fetched.

**Using configuration object (recommended):**

```tsx
<PolingoProvider
  create={{
    locale: 'en',
    locales: ['en', 'es'],
    loader: { baseUrl: '/i18n' },
  }}
>
  <App />
</PolingoProvider>
```

**Using factory function (advanced):**

```tsx
import { createPolingo } from '@polingo/web';

<PolingoProvider
  create={() =>
    createPolingo({
      locale: 'en',
      locales: ['en', 'es'],
      loader: { baseUrl: '/i18n' },
    })
  }
>
  <App />
</PolingoProvider>;
```

### useTranslation()

Returns an object with:

- `t(msgid, vars?)` – Basic translation with optional variable interpolation
- `tp(context, msgid, vars?)` – Translation with context (disambiguates homonyms)
- `tn(msgid, msgidPlural, count, vars?)` – Plural-aware translation
- `tnp(context, msgid, msgidPlural, count, vars?)` – Plural translation with context
- `locale: string` – Current locale code
- `setLocale(locale: string): Promise<void>` – Switch to a different locale
- `loading: boolean` – True while catalogs are loading
- `error: Error | null` – Error if catalog loading failed

```tsx
function MyComponent() {
  const { t, tn, tp, tnp, locale, setLocale, loading, error } = useTranslation();

  if (loading) return <div>Loading translations...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return <div>{t('Hello')}</div>;
}
```

### useTranslator() / usePolingo()

Low-level hook that returns the `Translator` instance directly:

```tsx
const translator = useTranslator();
translator.t('Hello');
translator.setLocale('es');
```

### useLocale()

Returns just the current locale string:

```tsx
const locale = useLocale();
```

### Trans

Props:

- `message: string` – Translation key with `<0>`, `<1>`, etc. placeholders
- `components?: ReactNode[]` – Array of React elements to interpolate
- `vars?: Record<string, string | number>` – Variables for `{varName}` placeholders
- `context?: string` – Optional context (uses `tp` instead of `t`)
- `count?: number` – For plural forms (uses `tn` or `tnp`)
- `plural?: string` – Plural form of message (required when `count` is provided)

```tsx
<Trans
  message="I agree to the <0>terms</0>"
  components={[<a href="/terms" />]}
/>

<Trans
  message="You have <0>{n}</0> new message"
  plural="You have <0>{n}</0> new messages"
  count={5}
  components={[<strong />]}
  vars={{ n: 5 }}
/>
```

## Advanced patterns

### Loading states

Handle loading and error states gracefully:

```tsx
function App() {
  return (
    <PolingoProvider
      create={{
        locale: 'en',
        locales: ['en', 'es'],
        loader: { baseUrl: '/i18n' },
      }}
      loadingFallback={<div>Loading translations...</div>}
      onError={(error) => console.error('Translation error:', error)}
    >
      <AppContent />
    </PolingoProvider>
  );
}

function AppContent() {
  const { loading, error } = useTranslation();

  if (loading) {
    return <div>Loading translations...</div>;
  }

  if (error) {
    return <div>Failed to load translations: {error.message}</div>;
  }

  return <YourApp />;
}
```

### Server-side rendering (SSR)

For SSR frameworks like Next.js or Remix, create the translator on the server and pass it to the provider:

```tsx
// Server-side (e.g., Next.js getServerSideProps)
import { createPolingo } from '@polingo/web';

export async function getServerSideProps({ locale }) {
  const translator = await createPolingo({
    locale,
    locales: ['en', 'es'],
    loader: { baseUrl: '/i18n' },
  });

  return {
    props: {
      translator: translator.serialize(), // If you implement serialization
    },
  };
}

// Client-side
function App({ translator }) {
  return (
    <PolingoProvider translator={translator}>
      <YourApp />
    </PolingoProvider>
  );
}
```

### Custom loader paths

Customize where catalogs are loaded from:

```tsx
<PolingoProvider
  create={{
    locale: 'en',
    locales: ['en', 'es'],
    loader: {
      buildUrl: (locale, domain) => `https://cdn.example.com/translations/${locale}/${domain}.json`,
      requestInit: {
        credentials: 'include',
        cache: 'force-cache',
      },
    },
  }}
>
  <App />
</PolingoProvider>
```

### Multiple domains

Use different translation domains (e.g., `messages`, `errors`, `admin`):

```tsx
<PolingoProvider
  create={{
    locale: 'en',
    locales: ['en', 'es'],
    domain: 'admin', // Default domain
    loader: { baseUrl: '/i18n' },
  }}
>
  <App />
</PolingoProvider>

// This will load /i18n/en/admin.json
```

### Runtime locale detection

Detect locale from URL, localStorage, or browser settings:

```tsx
const getUserLocale = (): string => {
  // 1. Check URL parameter
  const urlParams = new URLSearchParams(window.location.search);
  const urlLocale = urlParams.get('locale');
  if (urlLocale) return urlLocale;

  // 2. Check localStorage
  const savedLocale = localStorage.getItem('locale');
  if (savedLocale) return savedLocale;

  // 3. Check browser language
  const browserLocale = navigator.language.split('-')[0]; // 'en-US' -> 'en'
  if (['en', 'es', 'fr'].includes(browserLocale)) return browserLocale;

  // 4. Default fallback
  return 'en';
};

const App = () => (
  <PolingoProvider
    create={{
      locale: getUserLocale(),
      locales: ['en', 'es', 'fr'],
      loader: { baseUrl: '/i18n' },
    }}
  >
    <YourApp />
  </PolingoProvider>
);
```

### Persisting locale choice

Save the user's locale preference:

```tsx
function LanguageSwitcher() {
  const { locale, setLocale } = useTranslation();

  const handleLocaleChange = async (newLocale: string) => {
    await setLocale(newLocale);
    localStorage.setItem('locale', newLocale);
  };

  return (
    <select value={locale} onChange={(e) => handleLocaleChange(e.target.value)}>
      <option value="en">English</option>
      <option value="es">Español</option>
      <option value="fr">Français</option>
    </select>
  );
}
```

## Related packages

- [`@polingo/core`](../core) – Translation runtime shared by all adapters
- [`@polingo/node`](../node) – Filesystem loader, middleware, and watcher for Node.js
- [`@polingo/web`](../web) – Fetch-based loader for browsers that pairs well with React
- [`@polingo/cli`](../cli) – Command line tooling for extraction, compilation, and validation

## License

MIT © Reinier Hernández Avila