How Plane's i18n System Supports Multiple Locales Using next-i18next and react-i18next
TLDR: Plane's internationalization layer leverages a centralized @plane/i18n package that combines next-i18next for server-side locale detection and react-i18next for client-side translation delivery, enabling automatic language switching through a provider pattern and namespace-organized JSON files.
The open-source project management platform Plane (makeplane/plane) implements a robust i18n system that supports multiple locales using next-i18next and react-i18next. This architecture centralizes translation logic within a dedicated monorepo package while enabling seamless server-side rendering and client-side language persistence across the entire application.
Configuration Architecture with next-i18next
The foundation of Plane's internationalization resides in the global configuration file at [packages/i18n/next-i18next.config.js](https://github.com/makeplane/plane/blob/preview/packages/i18n/next-i18next.config.js). This file declares the supported locales, default language, and resource path structure.
// packages/i18n/next-i18next.config.js
const path = require('path');
module.exports = {
i18n: {
defaultLocale: 'en-US',
locales: ['en-US', 'zh-CN', 'zh-TW', 'vi-VN', 'ua'],
},
localePath: path.resolve('./packages/i18n/src/locales'),
ns: ['common', 'workspace', 'project'],
defaultNS: 'common',
};
This configuration establishes en-US as the fallback locale while supporting five distinct languages. The localePath directs next-i18next to the centralized translation directory, ensuring consistent resource loading across both server and client environments.
Server-Side Locale Detection
When Next.js renders a page, next-i18next automatically detects the preferred locale from incoming request headers or URL parameters. The server pre-loads the appropriate translation resources and injects them into the page props as initialI18nStore and initialLocale. This hydration strategy ensures that the React application receives the correct translation data on initial render, preventing content flickering or locale mismatches between server and client.
The I18nextProvider Wrapper
Plane wraps the React application with a custom provider defined in [packages/i18n/src/provider/index.tsx](https://github.com/makeplane/plane/blob/preview/packages/i18n/src/provider/index.tsx). This component initializes the i18next instance using initReactI18next and connects the pre-loaded server data to the React context.
// packages/i18n/src/provider/index.tsx
import i18next from 'i18next';
import { I18nextProvider } from 'react-i18next';
import { initReactI18next } from 'react-i18next';
import nextI18NextConfig from '../../next-i18next.config';
i18next.use(initReactI18next).init({
...nextI18NextConfig.i18n,
ns: ['common', 'workspace', 'project'],
fallbackLng: 'en-US',
interpolation: { escapeValue: false },
react: {
useSuspense: false,
},
});
export const I18nProvider = ({ children }: { children: React.ReactNode }) => (
<I18nextProvider i18n={i18next}>{children}</I18nextProvider>
);
By instantiating i18next at the package level and exporting the provider, Plane ensures that all consuming applications in the monorepo share a single, consistent translation instance.
Namespace-Based Translation Organization
Translation strings are organized into namespaces that mirror functional domains within the application. The directory structure follows the pattern packages/i18n/src/locales/<locale>/<namespace>.json.
For example, the Simplified Chinese workspace translations reside at [packages/i18n/src/locales/zh-CN/workspace.json](https://github.com/makeplane/plane/blob/preview/packages/i18n/src/locales/zh-CN/workspace.json). This modular approach allows different teams to manage translations for specific features without merge conflicts, and enables webpack to split translation bundles for code-splitting optimization.
Client-Side Translation Hooks
Components consume translations through the useTranslation hook from react-i18next. The hook automatically resolves the correct namespace based on the component's context or an explicit ns parameter.
import { useTranslation } from 'react-i18next';
export const WorkspaceHeader = () => {
const { t } = useTranslation('workspace');
return (
<header>
<h1>{t('workspace.title')}</h1>
<p>{t('workspace.description')}</p>
</header>
);
};
Because the I18nextProvider already contains the initialized instance with loaded resources, the t function immediately returns the localized string for the active locale without additional asynchronous fetching.
Dynamic Locale Switching
Plane implements language switching through the i18next core API. When users select a different language, the application calls i18n.changeLanguage(newLocale), which updates the internal state and triggers a React re-render with the new translations.
import i18n from 'i18next';
export const LanguageSwitcher = () => {
const handleChange = (locale: string) => {
i18n.changeLanguage(locale);
};
return (
<select
onChange={(e) => handleChange(e.target.value)}
defaultValue={i18n.language}
>
<option value="en-US">English</option>
<option value="zh-CN">中文(简体)</option>
<option value="zh-TW">中文(繁體)</option>
<option value="vi-VN">Tiếng Việt</option>
<option value="ua">Українська</option>
</select>
);
};
In production deployments, next-i18next synchronizes the locale change with the Next.js router, updating the URL prefix (e.g., /en-US/dashboard to /zh-CN/dashboard) to ensure that subsequent page loads maintain the selected language through server-side rendering.
Build-Time Optimization
During the build process, next-i18next validates translation keys against the codebase and extracts only the namespaces required for each page. This optimization reduces the client-side bundle size by ensuring that translation files are loaded on-demand rather than bundled with the initial JavaScript payload. The static extraction also surfaces missing translation keys early in the development cycle, preventing runtime errors in production.
Summary
- Centralized Configuration: The
next-i18next.config.jsinpackages/i18ndefines supported locales and resource paths, serving as the single source of truth for the entire monorepo. - Provider Pattern: The
I18nProviderinpackages/i18n/src/provider/index.tsxinitializes i18next withinitReactI18nextand injects the instance into React's context. - Namespace Organization: Translations are split into JSON files by locale and functional domain (e.g.,
workspace.json,common.json), enabling modular maintenance and code-splitting. - Automatic Locale Detection: next-i18next handles server-side locale detection from headers and URLs, pre-loading resources before React hydration.
- Dynamic Switching: The
i18n.changeLanguage()method enables runtime locale switching with immediate UI updates and URL synchronization.
Frequently Asked Questions
How does Plane handle fallback languages when a translation key is missing?
When a key is missing in the active locale, i18next falls back to the defaultLocale defined in next-i18next.config.js (typically en-US). The fallbackLng parameter in the provider initialization ensures that if a specific key does not exist in zh-CN, the system automatically retrieves the English equivalent from the en-US namespace, preventing broken UI elements while maintaining user experience.
Where are translation files stored in the Plane repository?
Translation files are stored in the packages/i18n/src/locales/ directory, with subdirectories named after each locale code (e.g., en-US, zh-CN). Each subdirectory contains JSON files representing namespaces such as common.json, workspace.json, and project.json. This structure allows the build system to load only the required locale data and enables contributors to add new languages by creating additional locale folders following the same naming convention.
Can components use multiple translation namespaces simultaneously?
Yes, components can access multiple namespaces by passing an array to the useTranslation hook. For example, useTranslation(['workspace', 'common']) returns a t function that searches both namespaces, with the first namespace serving as the default. This is useful for complex UI components that display text from both feature-specific and shared translation sets, reducing the need to import separate hook instances.
How does Plane ensure translations are available during server-side rendering?
The application leverages next-i18next's server-side utilities to preload translation resources during the Next.js data fetching phase. The server injects the initialI18nStore into the HTML response, which the I18nextProvider consumes during React hydration. This mechanism guarantees that the initial HTML render contains the correct localized content, eliminating client-side loading states and improving SEO for multilingual pages.
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 →