aethersdr / aethersdr/AetherSDR

[RFC] In-repo multilingual user documentation generator — bridge-driven, 5 languages

Open
#4,496 0 comments 0 reactions 1 assignee View on GitHub

@ten9876 is already working on this.

Since Jul 26, 2026.

documentation enhancement maintainer-review rfc
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:

  1. 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.yml cron is still firing daily into a repository nobody is currently watching.
  2. 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.
  3. The bus factor was always 1, and it has now been realized. The generator (docmesh) is deliberately unpublished — .gitignore excludes /_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.md requires user-facing prose to name a control by its visible label, not its C++ class — "the DIGI applet (class CatApplet)". accessibleName is 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 diff on 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 requires jieba segmentation 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-Hans vs zh-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.

  1. CI detects a new tag.
  2. Rebuild ui-surface.json against the tagged build.
  3. git diff the manifest → set of changed surfaces.
  4. Regenerate only affected English pages; propagate to the four translations.
  5. Open a PR. Merge is a human decision.
  6. mike publishes 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:

  1. 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.
  2. Grounding. Pages are generated from a manifest of real controls plus real handler source, not from model recall about SDR software in general.
  3. PR review. Diffs are per-page and reviewable; a section owner can read 15 changed pages per release.
  4. 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. BandPlanManager exists 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:

  1. Every control named in a page exists in ui-surface.json. Catches invented controls — the highest-consequence failure mode.
  2. Every page satisfies the §5 template — required headings present, in order.
  3. Every Related link resolves.
  4. 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.
  5. Every AppSettings key referenced exists in the source.

Checks 1 and 4 are the load-bearing ones. Neither requires a model.


12. Open questions for reviewers

  1. 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.
  2. Hosting (decision 0.2) — separate repo, or docs/user/ in-tree?
  3. @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 docmesh or 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.
  4. Does the Spanish tree at /latest/ get a redirect from the existing site, or do we let the two coexist?
  5. zh-Hant — is Traditional Chinese in scope later, and does that change the zh-Hans nav structure now?
  6. Screenshot automation (§8.3) — follow-up RFC, or fold into stage 6?
  7. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.