How Plane's i18n Internationalization System Works with i18next

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

The foundation lives in 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) 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 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) and drive the UI language selector components.

The Custom useTranslation Hook

Components interact with the system through 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) 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 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:

// 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>
  );
}
// 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"
}
// 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 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 (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 (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) during initialization. When changeLanguage is called, the hook saves the new value to localStorage (line 51 of 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, which uses dynamic imports to fetch only the required language and namespace combinations when needed, keeping the main application bundle size minimal.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →