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.ICUfromi18next-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 ofILanguageOptionobjects defining available locales.LANGUAGE_STORAGE_KEY– The string"userLanguage"used forlocalStoragepersistence.
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:
- Wraps
useI18nextTranslationfromreact-i18next(line 43) to access the core translation function. - Implements
coerceToString(lines 19-24) to guard against thei18next-icubug where non-string values are returned for namespace-node lookups, preventing React errors like "Objects are not valid as a React child". - Exposes utility properties including
currentLocale,changeLanguage, andlanguages(the supported list). - Handles persistence by saving language changes to
localStorage(line 51) and updatingdocument.documentElement.langfor 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 –
resourcesToBackendloads JSON translation files on demand to minimize initial bundle size. - Namespace pre-caching – Setting
fallbackNSto all namespaces and callingloadNamespacesprevents async fetch cascades and React re-renders. - ICU compatibility – The
i18next-icuplugin supports complex message formatting, withreturnObjects: falseensuring string outputs. - Runtime type safety – The
coerceToStringwrapper inuse-translation.tsprevents "Objects are not valid as a React child" errors by forcing string outputs. - Persistent preferences – User language selections are stored in
localStorageunder 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →