Adding a New Language¶
A step-by-step playbook for bootstrapping a locale from zero to a fully
translated, fully navigable manual — written for whoever (human or agent)
does the next one. Every step below was actually run, in this order, for
de, fr, es, it, pt-BR, and zh; the gotchas called out are real
failures hit while doing it, not hypotheticals.
Checklist¶
Work through in order; each item links to the section with the actual commands and the gotchas hit doing it for real. Don't skip straight to step 4 — steps 1 and 3 are cheap and avoid rework later.
- [ ] 1 — Confirm Ethos ships a UI in this language, and pick a locale code
mkdocs-materialactually has a template for (not necessarily the code FrSky's own tooling uses internally —pbvspt-BRbit us here). - [ ] 2 — Add the locale to
mkdocs.yml(nonav_translationsyet). - [ ] 3 — Seed a ~30-term glossary in
scripts/translate.py'sGLOSSARIES. - [ ] 4 — Run
scripts/translate.py --only <code>(dry-run first); confirm0 failed. - [ ] 5 — Check the old
ethos-manualrepo for an already-captured screenshot set before assuming the simulator pipeline is needed; bulk-copy and spot-check visually if one matches. - [ ] 6 — Run
python scripts/check_anchors.py --fix. - [ ] 7 —
mkdocs build --strictand check$?is0(not just that the output looks clean);check_anchors.pyreports 0. - [ ] 8 — Once page coverage is complete, add
nav_translations(leaf labels from each page's own H1, section tabs from the glossary). - [ ] 9 — Commit, push, watch the Action, verify live (allow for CDN propagation lag on brand-new paths).
1. Confirm the locale code before touching anything¶
Two separate things need to agree, and getting either wrong is annoying to unwind later (URLs bake the code in permanently):
- Does Ethos actually ship a UI in this language? A manual in a
language the firmware doesn't support is confusing, not useful. FrSky's
own Ethos Suite desktop app ships an
i18n/*.jsonfile per supported language — installed locally it's atProgram Files/Ethos Suite/i18n/. That list (cs,de,en,es,fr,he,it,nl,no,pb,sk,zh-CNat last check) is a reliable proxy for what Ethos itself supports. - Does
mkdocs-materialship a language-switcher template for that code? This is a different list, and the two don't always agree — Ethos Suite's own folder is literally namedpb, but Material has nopartials/languages/pb.html, onlypt-BR.html. Usingpbbuilds fine right up untilmkdocs build's post-build sitemap step, where it crashes withjinja2.exceptions.TemplateNotFound— and that crash does not contain the word "error" or "warning", so grepping build output for those (a totally reasonable thing to do) will report a clean build that actually exited non-zero. Always check$?aftermkdocs build --strict, not just its printed output. To see the exact codes Material supports:
import material
from pathlib import Path
p = Path(material.__file__).parent / "templates" / "partials" / "languages"
print(sorted(x.stem for x in p.glob("*.html")))
2. Add the locale to mkdocs.yml¶
languages:
- locale: <code>
name: <native display name>
build: true
No nav_translations yet — that's step 6, after there's real content to
match labels against.
3. Seed a glossary in scripts/translate.py¶
Add a GLOSSARIES["<code>"] entry (see the existing fr/de/es/it
entries for the term list to cover — flight-surface names, mix/output/
timer/trim vocabulary, switches, sensors, etc.). This is what keeps
terminology consistent from the very first translated page instead of
drifting page to page. ~30 terms is enough; it's a floor to build on, not
a complete dictionary.
If the console errors with UnicodeEncodeError partway through a run —
this hit zh specifically — it's because Windows' console defaults to
cp1252, which can't encode non-Latin scripts. Already fixed at the top
of the script (sys.stdout.reconfigure(encoding="utf-8", ...)); if it
resurfaces, that's where to look.
4. Translate¶
python scripts/translate.py --only <code> --dry-run # confirm scope/cost first
python scripts/translate.py --only <code> --yes
Independent locales can run in parallel (separate background
processes) — they only read shared files (docs/en/, mkdocs.yml) and
write to entirely separate docs/<code>/ trees, so there's no race
condition. Four locales translating concurrently finished in roughly the
same wall-clock time as one.
Check the log for Done: N translated, 0 failed before moving on.
5. Check for existing screenshots before considering the simulator¶
Don't assume new screenshots require running the simulator pipeline —
check first. The predecessor repo
(ethos-manual, typically
cloned as a sibling directory) may already have a captured, per-language
screenshot set from FrSky's own team sitting unused. It did for German,
French (via the french_LT/ folder — not the smaller, incomplete
french/ one), Italian, and Spanish; it had nothing for Portuguese or
Chinese. Check filename overlap against what this repo currently
references:
from pathlib import Path
old_repo_lang_assets = Path("../ethos-manual/<language-folder>/assets") # sibling checkout
current = {p.name for p in Path("docs/en/assets").iterdir() if p.suffix.lower() == ".png"}
old = {p.name for p in old_repo_lang_assets.glob("*.png")}
print(f"{len(old & current)} / {len(current)} would match")
A high match rate (≥90%, in practice) means it's a straight copy into
docs/<code>/assets/ — fallback_to_default in mkdocs.yml means
that's all that's needed; no markdown changes. Visually spot-check
at least one copied image before trusting the match (open it, confirm
it's genuinely the target language's UI, not a stale/mismatched capture)
— filenames matching doesn't strictly guarantee content matches, even
though it always has so far.
If there's no match (Portuguese, Chinese, or any future language the old repo never covered), the locale correctly falls back to English screenshots automatically. That's the expected, working state — closing the gap for real means porting/running the actual macro pipeline against the simulator (see Screenshot Pipeline), which is out of scope for a text-translation pass and needs a local simulator install.
6. Check and fix anchor links¶
Translating a heading changes its auto-generated slug, which silently
breaks any #that-heading-slug link from another page — and this is
not a build error: mkdocs build --strict does not fail on it, so
nothing will tell you it happened except a dead link a reader clicks.
python scripts/check_anchors.py # report only
python scripts/check_anchors.py --fix # pin every finding, in en + every locale that has the page
This is a real, recurring class of bug, not a one-time cleanup — every
locale added so far surfaced a handful of new instances (the ones that
happened to coincide with a <locale>-specific translated slug diverging
from English, which a different locale's translation didn't). Run it
after every batch of new/updated translations. It rebuilds the site
itself by default (mkdocs build --strict first) so results are never
stale.
7. Verify for real¶
mkdocs build --strict; echo "exit code: $?" # must be 0, not just free of "error"/"warn" text
python scripts/check_anchors.py # must report 0
8. Add nav_translations — once, after page coverage is complete¶
Tab and sidebar labels in nav: don't pick up a locale's translated page
title automatically unless the nav entry has no explicit label at all.
Add nav_translations under the locale's mkdocs.yml entry once (not
before) the locale has full — or near-full — page coverage; translating
the chrome ahead of the content it points to reads oddly. Leaf labels
should be copied verbatim from each translated page's own H1 (so the
sidebar text matches the page heading exactly); section-tab labels
(Home, Getting Started, ...) should match the glossary from step 3.
Extract every H1 programmatically rather than retyping labels by hand —
it's faster and removes any chance of a transcription mismatch:
import re
h1 = re.search(r"^#\s+(.+)$", Path(f"docs/{code}/{rel_path}").read_text(encoding="utf-8"), re.MULTILINE).group(1).strip()
Skip Translation Status — it's a generated, English-only maintainer
page with no translated equivalent in any locale.
9. Ship it¶
Commit, push to main, and watch the Deploy Docs Action run. GitHub
Pages' CDN can 404 a brand-new locale path for the first 15–30+ seconds
after a genuinely successful deploy — that's edge-cache propagation lag,
not a failure. Confirm via the GitHub API that the file exists on
gh-pages before worrying:
gh api "repos/<owner>/<repo>/contents/<version>/<code>/<path>?ref=gh-pages" --jq '.sha, .size'
then retry the live URL with a short backoff.