Mesh-Client is designed to be accessible to mesh users worldwide. The application currently supports 16 languages and uses static translation bundles to ensure full functionality even when offline.
The following languages are currently supported:
- English (Source of Truth)
- Spanish (Español)
- Ukrainian (Українська)
- German (Deutsch)
- Chinese (Simplified) (中文)
- Portuguese (Brazilian) (Português do Brasil)
- French (Français)
- Italian (Italiano)
- Polish (Polski)
- Czech (Čeština)
- Japanese (日本語)
- Russian (Русский)
- Dutch (Nederlands)
- Korean (한국어)
- Turkish (Türkçe)
- Indonesian (Bahasa Indonesia)
To change the interface language:
- Locate the globe icon in the application header.
- Click the icon to open the language selection dropdown.
- Select your preferred language.
The application will immediately update the UI strings. Your language preference is saved to your local settings and will persist across app restarts.
Most translations in Mesh-Client are initially machine-generated using MyMemory. We rely on community contributions to improve translation quality and accuracy.
If you find a mistranslation or an awkward phrasing:
- Go to the Mesh-Client Issues page.
- Open a new Translation Error issue.
- Provide the current text and your suggested correction.
If you would like to help us add support for a new language:
- Check existing issues to see if someone is already working on it.
- Open a Feature Request specifically for the new language.
- We will help you set up the initial locale files and guide you through the translation process.
Translations are bundled as static JSON files within the application. Unlike many web apps, Mesh-Client does not make network calls to fetch translations at runtime. This ensures that the interface remains in your preferred language even when you are operating off-grid or in environments with no internet access.
For contributors changing UI text. English (src/renderer/locales/en/translation.json) is the source of truth; every other locale is filled from it.
Add the key to en/translation.json and use t('your.key') in components. pnpm run check:i18n enforces that every call site resolves to an English key, and fails on unused English keys (a key with no static t() call, registered dynamic prefix, quoted literal in src/, or tabs.* entry from TAB_SLOT_IDS).
Delete the key from en/translation.json, then run pnpm run i18n:prune-unused -- --write to drop it from every locale (or remove it by hand). check:i18n blocks orphaned English keys.
pnpm run i18n:auto-translate uses MyMemory by default, or LibreTranslate when LIBRETRANSLATE_URL is set. Existing translations are never overwritten.
| Mode | How to run | What it fills |
|---|---|---|
| Default (pre-commit) | pnpm run i18n:auto-translate |
Keys that are new in English vs HEAD and still missing from a locale |
| Backfill | pnpm run i18n:auto-translate --all or I18N_TRANSLATE_ALL=1 |
Every key missing from a locale |
| Audit | add --audit or I18N_AUDIT=1 |
Also retranslates keys whose locale value is still identical to English |
| Limit to a key subtree | add --prefix a.b,c.d (or --prefix=a.b) |
Only keys equal to, or nested under, a prefix (modulePanel matches modulePanel.foo, not modulePanelExtra) |
--audit skips values that are legitimately identical (brands / protocol loanwords via hasTranslatableContent), technical samples (MACs, hex digests, path/URI placeholders — not token pairs like WiFi/HTTP), and keep-English prefixes (radio region/modem/OLED codes, theme palette names, MeshCore network presets, routing-port identifiers). Locale JSON is written by a subprocess that stages beside the target file then renames, so a partial write cannot truncate an existing locale.
MyMemory sends the contact info@coloradomesh.org by default for its 50k words/day quota; override with MYMEMORY_EMAIL.
pnpm run check:i18n hard-fails on missing or unused English keys. It warns (but does not fail) on incomplete locale coverage, so translation rate limits don't block commits. pnpm run check:i18n:branch skips the unused-key pass and only checks keys new or changed vs HEAD.
CI does not run check:i18n as a standalone workflow step. Quality rules run via pre-commit and indirectly in CI through Vitest (locale-quality.test.ts runs it as a subprocess).
scripts/check-i18n-quality.mjs enforces more than missing keys. Notable rule families:
- Mojibake — garbled encodings are rejected.
- Dash placeholders — any English value that is a single dash (such as
common.emDashorsignalMeter.noData) must match exactly. - Boot-sequence transport labels and RRC slash-command tokens — kept verbatim; RRC room wording must avoid false friends.
- Reticulum hub/stack (
connectionPanel.reticulumInterfaces.*,reticulumStack*,reticulumPeers.*) — “hub”, “stack”, “peer”, and “host” must not become unrelated words (pressure, colleague, chimney stack, etc.). - TX/RX Texas — radio
TX/RXmust not become the US state (Teksas, Техас, 德克萨斯) on any key. - URI / token spacing —
tcp://,Wi-Fi, andI2Pmust stay contiguous (no CATtcp :///Wi - Fi/I 2 P). - Flood / advert / room / backbone — gated by English meaning, not a handful of leaf names (routing Flood, mesh advert, MeshCore/RRC room, network backbone — not water, ads, hotels, or spines).
- Routing-port identifiers —
NodeInfo,Telemetry,NeighborInfo,DiscoveryFlood,RoomAdvertmust match English verbatim (matched on the English value, not camelCase leaf names). - gamesPanel — resign is forfeit (not quitting a job); draw is a tie (not a sketch/lottery); challenge/threefold are chess terms.
- Repeaters CLI danger confirm (
repeatersPanel.cliDangerConfirmAction) — confirm button must be translated and must not read like “delete” or other false friends. repeatersPanel.cliMultiHopHint— must describe multi-hop CLI and automatic Ping before the first command, not multi-tab UI wording.
Full rule set: scripts/check-i18n-quality.mjs and scripts/check-i18n-quality.test.mjs.
- Framework: i18next + react-i18next; static JSON bundles loaded at startup;
fallbackLng: 'en'. - Locale files:
src/renderer/locales/{en,es,uk,de,zh,pt-BR,fr,it,pl,cs,ja,ru,nl,ko,tr,id}/translation.json— English is source of truth (pnpm run check:i18nreports key count). - Locale persistence:
localekey inapp_settingsSQLite table (canonical) andmesh-client:appSettingslocalStorage (fast startup read); reconciled inApp.tsxon mount. - Reduce motion / 24-hour time:
reduceMotionanduse24HourTimeshare the sameapp_settings/ localStorage bundle (App → Appearance,AppPanel.tsx;timeFormatStore,formatDisplayTime). Behavior is documented in accessibility-checklist.md. - Unused keys:
scripts/i18n-unused-keys.mjsimplements unused-key detection. - Language selector:
src/renderer/components/LanguageSelector.tsx— globe-icon dropdown in the header; callsi18n.changeLanguage()+mergeAppSetting('locale', ...)+electronAPI.appSettings.set('locale', ...).