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_LANGand--langusage. 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
.poformat 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 themsgid. - Newlines: keep
\nexactly 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
- Append the locale code to
_KNOWN_LANGSinsrc/hs_fbb_cli/_i18n.py. - Append it to
SUPPORTED_LANGSinscripts/i18n.sh. ./scripts/i18n.sh update— pybabel willinitthe new.po.- 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>.jsonfield names, error code enums (DEVICE_NOT_RESPONDING,PORT_BUSY) — machine contract; skills branch on these literal values.fbb describecolumn labels (install dir,venv python, …) — kept English to preserve fixed-width column alignment with CJK widths.fbb envshell snippets (export PATH=...) — shell syntax that must execute, not user text.fbb setupinternal[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:
_i18n._install_into_argparse()repointsargparse._/argparse.ngettextat our translation functions. Because ours read the live translation on every call, argparse follows--lang/FBB_LANGwith no re-patching. Blast radius is this process only — the SDK'sbuild.pyruns as a subprocess with its own untouched argparse._argparse_strings.pylists each argparse msgid in a_()call sopybabel extractkeeps them in the.pot/.po. The module is never imported — it exists purely as an extraction anchor. (Without it,pybabel updatewould 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+ usesoptions). The strings in_argparse_strings.pyand the.poare copied verbatim from the argparse we ship against. If you bump the bundled Python, re-copy the msgids from the newargparse.pyand re-translate.tests/test_i18n.pyis 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. |