Translations

fbb ships translated user-facing strings via Python's standard gettext framework. Source language is English; non-English locales live as compiled .mo files inside the installed package.

For end-users: see README.md for FBB_LANG and --lang usage. This document is for translators and maintainers.

Currently shipped

Locale Status File
en source (no .mo needed)
zh_CN complete src/hs_fbb_cli/locales/zh_CN/LC_MESSAGES/hs_fbb_cli.po

Workflow

All commands run from the repo root.

1. After adding / changing a translatable string in the source

Strings to translate must be wrapped with _() (from hs_fbb_cli._i18n), using .format(...) for variable interpolation — not f-strings:

from hs_fbb_cli._i18n import _

print(_("Building target: {target}").format(target=t))   # right
print(_(f"Building target: {t}"))                         # WRONG — f-string is
                                                          # interpolated before
                                                          # gettext, breaks lookup

Re-extract the source template (.pot) after editing strings:

./scripts/i18n.sh extract        # rebuild src/hs_fbb_cli/locales/hs_fbb_cli.pot

2. Merge new strings into each language's .po

./scripts/i18n.sh update         # merge .pot into each zh_CN/...po

pybabel update preserves existing translations and marks newly added strings as fuzzy so translators can find them.

3. Translate

Open src/hs_fbb_cli/locales/<lang>/LC_MESSAGES/hs_fbb_cli.po in an editor and fill in each msgstr "". Conventions:

  • Quote balance: the .po format requires balanced "" per line. Use multi-line strings for long entries (see existing examples).
  • Variable placeholders: keep {name} placeholders intact and in the same order as the msgid.
  • Newlines: keep \n exactly as in the source. Don't add or remove trailing newlines.
  • Backticks: `like this` are CLI commands; don't translate the literal command name inside the backticks.

After translating, remove any #, fuzzy comments above the entries you finished.

4. Compile and verify

./scripts/i18n.sh compile         # .po -> .mo (runtime binary)

Test locally:

FBB_LANG=zh_CN fbb --help          # POSIX
fbb --lang zh_CN doctor            # CLI flag override

On Windows the console may render Chinese incorrectly unless UTF-8 mode is enabled. Set:

chcp 65001
$env:PYTHONUTF8 = '1'
$env:PYTHONIOENCODING = 'utf-8'

(This is a Windows console quirk, not an fbb bug — Linux / macOS terminals render correctly out of the box.)

Adding a new language

  1. Append the locale code to _KNOWN_LANGS in src/hs_fbb_cli/_i18n.py.
  2. Append it to SUPPORTED_LANGS in scripts/i18n.sh.
  3. ./scripts/i18n.sh update — pybabel will init the new .po.
  4. Translate. Compile. Commit .po + .mo.

The _normalize() function in _i18n.py maps common aliases (zh-CN, zh_CN.UTF-8, zh, etc.) onto the canonical code; add a mapping there if your new locale has unusual aliases.

Locale priority chain

How fbb picks the active locale at startup (high → low):

# Source Notes
1 --lang LANG CLI flag one-shot override
2 FBB_LANG env var fbb-private override
3 LC_ALL / LC_MESSAGES / LANG POSIX standard
4 Windows UI language GetUserDefaultUILanguage
5 en fallback

Implemented in src/hs_fbb_cli/_i18n.py.

Scope choices (why some strings are NOT translated)

  • CLI command names (build, flash, etc.) — programmatic interface; stable English forever.
  • <chip>.json field names, error code enums (DEVICE_NOT_RESPONDING, PORT_BUSY) — machine contract; skills branch on these literal values.
  • fbb describe column labels (install dir, venv python, …) — kept English to preserve fixed-width column alignment with CJK widths.
  • fbb env shell snippets (export PATH=...) — shell syntax that must execute, not user text.
  • fbb setup internal [OK]/[WARN]/[INFO] lines — visual status via prefix; translating each line is high effort for low value. Only the section headers and final summary are translated.

When in doubt, run ./scripts/i18n.sh extract and inspect the diff in hs_fbb_cli.pot — if a string showed up that shouldn't be translated, remove the _() wrapper in the source.

argparse's own strings (help chrome + usage errors)

argparse generates its section titles (positional arguments, options), the usage: prefix, the -h/-V help lines, and every exit-2 usage error (invalid choice, unrecognized arguments, …) through its own module-level gettext binding, which uses the process-global text domain we never populate. Left alone, those stay English even under zh_CN.

Two pieces make them localize:

  1. _i18n._install_into_argparse() repoints argparse._ / argparse.ngettext at our translation functions. Because ours read the live translation on every call, argparse follows --lang / FBB_LANG with no re-patching. Blast radius is this process only — the SDK's build.py runs as a subprocess with its own untouched argparse.
  2. _argparse_strings.py lists each argparse msgid in a _() call so pybabel extract keeps them in the .pot/.po. The module is never imported — it exists purely as an extraction anchor. (Without it, pybabel update would mark these strings obsolete since they have no other call site.)

⚠️ Version caveat. argparse's msgids change across CPython releases (e.g. ≤3.9 used optional arguments; 3.10+ uses options). The strings in _argparse_strings.py and the .po are copied verbatim from the argparse we ship against. If you bump the bundled Python, re-copy the msgids from the new argparse.py and re-translate. tests/test_i18n.py is the alarm: it asserts the zh_CN chrome is Chinese and the English source does not leak — a msgid mismatch turns it red.

When you change anything here, run tests/test_i18n.py (uv run --extra test pytest tests/test_i18n.py).

Why gettext (and not Fluent / custom dict / etc.)

Option Why we picked / didn't
gettext + Babel ✅ Python stdlib + 30+ year industry standard; PO/MO tooling mature; translators already know the format.
Fluent Newer, richer plural rules; thin Python ecosystem; over-engineered for a CLI.
Custom dict 0 deps but loses extraction / merge / validation tooling — long-term maintenance burden.