# How GeoLibre's react-i18next i18n System Supports Translations and RTL Locales

> Discover how GeoLibre's react-i18next i18n system enables seamless translations and automatic RTL mirroring for diverse locales like Arabic, enhancing global accessibility.

- Repository: [Open Geospatial Solutions/GeoLibre](https://github.com/opengeos/GeoLibre)
- Tags: internals
- Published: 2026-08-05

---

**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`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/i18n/index.ts). Here the i18next instance is created and wired with `initReactI18next` from the React integration:

```typescript
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`](https://github.com/opengeos/GeoLibre/blob/main/index.ts) (lines 17-27) uses `import.meta.glob` to build a runtime registry of locale codes to import functions:

```typescript
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**:

1. **Query parameters** (`?locale=` or `?lang=`) — highest priority for embeds
2. **Persisted setting** — previously saved user preference
3. **Browser preferences** — `navigator.languages` fallback
4. **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:

```typescript
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`](https://github.com/opengeos/GeoLibre/blob/main/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:

```typescript
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`](https://github.com/opengeos/GeoLibre/blob/main/StylePanel.tsx) component demonstrates typical usage:

```tsx
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:

```tsx
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**:

```html
<!-- 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`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/i18n/index.ts) | Core bootstrap, lazy loading, language switching, document direction |
| [`apps/geolibre-desktop/src/i18n/languages.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/i18n/languages.ts) | Direction mapping, language resolution utilities |
| [`apps/geolibre-desktop/src/i18n/locales/en.json`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/i18n/locales/en.json) | Fallback catalog and TypeScript type source |
| [`docs/i18n.md`](https://github.com/opengeos/GeoLibre/blob/main/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.glob` enable runtime language addition
- **Race-condition safe** switching through `setActiveLanguage`'s queuing mechanism
- **Automatic RTL mirroring** via `document.dir` and 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`](https://github.com/opengeos/GeoLibre/blob/main/xx.json) file to `locales/` and rebuilding includes it automatically. The `languageDirection` helper in [`languages.ts`](https://github.com/opengeos/GeoLibre/blob/main/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.