---
title: Internationalization
description: Configure i18next translations in React Text Game, from the quick inline setup to custom UIs and advanced i18next plugins.
keywords:
    - i18n
    - internationalization
    - translations
    - i18next
    - react-i18next
    - language switcher
---

# Internationalization (i18n)

React Text Game ships with a first-class internationalization layer powered by **i18next** and **react-i18next**. The core package owns all i18n configuration, persists the player’s language in the save database, and exposes hooks that keep React components in sync. The UI package builds on top of that foundation with pre-translated menus and a ready-to-use language toggle.

This guide walks through three typical setups:

1. The simplest inline configuration using the stock UI package.
2. A custom UI that manages its own translations while still leveraging the core engine.
3. Advanced usage with i18next plugins such as language detectors or HTTP backends.

Along the way we will highlight the hooks and utilities you will use in day-to-day development: `useGameTranslation`, `getGameTranslation`, and the optional `LanguageToggle` component.

## Quick Start: Inline Resources

If you are building on top of `@react-text-game/ui`, the fastest path is to inline your translations directly in the `GameProvider` options. The provider calls `Game.init` under the hood, merges your resources with the UI defaults, and keeps the active language in persistent storage.

```tsx
import { useGameTranslation } from "@react-text-game/core/i18n";
import {
    GameProvider,
    PassageController,
    LanguageToggle,
} from "@react-text-game/ui";
import "@react-text-game/ui/styles";

export function App() {
    return (
        <GameProvider
            options={{
                gameName: "My Adventure",
                translations: {
                    defaultLanguage: "en",
                    fallbackLanguage: "en",
                    resources: {
                        en: {
                            passages: {
                                intro: "Welcome to the adventure!",
                            },
                            common: {
                                currentLanguage: "Language: {{language}}",
                            },
                        },
                        es: {
                            passages: {
                                intro: "¡Bienvenido a la aventura!",
                            },
                            common: {
                                currentLanguage: "Idioma: {{language}}",
                            },
                        },
                    },
                },
            }}
        >
            <HUD />
            <PassageController />
            <LanguageToggle />
        </GameProvider>
    );
}

function HUD() {
    const { t, currentLanguage } = useGameTranslation("common");
    return (
        <header className="flex items-center gap-3">
            <span>{t("currentLanguage", { language: currentLanguage })}</span>
        </header>
    );
}
```

**What you get out of the box:**

- **Language persistence.** Switching languages writes to the save database so the preference survives reloads and new sessions.
- **UI translations.** The UI package provides an English `ui` namespace. Your resources override its defaults when you supply the same keys.
- **React hooks.** `useGameTranslation(namespace)` provides the familiar `t` function, a `changeLanguage` helper, and `languages` filtered for production use (the debug `cimode` language is hidden unless you enable `debug`).

Use this setup when you want the UI components and only need to localize the stock experience.

## Custom UI: Bring Your Own Components

Teams that build bespoke interfaces can still rely on the core i18n layer. The trick is to pass a shared `translations` object into `Game.init` and call the hooks in your own components. You can keep translation files alongside your UI code or load them dynamically.

```tsx
// src/locales/en/common.ts
export const commonEn = {
    hud: {
        hp: "HP",
        mp: "MP",
        inventory: "Inventory",
    },
};

// src/locales/es/common.ts
export const commonEs = {
    hud: {
        hp: "Salud",
        mp: "Maná",
        inventory: "Inventario",
    },
};

// src/game/init.ts
import { Game } from "@react-text-game/core";
import { commonEn } from "../locales/en/common";
import { commonEs } from "../locales/es/common";

export async function bootGame() {
    await Game.init({
        gameName: "Legends of the Vale",
        translations: {
            defaultLanguage: "en",
            fallbackLanguage: "en",
            resources: {
                en: { common: commonEn },
                es: { common: commonEs },
            },
        },
    });
}

// src/ui/Hud.tsx
import {
    useGameTranslation,
    getGameTranslation,
} from "@react-text-game/core/i18n";

export function Hud() {
    const { t } = useGameTranslation("common");
    return (
        <aside>
            <p>{t("hud.hp")}: 74</p>
            <p>{t("hud.mp")}: 21</p>
        </aside>
    );
}

export function getInventoryLabel() {
    const t = getGameTranslation("common");
    return t("hud.inventory");
}
```

Key points for custom UIs:

- **No UI package required.** If `@react-text-game/ui` is absent, `loadUITranslations` simply returns an empty object—your resources dictate the available languages.
- **Share resources.** Reuse the same `translations` object across the app to avoid drifting keys. Consider exporting the config from a single module.
- **Non-React usage.** `getGameTranslation(namespace)` is ideal for game logic, data loaders, or any code running outside the React tree.

## Advanced Plugins: Language Detection and Remote Backends

i18next has a vibrant plugin ecosystem. The core engine exposes a `modules` array within the `translations` configuration so you can register any compatible plugin. Modules are chained after `initReactI18next`, granting you full control.

```tsx
import LanguageDetector from "i18next-browser-languagedetector";
import HttpBackend from "i18next-http-backend";

await Game.init({
    gameName: "Galactic Courier",
    translations: {
        defaultLanguage: "en",
        fallbackLanguage: "en",
        // Leave resources empty to load from the backend
        resources: {},
        modules: [LanguageDetector, HttpBackend],
    },
});
```

Things to keep in mind when adding plugins:

- **Backends still need namespaces.** Ensure your backend serves JSON in the `{ namespace: { key: value } }` shape for each language.
- **Detectors and persistence.** The core engine will still store the language in the settings table. Detectors typically run before persistence kicks in, so the saved language remains authoritative unless the detector selects a supported language that differs.
- **Custom setup logic.** Some plugins expose an options object (e.g., `HttpBackend`). Configure them directly before passing the module to `modules`, or initialize them via side effects using i18next’s plugin API.

```ts
import i18next from "i18next";

i18next
    .createInstance()
    .use(HttpBackend)
    .init({
        backend: {
            loadPath: `/locales/{{lng}}/{{ns}}.json`,
        },
    });
```

Because the engine calls `module => i18next.use(module)` in order, you can rely on module-specific configuration blocks or dynamic imports if certain features should be opt-in.

## Package Defaults: `registerTranslations`

Companion packages ship their own namespace of default strings so a game gets working text without copying resource bundles around. `@react-text-game/messenger` does this for its `messenger` namespace: importing the package is enough.

If you write a package of your own, register its defaults the same way:

```typescript
import { registerTranslations } from "@react-text-game/core/i18n";

registerTranslations({
    en: { myPackage: { greeting: "Hello" } },
    ru: { myPackage: { greeting: "Привет" } },
});
```

Precedence runs from most general to most specific, so **your resources always win**:

1. defaults registered through `registerTranslations`
2. defaults from `@react-text-game/ui`
3. your `translations.resources`

Call order does not matter. Registering before `Game.init()` folds the resources into the initial i18next configuration; registering afterwards adds them through `addResourceBundle` without overwriting keys that are already present. Repeated calls accumulate, so several packages can contribute at once.

To override a package default, simply declare the same key in your own resources:

```typescript
await Game.init({
    gameName: "My Game",
    translations: {
        resources: {
            en: {
                messenger: { typing: "is writing..." },
            },
        },
    },
});
```

## Best Practices

- **Keep namespaces focused.** Organize translations by intent: `passages`, `common`, `ui`, `system`, etc. This keeps resource bundles tidy and makes it easy to reason about overrides.
- **Document key usage.** When collaborating with narrative designers, export TypeScript helpers or comments showing which components expect specific keys.
- **Avoid circular imports.** Define translations in plain objects or JSON files. Hook modules should import from the translation layer, not the other way around.
- **Test language switching.** The persistence layer uses the saves database. In development, clear the settings table or call `changeLanguage` programmatically to validate behaviors.

With these guidelines, you can scale your translations from lightweight demos to large narrative games without sacrificing developer experience.
