Translating Trobar
Trobar ships five apps — server, Android, desktop, Garmin, and the Home Assistant integration — each with its own i18n system, currently in English (EN) and French (FR). There's no shared toolchain, so adding a language means doing it once per app. This page walks through each. Pick the app(s) you want to translate and open a PR against that app's repo.
Every app already gates translation completeness in CI, so the checklists below double as what a reviewer will actually check.
Server (trobar-server) — Flask-Babel / gettext
Source strings live in Python (_("...")) and Jinja templates
({% trans %}), extracted into app/translations/messages.pot.
- Initialize a new catalog (first time for that language) or update an
existing one — run from inside
app/, not the repo root: extracting from the root prefixes every#:path comment in the.pot/.pofiles withapp/.cd app pybabel extract -F babel.cfg -o translations/messages.pot . pybabel init -i translations/messages.pot -d translations -l <lang> # or, updating an existing catalog after new strings were added: pybabel update -i translations/messages.pot -d translations -l <lang>pybabel extractrewrites the.pot's header from its own template wherever it runs, dropping theSPDX-FileCopyrightTextandSPDX-License-Identifierlines, andreuse lintthen fails. Put the header back as it was before committing (git diffon the.potshows it).pybabel updateleaves each.po's header alone. It can also mark a new string fuzzy with a guessed translation from a similar one; check every#, fuzzyentry and translate it properly. - Translate every entry in
app/translations/<lang>/LC_MESSAGES/messages.po. - Compile it:
pybabel compile -d app/translations. - Register the language in
app/main.py:_get_locale()checks the language code in two places (the cookie-value check and thebest_match([...])fallback), andset_language()'sif lang not in (...)guard is a third — all three need the new code added. - Add the language to the Preferences language-picker links in
app/templates/index.html(search forset_language).
CI gate: dev/check_translations.py (run via dev/verify.sh) fails the
build on any untranslated or fuzzy .po entry, or on a placeholder
({name}, %(x)s) that doesn't match the English source — a PR must be
translation-complete to pass.
Android (trobar-android) — Android string resources
- Add
app/src/main/res/values-<lang>/strings.xml, translating every string that exists inapp/src/main/res/values/strings.xml. - Add the language to the in-app picker: the
ChoiceRowoptions list inMainActivity.kt(search for"fr" to "Français").
CI gate: lint's MissingTranslation / ExtraTranslation checks (see
app/build.gradle.kts) fail the build if values-<lang>/ doesn't have an
exact 1:1 key match with values/ — nothing missing, nothing left over.
Desktop (trobar-desktop) — Flutter ARB
- Add
lib/l10n/app_<lang>.arb, translating every entry inlib/l10n/app_en.arb(the template file, perl10n.yaml). - Run
flutter gen-l10nto regeneratelib/l10n/gen/app_localizations.dart— this is also where an untranslated entry surfaces, vialib/l10n/untranslated.txt. - Add the language code to
languageValuesinlib/app_prefs.dartand to the picker inlib/settings_screen.dart(search forFrançais).supportedLocalesinmain.dartis generated from the ARB files automatically — no separate registration there.
Garmin (trobar-garmin) — Connect IQ resources
- Add
resources-<lang>/strings.xml, translating every string from the baseresources/strings.xml. - Declare the language in
manifest.xml's<iq:languages>block — note Connect IQ uses 3-letter codes here (eng,fre), not the 2-letter codes the other three apps use. - Wire the resource path explicitly in
monkey.jungle:base.lang.<code> = $(base.lang.<code>);resources-<lang>
⚠️ This step is the trap. A language declared in manifest.xml with
no resource path wired in monkey.jungle compiles green and lints
clean — and silently ships English strings on-device
. The SDK's own default resource discovery may already
cover common language codes, but there's no build-time signal that tells
you whether it actually found your resources-<lang>/ folder or silently
fell back to English. Wiring the path explicitly costs nothing if the
default already had it covered, and is the actual fix if it didn't.
4. Verify it renders, not just that it builds: run the simulator in the
new locale and confirm the strings actually show translated. A green
build is not proof of a working translation for this app.
Home Assistant integration (trobar-ha) — plain JSON, not gettext
This one genuinely works differently from the other four, worth stating
plainly rather than leaving a reader to assume it follows the gettext
pattern above. Home Assistant's own translation model: no .po/.pot, no
pybabel, no compile step. Strings live in plain JSON, keyed by structure
rather than extracted from source — config.step.user.data.url,
entity.sensor.pending_tracks.name, and so on.
custom_components/trobar/strings.jsonis the source of truth.custom_components/trobar/translations/en.jsonis its English copy.-
Every other language is its own file, e.g.
translations/fr.json— mirroringstrings.jsonkey-for-key, nothing more, nothing less. Core Home Assistant integrations get non-English translations generated by Lokalise; a custom integration like this one just commits the files by hand. -
Copy
translations/en.jsontotranslations/<lang>.json. - Translate every value, keeping every key exactly as it is.
- Check wording that names a specific navigation path (e.g. where the
integration token is created) against the server's own translation
for that path in
app/translations/<lang>/LC_MESSAGES/messages.po, rather than translating the English fresh — the two need to describe the same click-path in the same words.
CI gate: tests/test_translations.py flattens strings.json and every
translations/*.json into key sets and asserts they're identical, plus that
every value is a non-empty string — the JSON equivalent of
dev/check_translations.py's job above, since none of the gettext-era
tooling on this page applies to this app.
Submitting
Open a PR against the relevant repo (each of the five is translated
independently — you don't need to touch all of them). See each repo's
CONTRIBUTING.md for the general PR process.