Skip to main content

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.

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.

// 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.

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.
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.

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.