# How Plane's i18n Internationalization System Works with i18next

> Discover how Plane's i18n internationalization system leverages i18next React bindings ICU formatting and dynamic JSON loading for robust multilingual support. Learn about its custom hook for error prevention.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: internals
- Published: 2026-06-18

---

**Plane implements its i18n layer using i18next with React bindings, ICU formatting, and dynamic JSON loading, wrapped in a custom `useTranslation` hook that handles namespace fallbacks and type coercion to prevent runtime errors.**

Plane's internationalization architecture in the `makeplane/plane` repository provides a type-safe, lazy-loaded translation system built on the i18next ecosystem. The implementation combines `react-i18next` for React integration, `i18next-icu` for advanced message formatting, and a custom resource loader to minimize bundle size while ensuring all namespaces are available without asynchronous cascading fetches.

## Core Architecture Components

The system consists of three tightly integrated layers that handle initialization, global configuration, and React component consumption.

### i18next Instance Configuration in [`instance.ts`](https://github.com/makeplane/plane/blob/main/instance.ts)

The foundation lives in [`packages/i18n/src/core/instance.ts`](https://github.com/makeplane/plane/blob/main/packages/i18n/src/core/instance.ts), where the shared `i18nInstance` is created and configured with specific plugins to handle React integration, ICU formatting, and dynamic resource loading.

The initialization chain includes:

- **`initReactI18next`** – Integrates the i18next instance with React's context system.
- **`ICU`** from `i18next-icu` – Enables ICU message syntax for complex pluralization, date, and number formatting.
- **`resourcesToBackend`** – Implements lazy loading by dynamically importing JSON files from `../locales/${language}/${namespace}.json`, keeping the initial bundle size small.

The configuration also defines a comprehensive list of **namespaces** (imported from [`packages/i18n/src/constants/namespaces.ts`](https://github.com/makeplane/plane/blob/main/packages/i18n/src/constants/namespaces.ts)) and sets the **default namespace** to `"common"`. Critically, `fallbackNS` is set to all available namespaces, allowing components to call `t(key)` without specifying a namespace while avoiding async load cascades.

### Global Language Constants

Language-related constants are centralized in [`packages/i18n/src/constants/language.ts`](https://github.com/makeplane/plane/blob/main/packages/i18n/src/constants/language.ts) to ensure consistency across the application. These include:

- **`FALLBACK_LANGUAGE`** – Set to `"en"` as the default fallback.
- **`SUPPORTED_LANGUAGES`** – An array of `ILanguageOption` objects defining available locales.
- **`LANGUAGE_STORAGE_KEY`** – The string `"userLanguage"` used for `localStorage` persistence.

These values feed directly into the `init` call (lines 28-30 of [`instance.ts`](https://github.com/makeplane/plane/blob/main/instance.ts)) and drive the UI language selector components.

### The Custom `useTranslation` Hook

Components interact with the system through [`packages/i18n/src/hooks/use-translation.ts`](https://github.com/makeplane/plane/blob/main/packages/i18n/src/hooks/use-translation.ts), which wraps the standard `react-i18next` functionality with Plane-specific safeguards.

The hook performs several critical functions:

1. **Wraps `useI18nextTranslation`** from `react-i18next` (line 43) to access the core translation function.
2. **Implements `coerceToString`** (lines 19-24) to guard against the `i18next-icu` bug where non-string values are returned for namespace-node lookups, preventing React errors like "Objects are not valid as a React child".
3. **Exposes utility properties** including `currentLocale`, `changeLanguage`, and `languages` (the supported list).
4. **Handles persistence** by saving language changes to `localStorage` (line 51) and updating `document.documentElement.lang` for accessibility and SEO.

## Namespace Management and Loading Strategy

Plane solves the common problem of async namespace fetching causing React re-render cascades through a pre-caching strategy. During initialization, `fallbackNS` is set to all namespaces, and `loadNamespaces(NAMESPACES)` is called (lines 49-52 of [`instance.ts`](https://github.com/makeplane/plane/blob/main/instance.ts)) to preload all translation files.

This approach ensures that when components call `t("key")` without specifying a namespace, the translation resolves immediately from the preloaded cache rather than triggering an asynchronous fetch that would cause component re-renders.

## ICU Formatting and Runtime Safety

The integration of `i18next-icu` requires specific configuration to prevent type-related runtime errors. The instance sets `returnObjects: false` (line 46) to prevent the formatter from returning raw objects.

The custom `coerceToString` function in [`use-translation.ts`](https://github.com/makeplane/plane/blob/main/use-translation.ts) provides an additional safety layer by converting numbers and booleans to strings and falling back to the translation key for unsupported types. In development mode, this function issues warnings (lines 25-36) to help developers identify problematic translation values early.

## Implementation Examples

Here is how to use the Plane i18n system in components and configuration:

```tsx
// Using the Plane i18n hook in a component
import { useTranslation } from "@plane/i18n";

export function Header() {
  const { t, changeLanguage, currentLocale, languages } = useTranslation();

  return (
    <header>
      <h1>{t("home.title")}</h1>
      <select
        value={currentLocale}
        onChange={e => changeLanguage(e.target.value as any)}
      >
        {languages.map(lang => (
          <option key={lang.value} value={lang.value}>
            {lang.label}
          </option>
        ))}
      </select>
    </header>
  );
}

```

```tsx
// Adding a new translation key
// In the UI component
const { t } = useTranslation();
<p>{t("project.create")}</p>

// In the locale file (e.g. packages/i18n/locales/en/project.json)
{
  "create": "Create Project"
}

```

```ts
// Manually loading a namespace (rarely needed)
import { i18nInstance } from "@plane/i18n";
await i18nInstance.loadNamespaces(["project"]);

```

## Summary

- **Lazy-loaded resources** – `resourcesToBackend` loads JSON translation files on demand to minimize initial bundle size.
- **Namespace pre-caching** – Setting `fallbackNS` to all namespaces and calling `loadNamespaces` prevents async fetch cascades and React re-renders.
- **ICU compatibility** – The `i18next-icu` plugin supports complex message formatting, with `returnObjects: false` ensuring string outputs.
- **Runtime type safety** – The `coerceToString` wrapper in [`use-translation.ts`](https://github.com/makeplane/plane/blob/main/use-translation.ts) prevents "Objects are not valid as a React child" errors by forcing string outputs.
- **Persistent preferences** – User language selections are stored in `localStorage` under the `"userLanguage"` key and applied to the HTML document element.

## Frequently Asked Questions

### How does Plane handle namespace loading without causing React re-render cascades?

Plane preloads all namespaces during initialization by setting `fallbackNS` to all available namespaces and calling `loadNamespaces(NAMESPACES)` in [`packages/i18n/src/core/instance.ts`](https://github.com/makeplane/plane/blob/main/packages/i18n/src/core/instance.ts) (lines 49-52). This ensures translation keys resolve immediately from the cache rather than triggering asynchronous fetches that would cause component re-renders when data arrives.

### What is the purpose of the `coerceToString` wrapper in Plane's i18n implementation?

The `coerceToString` function in [`packages/i18n/src/hooks/use-translation.ts`](https://github.com/makeplane/plane/blob/main/packages/i18n/src/hooks/use-translation.ts) (lines 19-24) guards against a known `i18next-icu` bug where the translation function returns non-string values (objects, numbers, or booleans) for certain namespace lookups. It converts these values to strings and falls back to the key for unsupported types, preventing React runtime errors like "Objects are not valid as a React child".

### How does Plane persist user language preferences across sessions?

The `useTranslation` hook reads the initial language from `localStorage` using the key `"userLanguage"` (defined in [`packages/i18n/src/constants/language.ts`](https://github.com/makeplane/plane/blob/main/packages/i18n/src/constants/language.ts)) during initialization. When `changeLanguage` is called, the hook saves the new value to `localStorage` (line 51 of [`use-translation.ts`](https://github.com/makeplane/plane/blob/main/use-translation.ts)) and updates `document.documentElement.lang` to reflect the current locale for accessibility and SEO purposes.

### Where are the translation files stored and how are they loaded?

Translation files are stored as JSON in `packages/i18n/locales/<language>/<namespace>.json`. They are loaded dynamically by the `resourcesToBackend` plugin configured in [`packages/i18n/src/core/instance.ts`](https://github.com/makeplane/plane/blob/main/packages/i18n/src/core/instance.ts), which uses dynamic imports to fetch only the required language and namespace combinations when needed, keeping the main application bundle size minimal.