Skip to content

Internationalization (i18n)

GeoLibre's interface is translatable. UI strings live in JSON message catalogs rather than being hard-coded in components, so the app can be shipped in any language by dropping in a catalog. The layer is built on react-i18next and lives in apps/geolibre-desktop/src/i18n/.

This is an incremental effort: high-traffic surfaces (the top toolbar and menus, the Settings dialog) are translated first, and untranslated strings fall back to English until they are migrated. Adding a new language never requires touching component code.

How it works

apps/geolibre-desktop/src/i18n/
├── index.ts          # i18next init, catalog auto-discovery, language detection
├── languages.ts      # friendly language names + resolution helpers
├── i18next.d.ts      # types t() keys against the English catalog
└── locales/
    ├── en.json       # the baseline English catalog (source of truth)
    ├── zh.json       # Simplified Chinese
    ├── es.json       # Spanish
    ├── fr.json       # French
    ├── de.json       # German
    ├── pt.json       # Portuguese
    ├── it.json       # Italian
    ├── nl.json       # Dutch
    ├── ja.json       # Japanese
    ├── ko.json       # Korean
    ├── ru.json       # Russian
    ├── tr.json       # Turkish
    ├── id.json       # Indonesian
    └── hi.json       # Hindi

The non-English catalogs were seeded with machine-assisted translations and are open to native-speaker review and correction (PRs welcome).

  • en.json is the source of truth. Every key used by t() must exist here; i18next.d.ts types the t() function against it, so a missing or misspelled key is a compile error (npm run typecheck).
  • Catalogs are auto-discovered. index.ts eagerly imports every locales/*.json via import.meta.glob, so adding locales/fr.json is all it takes to make French selectable — no code changes.
  • Other locales may be partial. Any key missing from a non-English catalog falls back to en at runtime (fallbackLng: "en").

Language detection

On startup the initial language is resolved in priority order (getInitialLanguage in index.ts):

  1. the ?locale= or ?lang= query parameter (for embeds — consistent with the existing ?theme= / ?maponly params),
  2. the language persisted in desktop settings (language field),
  3. the browser's preferred languages (navigator.languages),
  4. the default, en.

Only languages that ship a catalog are honored; anything else falls through to the next rule. The in-app selector (the Settings menu → Language, or the Layout tab of the Settings dialog) changes the language live and records the choice so it survives reloads.

The ?locale parameter makes embeds language-aware, e.g.:

https://geolibre.app/?maponly&locale=es

Using translations in components

import { useTranslation } from "react-i18next";

function Example() {
  const { t } = useTranslation();
  return <button aria-label={t("common.cancel")}>{t("common.save")}</button>;
}
  • Interpolation: t("settings.env.removeAria", { name }) against a catalog value like "Remove {{name}}".
  • Pluralization: define key_one / key_other and call t("settings.env.variablesCount", { count }).
  • Rich text (links, bold): use the <Trans> component with named components, e.g. a catalog value of "… at <tokenLink>share.geolibre.app/settings</tokenLink>." rendered with <Trans i18nKey="…" components={{ tokenLink: <a href="…" /> }} />.
  • Module-scope constants can't call t() (no hook in scope). Store a stable catalog key on the constant instead of the English string and resolve it with t(item.labelKey) at the render site. See SECTION_ITEMS in SettingsDialog.tsx and the command/menu arrays in TopToolbar.tsx.

Do not translate: developer logs (console.*), URLs, store IDs, file-format names/extensions, or persisted data values.

Adding a new language

  1. Copy apps/geolibre-desktop/src/i18n/locales/en.json to locales/<code>.json (e.g. fr.json, pt-BR.json) and translate the values. You may delete keys you haven't translated yet — they fall back to English.
  2. Use your language's CLDR plural categories for plural keys: keep only the forms your language has (e.g. Indonesian/Japanese/Korean/Chinese drop _one and keep _other; Russian adds _few/_many). The catalog test compares plural-normalized keys, so a locale may carry more or fewer plural forms than en without failing.
  3. If the code isn't already in LANGUAGE_NAMES (languages.ts), add an entry so the selector shows a friendly native name. (Optional — it falls back to the raw code.)
  4. That's it. The language appears in Settings → Language (and the Layout tab of the Settings dialog) and works with ?locale=<code>.

Keeping formatting locale-aware

Number, date, and coordinate formatting should continue to use the runtime locale via Intl APIs (e.g. new Intl.NumberFormat(undefined, …) / Intl.DateTimeFormat) rather than hard-coded formatting, so values render in the user's regional conventions.