aethersdr / aethersdr/AetherSDR
[RFC] In-repo multilingual user documentation generator — bridge-driven, 5 languages
@ten9876 is already working on this.
Since Jul 26, 2026.
- Dominant language
- C++
- Stars
- 221
- Forks
- 117
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 299
Description
0. Open decisions (need maintainer sign-off)
| # | Decision | Recommendation |
|---|---|---|
| 0.1 | Language set — see §4.1 | Ship the 5 specified; revisit after §12.1 |
| 0.2 | Hosting: in-repo docs/user/ vs. a separate AetherSDRDocs repo |
Separate repo — keeps a 5-language, ~3M-token tree out of every git clone of the app |
| 0.3 | Reach out to LU5DX before or after landing this | Before — courtesy, and he may want to hand over docmesh |
| 0.4 | Generation model + spend ceiling per release | See §8.2 |
1. Problem
AetherSDR has no user documentation in this repository. Everything under docs/ is developer-facing — RFCs, design records, CI, commit signing, accessibility, protocol implementation notes. README.md covers highlights, hardware, download, and building from source. Nothing explains how to operate the application.
That gap has been filled since v0.8.19 by an external, independently maintained manual — AetherSDRDocsEnglish by @LU5DX, with a parallel Spanish mirror. It is genuinely good: 560 pages, 5,487 indexed sections, ~800K tokens, ~58 feature folders, plus Reference and Troubleshooting.
Three things have now converged into a real risk:
- It is stale and diverging daily. Last update 2026-06-15. The published manual documents 26.6.3; we shipped 26.7.4. That is roughly a version and a half and 400+ commits of drift, and the repo's
notify-on-new-version.ymlcron is still firing daily into a repository nobody is currently watching. - We cannot adopt it. Neither docs repo carries a license. Default is all-rights-reserved: we may not vendor, index, ship, or redistribute that corpus, and an assistant built on it (see §2.4) would inherit the problem.
- The bus factor was always 1, and it has now been realized. The generator (
docmesh) is deliberately unpublished —.gitignoreexcludes/_generator/as "Internal authoring tooling — not for the published repo". When the maintainer is unavailable, both the manual and the Spanish translation stop together, and there is no path for anyone else to run a regeneration.
This is not a criticism of a volunteer who built something excellent and gave it away. It is an argument that user documentation is load-bearing enough to belong in the project's own supply chain, generated from our own source, under our own license, regenerable by anyone with commit access.
1.1 Why now rather than later
Documentation debt compounds silently. Every release widens the gap between what the manual describes and what ships, and every widening makes a future regeneration less of a diff and more of a rewrite. We are already past the point where a human could reasonably reconcile 400 commits against 560 pages by hand.
2. Background: what already exists (the load-bearing facts)
2.1 We have a machine-readable UI surface — and it is better than source parsing
This is the fact the whole proposal rests on. The Agent Automation Bridge (AETHER_AUTOMATION=1, docs/automation-bridge.md) already exposes the entire live UI:
| Verb | Gives us |
|---|---|
dumpTree |
objectName, class, accessibleName, toolTip, enabled, visible, geometry, value, range, combo items[] + currentIndex, checkable + checked, keying |
menu |
every menu action, invocable while closed |
get |
live model state — radio, transmit, slice, pan, equalizer, dsp, … |
dss snapshot, floors, streams |
subsystem counters |
dumpTree's shape is already the | Control | Kind | Behavior | table a user manual needs, and it beats static source parsing on three counts:
- Ground truth. It reports what actually renders, not what the code appears to construct. Conditional widgets, runtime-populated combos, and platform-specific controls are all correct by construction.
- On-screen labels for free.
AGENTS.mdrequires user-facing prose to name a control by its visible label, not its C++ class — "the DIGI applet (classCatApplet)".accessibleNameis that label. A source-parsing generator has to infer it; we can read it. - Cannot silently drift. Regenerate against a running build and the docs are structurally incapable of describing a control that no longer exists (see the §11 conformance check).
Every other candidate input is enumerable too: AppSettings keys give us the Reference section, resources/bandplans/ gives region-aware band data, and CHANGELOG.md gives per-release "what's new".
2.2 We have a model-artifact delivery precedent
scripts/setup/setup-deepfilter.sh fetches per-platform artifacts from a GitHub release and verifies them against pinned SHA256s via the shared scripts/setup/_verify_sha256.sh. If any stage of this pipeline needs a downloadable artifact, the pattern is established and reviewed.
2.3 The proven publishing stack
The external manual demonstrates a stack that works for exactly this content shape, and we should not re-litigate it: MkDocs Material + mike (per-version sites — /26.6.3/, /latest/) + awesome-pages (per-directory nav ordering, so a new folder merges without touching a central nav: block).
2.4 Relationship to the in-app assistant
A separate thread of work proposes an in-app assistant. This RFC is a prerequisite for it, and is independently justified without it. An assistant is a retrieval layer over a corpus; today we own no corpus we are allowed to index. Landing this produces one — correctly licensed, structured, per-release versioned, and multilingual. Whether an assistant is ever built is out of scope here.
2.5 The app is English-only
There is no QTranslator, no .ts/.qm, and no Qt6::LinguistTools anywhere in the build. The UI is English and will remain so unless a separate RFC changes that. This is not a limitation for this proposal — it is a hard constraint that determines the translation contract in §4.2, and getting it wrong would make translated documentation actively unusable.
3. Proposed architecture
A two-stage pipeline. The separation is the important part.
┌──────────────── STAGE 1: EXTRACT (deterministic) ────────────────┐
AetherSDR ──────► │ tools/dump_ui_surface.py │
(AETHER_AUTOMATION│ • launch app, walk every menu + dialog via `menu` │
=1, TX gated off)│ • dumpTree each surface │
│ • enumerate AppSettings keys, band plans, menu actions │
└──────────────────────────────┬───────────────────────────────────┘
▼
docs/user/ui-surface.json ◄── COMMITTED, diffable, reviewable
│
┌──────────────── STAGE 2: GENERATE (model) ───────────────────────┐
│ tools/gen_user_docs.py │
│ • diff manifest vs. last release → changed surfaces only │
│ • per page: manifest slice + source context + CHANGELOG │
│ • emit English page against the §5 template contract │
│ • translate → zh-Hans, hi, es, fr (§4) │
└──────────────────────────────┬───────────────────────────────────┘
▼
MkDocs tree ──► mike ──► GitHub Pages
3.1 Why the manifest is a committed artifact
Stage 1 is deterministic; stage 2 is not. Committing ui-surface.json between them buys us disproportionately much:
- Diff-driven regeneration.
git diffon the manifest tells us exactly which surfaces changed between releases, so we regenerate ~15% of pages per release rather than 100%. This is the difference between a sustainable process and one that gets switched off after two releases on cost. - The nondeterminism is fenced into one stage. Everything before it is reproducible and reviewable in a PR like any other change.
- Conformance is mechanical. §11.2 — a generated page may only name controls that appear in the manifest. That check needs no model.
- It is independently useful. A committed UI-surface manifest is a good input for accessibility auditing (
tools/check_a11y.py), UI regression detection, and any future assistant.
3.2 What stage 2 sees per page
| Input | Source |
|---|---|
| Control inventory for the surface | manifest slice |
| Handler behavior (what the button does) | source context — the MainWindow_*.cpp / applet TU that implements it |
| Constants and limits | source (e.g. "displays up to 2000 lines") |
| Related settings keys | AppSettings enumeration |
| What changed this release | CHANGELOG.md entry |
| Style + naming rules | AGENTS.md §"User-facing names match the on-screen UI labels" |
4. Language strategy
4.1 Target set
Five languages, selected as the most-spoken worldwide by total speakers:
| Locale | Language | ~Total speakers |
|---|---|---|
en |
English | 1.5B |
zh-Hans |
Mandarin Chinese (Simplified) | 1.1B |
hi |
Hindi | 610M |
es |
Spanish | 560M |
fr |
French | 310M |
See §12.1 — this set optimizes for global reach. The top five by licensed amateur population is a materially different list (Japan, Germany, Italy, Brazil, Russia all rank above Hindi and French among hams). Both are defensible; the choice is recorded here as deliberate and is open for reviewer input.
Spanish is on the list independently of ranking: an existing Spanish manual has been serving those users for months, and dropping it silently would be a regression.
4.2 The translation contract — prose translates, identifiers do not
Because the application UI is English-only (§2.5), every translated page must preserve control labels, menu paths, settings keys, and file paths verbatim in English.
A Hindi-speaking operator reading Hindi prose that says click Enable All can find that button. If we translate the label, they cannot. This is the single most important rule in the pipeline and it is enforced mechanically, not by prompt alone (§11.2).
| Translate | Never translate |
|---|---|
| Explanatory prose, steps, tips | Control labels (Enable All, Master volume) |
| Section headings | Menu paths (Help → Support) |
Table Behavior column |
AppSettings keys (DisplayNoiseFloorEnable) |
| Preconditions | File paths, CLI flags, env vars |
| Protocol terms with no accepted translation (VITA-49, SmartLink, DAX) |
If the app is ever localized, this contract inverts and the pipeline must follow the app's locale. That is a known future coupling, not a problem today.
4.3 Termbase
Amateur-radio vocabulary must translate consistently across 560 pages, and general-purpose translation will not do that reliably ("slice", "panadapter", "waterfall", "squelch", "break-in", "split"). Ship docs/user/termbase/<locale>.json — a reviewed, pinned glossary injected into every translation call. Community review of the termbase is the highest-leverage contribution a non-developer operator can make, and it is a far smaller ask than reviewing 560 pages.
4.4 English is the source language
The four non-English trees are translations of the generated English pages, not independent generations from the manifest. This keeps structure identical across locales (so mike versioning, nav, and cross-links line up), makes review tractable, and roughly halves cost versus generating each language from scratch.
4.5 Known per-language technical issues
zh-Hans— MkDocs Material search requiresjiebasegmentation for Chinese; the default separator will silently produce a near-useless search index. Must be configured and verified, not assumed.hi— Devanagari rendering needs a font stack that does not fall back to tofu on Windows; verify in CI screenshots.- RTL — none of the five are right-to-left. Arabic (the most likely sixth) is, and MkDocs Material's RTL support should be validated before committing to it.
zh-Hansvszh-Hant— Simplified only for v1; Traditional is a separate locale, not a variant, and is deferred (§12.5).
5. The page contract
One folder per applet/dialog; one page per task, not per control. This is the information architecture the external manual validated over 560 pages, and it is the right one — users arrive with a goal ("enable noise reduction"), not a control name.
Task page:
# <Imperative task title>
## Before you start ← preconditions: hardware, connection state, prerequisites
## Steps ← numbered, each step naming a real control
## What each control does ← | Control | Kind | Behavior | — generated from the manifest
## Tips ← non-obvious behavior, limits, gotchas
## Related ← sibling pages in this folder
Section index (overview.md) — one per folder: # <Section> overview → ## How it works → ## Tips → ## Related.
Every heading is fixed. The template is a contract the generator must satisfy and the conformance check validates (§11.2).
5.1 Tree
docs/user/<locale>/
├── getting-started/ install, first connect, first QSO, core concepts
├── features/ one folder per applet/dialog (~58 today)
├── operating/ cross-cutting guides — digital modes, remote, contesting
├── reference/ menu actions, AppSettings keys, band plans
└── troubleshooting/ symptom-first, cross-linked to features/
6. Incremental regeneration
Full regeneration is the exception, not the release process.
- CI detects a new tag.
- Rebuild
ui-surface.jsonagainst the tagged build. git diffthe manifest → set of changed surfaces.- Regenerate only affected English pages; propagate to the four translations.
- Open a PR. Merge is a human decision.
mikepublishes the new version;/latest/moves.
A .docs-version stamp records the last processed tag. Generated documentation lands via PR and is never auto-merged — Principle IX: model output does not reach main without passing the gate.
7. Hosting
Recommend a separate aethersdr/AetherSDRDocs repository (decision 0.2). Five languages × ~800K tokens plus screenshots is a large tree that would otherwise sit in every git clone of the application, and the docs release cadence is not the app's. The generator (tools/) and the manifest (docs/user/ui-surface.json) stay in this repo, because both are derived from and validated against this source.
8. The hard problems (honest cost)
8.1 Generated documentation is a claim, not a fact
This is the real risk and it deserves to be stated plainly. Principle VIII exists because "frontier models produce fluent, confident, plausible fix descriptions that turn out wrong at a rate that makes unreviewed AI-generated commits dangerous." Generated documentation has exactly that failure mode, aimed at users instead of the codebase — and users have no way to verify it.
Mitigations, in order of strength:
- Mechanical conformance (§11.2) — every control named must exist in the manifest. Catches the most common and most damaging failure: a confidently described control that does not exist.
- Grounding. Pages are generated from a manifest of real controls plus real handler source, not from model recall about SDR software in general.
- PR review. Diffs are per-page and reviewable; a section owner can read 15 changed pages per release.
- Scope discipline. Where a wrong answer has consequences beyond confusion — band edges, power limits, transmit configuration — pages must link to authoritative sources rather than restating limits.
BandPlanManagerexists precisely because these differ across IARU regions 1/2/3, and a confidently wrong band-edge statement is a licensing problem for the operator, not a docs bug. This is a hard rule, not a preference.
8.2 Cost
Rough order of magnitude, to be verified with count_tokens before implementation rather than trusted:
| Output tokens | Notes | |
|---|---|---|
| Full English generation | ~800K | 560 pages × ~1.4K |
| 4 translations | ~3.1M | comparable length per language |
| Full 5-language regen | ~3.9M | tens of dollars at current Sonnet-tier rates |
| Typical incremental release | ~15% of the above | single-digit dollars |
This is a maintainer build-time cost, not a user runtime cost — an important distinction if this is later confused with the in-app assistant question. Input is heavily cacheable (the manifest and style rules are a stable prefix), which materially reduces the real figure.
8.3 Screenshots
The external manual uses them; they are the highest-maintenance element of any UI manual. The bridge's grab <widget> can capture any widget — including a correct GPU-framebuffer readback of SpectrumWidget — so screenshots can be regenerated automatically. Deferred to a follow-up. Getting text right for five languages is enough scope for one RFC.
8.4 Translation review
We will be publishing Hindi and Mandarin documentation that no current maintainer can read. That is a real and unavoidable exposure. The termbase (§4.3) narrows it to a reviewable artifact, and per-locale community reviewers are the durable answer. The RFC should not pretend this is solved. Recommend shipping non-English locales with a visible "community-reviewed translation — report issues here" banner until a reviewer exists for that locale.
9. Alternatives considered
| Alternative | Why not |
|---|---|
| Wait for @LU5DX to return | Divergence compounds daily; the licensing and bus-factor problems remain even if he does. Not mutually exclusive — see §12.3. |
| Fork the existing docs | Unlicensed. All-rights-reserved by default. Not available to us. |
| Hand-write the manual | 560 pages × 5 languages, re-verified every release. Not sustainable at this cadence. |
Parse the C++ source directly (what docmesh appears to do) |
Works, but strictly worse: infers on-screen labels instead of reading them, misses runtime-populated controls, and drifts silently. We have a live bridge; a source parser would be choosing the weaker input. |
| English only, translate later | Retrofitting the translation contract (§4.2) after 560 English pages exist means re-auditing every page for label handling. Cheaper to design in now. |
| Ship docs inside the app | Orthogonal. The generated tree can be bundled later; hosting is a separate decision. |
10. Staged plan
Each stage lands independently and leaves the tree in a working state.
| Stage | Deliverable | Gate |
|---|---|---|
| 1 | tools/dump_ui_surface.py + committed ui-surface.json. No model involved. |
Manifest covers ≥95% of applets/dialogs; stable across two consecutive runs |
| 2 | tools/gen_user_docs.py — English, one section only (proposed: Support & Diagnostics, 7 pages) |
Maintainer reads all 7 and signs off on the template |
| 3 | Full English tree + MkDocs Material + mike + awesome-pages | Site builds; conformance check green |
| 4 | Translation layer + termbase, Spanish first | Native-speaker review of ≥20 sampled pages |
| 5 | zh-Hans, hi, fr |
Per-locale search verified (§4.5); banner per §8.4 |
| 6 | CI: regenerate on release tag, open PR | Two consecutive releases documented with no manual intervention |
Stage 1 is valuable on its own and is the recommended commitment point. If the project decides against generated documentation entirely, a committed UI-surface manifest still pays for itself in accessibility auditing and UI regression detection.
11. Constitution compliance
| Principle | How this complies |
|---|---|
| IV — Clean-room | Generated from our own GPLv3 source via our own tooling. The unlicensed external corpus is not adopted, vendored, or used as generation input. Information architecture is a functional choice; section headings are ours. |
| VI — Transmit on intent | Extraction drives the app. It runs without AETHER_AUTOMATION_ALLOW_TX, so the bridge refuses every transmit-keying control. CI asserts txAllowed=false before extraction begins. |
| VII — Boundary validation | ui-surface.json is machine-generated input to a generator; schema-validated on read. |
| VIII — Evidence over assertion | §11.2 below. Generated prose is a claim until the conformance check passes. |
| IX — Surface only what survives | Generated docs land via PR, never auto-merged. |
| XIV — Persist atomically | Manifest and generated tree are written to temp and renamed into place; a failed run never leaves a half-written manifest. |
AGENTS.md naming rule |
Satisfied by construction — accessibleName is the on-screen label. |
11.2 The conformance check (tools/check_user_docs.py)
Runs in CI, blocking:
- Every control named in a page exists in
ui-surface.json. Catches invented controls — the highest-consequence failure mode. - Every page satisfies the §5 template — required headings present, in order.
- Every
Relatedlink resolves. - Translated pages preserve identifiers verbatim (§4.2) — control labels, menu paths, settings keys, and file paths must appear byte-identical to the English source. This is the check that makes translated docs usable rather than decorative.
- Every
AppSettingskey referenced exists in the source.
Checks 1 and 4 are the load-bearing ones. Neither requires a model.
12. Open questions for reviewers
- Language set. Specified as the five most-spoken worldwide. By licensed amateur population the list would skew toward Japanese, German, Italian, Portuguese, and Russian. Reach vs. audience fit — worth a deliberate answer, and it is cheap to change before stage 4 and expensive after.
- Hosting (decision 0.2) — separate repo, or
docs/user/in-tree? - @LU5DX. Should we reach out before landing this? He built something excellent, the project benefited from it for many releases, and he may want to hand over
docmeshor co-maintain. Recommend yes, as courtesy and not as a dependency — this RFC must not block on a reply. Regardless of outcome, his contribution warrants acknowledgement in the generated site. - Does the Spanish tree at
/latest/get a redirect from the existing site, or do we let the two coexist? zh-Hant— is Traditional Chinese in scope later, and does that change thezh-Hansnav structure now?- Screenshot automation (§8.3) — follow-up RFC, or fold into stage 6?
- Is a future in-app assistant an accepted goal? It does not change this design, but it would raise the priority of the machine-readable manifest and argue for a chunk-stable page structure.
Drafted with Claude Code against the repository at 3288dbc0. External-repository facts (structure, workflows, .gitignore, page template, scale) were read from the public LU5DX/AetherSDRDocsEnglish repository and published site to infer the approach; no content, prose, or tooling from it is proposed for adoption.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.