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.jsonis the source of truth. Every key used byt()must exist here;i18next.d.tstypes thet()function against it, so a missing or misspelled key is a compile error (npm run typecheck).- Catalogs are auto-discovered.
index.tseagerly imports everylocales/*.jsonviaimport.meta.glob, so addinglocales/fr.jsonis 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
enat runtime (fallbackLng: "en").
Language detection¶
On startup the initial language is resolved in priority order
(getInitialLanguage in index.ts):
- the
?locale=or?lang=query parameter (for embeds — consistent with the existing?theme=/?maponlyparams), - the language persisted in desktop settings (
languagefield), - the browser's preferred languages (
navigator.languages), - 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_otherand callt("settings.env.variablesCount", { count }). - Rich text (links, bold): use the
<Trans>component with namedcomponents, 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 witht(item.labelKey)at the render site. SeeSECTION_ITEMSinSettingsDialog.tsxand the command/menu arrays inTopToolbar.tsx.
Do not translate: developer logs (console.*), URLs, store IDs, file-format
names/extensions, or persisted data values.
Adding a new language¶
- Copy
apps/geolibre-desktop/src/i18n/locales/en.jsontolocales/<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. - Use your language's CLDR plural categories for plural keys: keep only the
forms your language has (e.g. Indonesian/Japanese/Korean/Chinese drop
_oneand keep_other; Russian adds_few/_many). The catalog test compares plural-normalized keys, so a locale may carry more or fewer plural forms thanenwithout failing. - 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.) - 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.