How GeoLibre's react-i18next i18n System Supports Translations and RTL Locales
GeoLibre's internationalization system uses react-i18next with lazy-loaded language catalogs and automatic RTL mirroring for Arabic and other right-to-left locales.
GeoLibre is an open-source geospatial application built on React that requires robust multilingual support for its global user base. Its i18n system using react-i18next implements a sophisticated lazy-loading architecture that keeps bundle sizes small while supporting 15 non-English languages and seamless right-to-left layout switching.
Initializing the i18next Instance
The core configuration lives in apps/geolibre-desktop/src/i18n/index.ts. Here the i18next instance is created and wired with initReactI18next from the React integration:
import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
i18next
.use(initReactI18next)
.init({
lng: 'en',
fallbackLng: 'en',
// English bundled directly; others loaded on demand
});
English (en) is the only locale bundled directly with the application. All other languages are configured as dynamic imports, preventing unnecessary bytes from reaching users who don't need them.
Lazy-Loading Translation Catalogs
The loaders map in index.ts (lines 17-27) uses import.meta.glob to build a runtime registry of locale codes to import functions:
const loaders = import.meta.glob('./locales/*.json');
export async function loadCatalog(language: string): Promise<boolean> {
const loader = loaders[`./locales/${language}.json`];
if (!loader) return false;
const resources = await loader();
i18next.addResourceBundle(language, 'translation', resources);
return true;
}
When a user selects a non-default language, loadCatalog fetches the JSON chunk, registers it with i18next, and makes translations available immediately.
Resolving the Initial Language
The getInitialLanguage function (lines 31-40) implements a priority-based resolution strategy:
- Query parameters (
?locale=or?lang=) — highest priority for embeds - Persisted setting — previously saved user preference
- Browser preferences —
navigator.languagesfallback - Default (
en) — guaranteed fallback
This allows embedded maps to force a specific language via URL, while respecting user preferences in the full application.
Safe Language Switching with setActiveLanguage
Rapid language switches could cause race conditions. The setActiveLanguage function (lines 71-108) queues changes and returns true only when the request actually applied:
import { setActiveLanguage } from 'apps/geolibre-desktop/src/i18n';
async function handleLanguageChange(code: string) {
const applied = await setActiveLanguage(code);
if (applied) {
// Safe to persist — the change definitely took effect
localStorage.setItem('preferred-language', code);
}
}
The function first loads the catalog if needed, then calls i18n.changeLanguage, ensuring the UI never displays untranslated keys.
Automatic RTL Direction Handling
RTL support is implemented through document-level direction synchronization. The companion file apps/geolibre-desktop/src/i18n/languages.ts exports languageDirection, which maps language codes to "ltr" or "rtl" (Arabic → "rtl").
On every language change, applyDocumentDirection updates both attributes:
function applyDocumentDirection(language: string) {
const dir = languageDirection(language);
document.documentElement.lang = language;
document.documentElement.dir = dir;
}
// Called automatically via i18next.on('languageChanged', ...)
i18next.on('languageChanged', applyDocumentDirection);
Tailwind's logical properties (ms-, me-, ps-, pe-) ensure the layout mirrors automatically when dir="rtl" is set. No component-level CSS changes are required.
Using Translations in Components
Components throughout GeoLibre import useTranslation from react-i18next. The StylePanel.tsx component demonstrates typical usage:
import { useTranslation } from 'react-i18next';
export function StylePanel() {
const { t } = useTranslation();
return (
<div>
<h2>{t('style_panel.title')}</h2>
<p>{t('style_panel.description')}</p>
</div>
);
}
Because catalogs are pre-loaded or loaded on-demand, translated strings render without the "flash of untranslated content" common in simpler i18n implementations.
Complete Language Switcher Implementation
Here's a production-ready language selector that persists user choice:
import { useTranslation } from 'react-i18next';
import { setActiveLanguage } from 'apps/geolibre-desktop/src/i18n';
function LanguageSwitcher() {
const { i18n } = useTranslation();
const changeLang = async (code: string) => {
const applied = await setActiveLanguage(code);
if (applied) {
console.log(`Language changed to ${code}`);
// Optional: broadcast to analytics, sync across tabs, etc.
}
};
return (
<select
value={i18n.language}
onChange={e => changeLang(e.target.value)}
>
<option value="en">English</option>
<option value="ar">العربية</option>
<option value="es">Español</option>
<option value="fr">Français</option>
{/* Additional locales */}
</select>
);
}
Embed-Level Language Override
The query parameter parsing in getInitialLanguage enables immediate language control in embedded contexts:
<!-- Force Arabic UI with RTL layout -->
<iframe src="https://geolibre.example.com/embed?locale=ar"></iframe>
<!-- Alternative syntax also supported -->
<iframe src="https://geolibre.example.com/embed?lang=he"></iframe>
The embedded map initializes with the correct language and direction without requiring JavaScript coordination between parent and iframe.
Key Implementation Files
| File | Purpose |
|---|---|
apps/geolibre-desktop/src/i18n/index.ts |
Core bootstrap, lazy loading, language switching, document direction |
apps/geolibre-desktop/src/i18n/languages.ts |
Direction mapping, language resolution utilities |
apps/geolibre-desktop/src/i18n/locales/en.json |
Fallback catalog and TypeScript type source |
docs/i18n.md |
Design documentation and caching strategy |
Summary
- Lazy-loading keeps initial bundles minimal—only English ships with the main bundle
- Dynamic imports via
import.meta.globenable runtime language addition - Race-condition safe switching through
setActiveLanguage's queuing mechanism - Automatic RTL mirroring via
document.dirand Tailwind logical properties - Embed-ready with URL-parameter language forcing
Frequently Asked Questions
How does GeoLibre handle missing translations?
The system falls back to English (fallbackLng: 'en') for any keys absent in the selected language catalog. The English catalog serves as the complete reference, ensuring the UI never renders empty strings.
Can new languages be added without rebuilding the application?
Yes. Because import.meta.glob discovers locale files at build time, adding a new xx.json file to locales/ and rebuilding includes it automatically. The languageDirection helper in languages.ts should also be updated to specify LTR or RTL for the new code.
Why does the RTL implementation use document.dir rather than CSS transforms?
Setting document.documentElement.dir triggers standard browser bidi behavior, which works with native form controls, scrollbars, and canvas elements that CSS transforms cannot reliably mirror. Tailwind's logical properties then adapt spacing and positioning automatically.
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 →