Plane i18n Architecture Using i18next: Translation Structure and Implementation

Plane implements a type-safe, lazy-loaded internationalization system using a dedicated @plane/i18n package that wraps i18next with react-i18next, organizing translations into language-specific namespaces with automatic TypeScript generation.

Plane manages internationalization through a centralized @plane/i18n package that abstracts i18next complexity while providing React-specific bindings. The architecture follows a strict layered approach with clear separation between instance creation, initialization logic, and React integration. This setup enables efficient lazy loading of translation resources while maintaining compile-time type safety across the makeplane/plane codebase.

Core Architecture Layers

The @plane/i18n package organizes its responsibilities into distinct layers, each handling a specific aspect of the internationalization lifecycle.

The Instance Layer

The foundation resides in src/core/instance.ts, which creates a singleton i18nInstance using i18n.createInstance(). This file registers three critical plugins: i18next-icu for ICU message formatting (handling pluralization and date/number formatting), initReactI18next for React binding, and i18next-resources-to-backend for lazy loading translation files via dynamic import() calls using the pattern ../locales/${language}/${namespace}.json.

The Initialization Layer

Within the same src/core/instance.ts file, the initialization sequence performs the initial i18n.init call and stores the resulting promise as initPromise. The configuration object supplies supportedLngs from the SUPPORTED_LANGUAGES constant, declares all available namespaces, sets react.useSuspense: false to disable React Suspense for async loading, and configures fallbackNS to include all namespaces except the default. This allows any t('key') call to search across every namespace without triggering component re-renders.

The Provider Layer

The src/provider/index.tsx file exposes the ready i18nInstance to the React tree via <I18nextProvider>. The provider subscribes to initPromise and returns null until initialization completes, preventing any UI that depends on translations from rendering prematurely. Once ready, the provider renders children with full access to the configured i18next instance.

The Hooks Layer

Located in src/hooks/use-translation.ts, this layer provides convenience hooks including useTranslation, useTranslationNamespace, and useTranslationLanguage. These typed wrappers abstract direct i18next calls and add Plane-specific TypeScript definitions, ensuring components consume translations through a consistent, type-safe API.

Configuration Constants

Two critical files define the static configuration: src/constants/language.ts lists SUPPORTED_LANGUAGES, FALLBACK_LANGUAGE, and LANGUAGE_STORAGE_KEY for localStorage persistence, while src/constants/namespaces.ts declares all translation namespaces and the default namespace used throughout the UI.

How i18next Initialization Works in Plane

The initialization process follows a strict sequence to ensure translations are available before components render:

  1. Singleton Creation – i18n.createInstance() builds the isolated instance.

  2. Plugin Registration – The ICU plugin, React integration, and backend loader are registered to handle formatting, React bindings, and dynamic JSON imports respectively.

  3. Language Resolution – The initial language (initialLng) resolves from localStorage[LANGUAGE_STORAGE_KEY] or falls back to FALLBACK_LANGUAGE.

  4. Core Configuration – The init call configures supportedLngs, ns (namespaces), defaultNS, and fallbackNS to enable cross-namespace key lookups.

  5. Namespace Pre-loading – After core initialization, i18nInstance.loadNamespaces(NAMESPACES) eagerly caches every namespace for the initial language. This eliminates the cascade of re-renders that would occur if components loaded namespaces on-demand.

  6. Provider Activation – The <TranslationProvider> waits for initPromise resolution before rendering the application tree, ensuring t('my.key') calls resolve instantly without async delays.

Translation File Structure and Namespace Organization

Translations are stored under packages/i18n/src/locales/ following a hierarchical pattern that enables efficient code splitting and clear ownership.

Directory Structure


locales/
 ├─ <language-code>/          # e.g., zh-CN, vi-VN, it, de, en-US

 │   ├─ <namespace>.json      # one file per namespace

 │   │   └─ { "key": "value", … }
 │   └─ …
 └─ …

Namespace Organization

Namespaces defined in src/constants/namespaces.ts correspond to logical UI sections:

  • common – Generic UI strings (buttons, labels, confirmations)
  • navigation – Menu items, breadcrumbs, routing labels
  • project – Project-specific terminology and actions
  • workspace – Workspace-level entities and settings
  • auth – Authentication flows and error messages
  • editor – Rich text editor controls and formatting
  • automation – Workflow automation labels

Each JSON file contains a flat key/value map. For example, common.json might include:

{
  "save": "Save",
  "cancel": "Cancel",
  "delete": "Delete"
}

While navigation.json could hold:

{
  "dashboard": "Dashboard",
  "issues": "Issues",
  "settings": "Settings"
}

This structure enables tree-shaking (only required namespaces load), parallel loading (each namespace fetches via separate dynamic imports), and clear ownership (UI modules import only relevant namespaces).

Type Safety and Development Workflow

Plane maintains compile-time safety for translation keys through an automated generation pipeline.

The Type Generation Script

The scripts/generate-types.ts file (invoked by the CI pipeline) parses all translation JSON files and produces src/generated/types.d.ts. This declaration file provides TypeScript definitions that enforce valid namespace:key combinations when using the t() function.

Adding New Translations

To extend translations:

  1. Add the key-value pair to the appropriate <namespace>.json file for each supported language under src/locales/<language-code>/.

  2. Run the type generation script to update src/generated/types.d.ts.

  3. Import the namespace in your component using the typed useTranslation hook from src/hooks/use-translation.ts.

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

// Type-safe access to the 'common' namespace
const { t } = useTranslation("common");

// 'save' is validated at compile time
return <button>{t("save")}</button>;

Summary

  • Plane's i18n architecture centers on a dedicated @plane/i18n package that wraps i18next and react-i18next in a multi-layered abstraction.

  • Instance initialization in src/core/instance.ts creates a singleton with ICU formatting, React bindings, and lazy-loading backend configuration.

  • Translation files are organized by language code and namespace under src/locales/, enabling granular loading and clear separation of concerns.

  • Namespace pre-loading during initialization prevents UI re-render cascades by ensuring all translation resources are cached before the first component renders.

  • Type safety is enforced through scripts/generate-types.ts, which generates TypeScript definitions from JSON translation files to validate keys at compile time.

Frequently Asked Questions

How does Plane handle language switching at runtime?

Plane reads the initial language from localStorage using LANGUAGE_STORAGE_KEY defined in src/constants/language.ts, falling back to a configured FALLBACK_LANGUAGE. When switching languages, the application updates the storage value and re-initializes the i18next instance, leveraging the lazy-loading backend to fetch only the required language namespaces on demand.

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

Namespaces in src/constants/namespaces.ts logical group translation keys by UI domain (such as common, project, or navigation). This organization enables tree-shaking so only necessary translations load, prevents key collisions across features, and allows parallel loading of multiple JSON files via dynamic imports.

How does Plane ensure type safety for translation keys?

The scripts/generate-types.ts utility scans all JSON files in src/locales/ and generates TypeScript definitions in src/generated/types.d.ts. The custom hooks in src/hooks/use-translation.ts consume these generated types, ensuring that t('namespace:key') calls reference only valid keys and namespaces defined in the translation files.

Why does Plane disable React Suspense for i18next?

The initialization configuration sets react.useSuspense: false to prevent React Suspense from handling async translation loading. Instead, Plane uses the <TranslationProvider> in src/provider/index.tsx to block rendering until initPromise resolves, providing explicit control over loading states and avoiding UI flicker during namespace resolution.

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 →