How Plane Handles Internationalization (i18n): Architecture and Implementation

Plane implements internationalization through a dedicated @plane/i18n package built on the i18next ecosystem, utilizing a singleton instance with ICU formatting, a React context provider, and a custom hook that ensures type-safe translations with dynamic JSON loading.

Plane, the open-source project management platform, delivers multi-language support through a robust internalization layer encapsulated in the @plane/i18n package. This architecture leverages the i18next library with ICU formatting capabilities to provide runtime language switching and namespaced translation management. The implementation ensures optimal performance through eager resource loading and strict runtime type guards.

Core Architecture

Plane's internationalization system consists of three coordinated components that initialize once and provide translation capabilities throughout the React application tree.

Singleton i18next Instance

The foundation resides in packages/i18n/src/core/instance.ts, which creates and configures a singleton i18nInstance. This module initializes i18next with the ICU plugin for advanced formatting, wires it to react-i18next, and connects a dynamic backend that imports JSON files from packages/i18n/src/locales/<lang>/<ns>.json. The configuration eagerly loads all namespaces during initialization to prevent render-cascade reloads, and sets up fallback languages and supported locales.

React Provider Component

The packages/i18n/src/provider/index.tsx file exports the I18nProvider component that wraps the application tree with I18nextProvider. This provider checks i18nInstance.isInitialized and waits for the initPromise to resolve before rendering children, ensuring the UI only appears after translation resources are ready.

Custom useTranslation Hook

Located at packages/i18n/src/hooks/use-translation.ts, this wrapper around react-i18next's hook adds runtime safety checks and convenience methods. It verifies that translation results are strings (preventing crashes when ICU returns raw objects) and exposes a setLanguage helper that updates localStorage and invokes i18n.changeLanguage.

Language and Namespace Configuration

Language support and organizational structure are defined in centralized constants files.

Supported Languages and Fallbacks

The packages/i18n/src/constants/language.ts file declares supported languages, the default locale, and the fallback strategy. On initialization, the system checks localStorage for a stored language preference before defaulting to the configured primary language.

Namespace Organization

Translation keys are organized into namespaces—such as "common", "auth", and "settings"—defined in packages/i18n/src/constants/namespaces.ts. The architecture automatically derives a fallback namespace, allowing components to call t('key') without specifying a namespace while preventing unnecessary re-renders.

Runtime Initialization and Language Switching

The initialization sequence ensures translations load before the UI renders, while language switching updates the entire application state.

Initialization Sequence

When the application mounts, the I18nProvider triggers initPromise, causing i18nInstance to load all JSON resources for the detected language. Components access these translations through the custom useTranslation hook, which guarantees string outputs and provides the current locale.

Changing Languages at Runtime

The setLanguage method exposed by the custom hook persists the selection to localStorage and calls i18n.changeLanguage, triggering a re-render of all translation-aware components with the new locale strings.

Implementation Examples

Wrap your application root with the provider:

import { I18nProvider } from '@plane/i18n';

function App() {
  return (
    <I18nProvider>
      <YourAppComponents />
    </I18nProvider>
  );
}

Access translations in components using the default namespace:

import { useTranslation } from '@plane/i18n';

function Greeting() {
  const { t } = useTranslation();
  return <h1>{t('welcome_message')}</h1>;
}

Implement a language switcher with persistence:

import { useTranslation } from '@plane/i18n';

function LanguageSwitcher() {
  const { i18n, setLanguage } = useTranslation();

  const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    setLanguage(e.target.value);
  };

  return (
    <select value={i18n.language} onChange={handleChange}>
      <option value="en">English</option>
      <option value="es">Español</option>
      <option value="fr">Français</option>
    </select>
  );
}

Summary

Frequently Asked Questions

How does Plane store user language preferences?

Plane persists the selected language to localStorage through the setLanguage method exposed by the custom useTranslation hook. When the application initializes, the i18next instance checks this storage key before falling back to the default language defined in packages/i18n/src/constants/language.ts.

What is the purpose of namespaces in Plane's i18n implementation?

Namespaces organize translation keys into logical groups such as "common", "auth", and "settings", defined in packages/i18n/src/constants/namespaces.ts. This separation allows the system to load translations on demand and enables components to call t('key') without specifying a namespace, simplifying the API while maintaining performance.

Why does Plane use a custom wrapper around react-i18next's useTranslation hook?

The custom hook in packages/i18n/src/hooks/use-translation.ts adds runtime guards that ensure translation values are strings, preventing application crashes when ICU formatting returns raw objects. It also centralizes language switching logic by combining localStorage updates with i18n.changeLanguage into a single setLanguage helper.

How does Plane prevent translation loading delays from causing UI flicker?

The I18nProvider component waits for the initPromise returned by the singleton instance to resolve before rendering children. Additionally, the i18next instance is configured to eagerly load all namespaces during initialization rather than loading them on demand, eliminating render-cascade reloads once the application becomes visible.

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 →