Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ build/
.artifacts/
# Built by tools/build_site_artifacts.py from the source repositories. Codeplugs
# are disposable artifacts, so CI regenerates them rather than serving a commit.
site/codeplugs/
site/codeplugs/
site/*/codeplugs/
18 changes: 17 additions & 1 deletion site/codeplug-sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,23 @@
"",
"Which file formats a profile produces is NOT listed here. It comes from",
"the radio's row in site/radios.json, so a radio cannot advertise an",
"export column in the support table and then fail to offer the file."
"export column in the support table and then fail to offer the file.",
"",
"'i18n' holds per-language overrides of 'name' and 'description' for the",
"translated downloads pages; a language without an entry falls back to",
"the English text."
],
"sources": [
{
"id": "use-cases",
"name": "Use-case starter codeplugs",
"description": "Buy a supported radio and make it behave like the radio you wanted: a GMRS handheld, a PMR446 handheld, a MURS handheld, a marine VHF receiver, or all of them at once. Built from published ssrf-lite data only, with no personal DMR IDs. GMRS needs an FCC licence and the marine channels are receive-only; see the reference sheet in each download.",
"i18n": {
"es": {
"name": "Codeplugs iniciales por caso de uso",
"description": "Compra una radio compatible y haz que se comporte como la radio que querías: un portátil GMRS, un portátil PMR446, un portátil MURS, un receptor VHF marino, o todos a la vez. Compilados solo con datos publicados de ssrf-lite, sin IDs DMR personales. GMRS requiere una licencia de la FCC y los canales marinos son solo de recepción; consulta la hoja de referencia en cada descarga."
}
},
"repo": "https://github.com/Chicago-Offline/codeplugger",
"profiles": [
"examples/profiles/baofeng_bf888s/gmrs.yml",
Expand All @@ -40,6 +50,12 @@
"id": "chioff-shared",
"name": "Chicago Offline community net",
"description": "Shared GMRS and MURS channels for the Chicago Offline community net. Instance-agnostic: no personal DMR IDs, no per-operator settings.",
"i18n": {
"es": {
"name": "Red comunitaria de Chicago Offline",
"description": "Canales GMRS y MURS compartidos de la red comunitaria de Chicago Offline. Independientes de la instancia: sin IDs DMR personales, sin ajustes por operador."
}
},
"repo": "https://github.com/Chicago-Offline/chioff-codeplugger-profiles-shared",
"checkout": "chioff-codeplugger-profiles-shared",
"ssrf_roots": [
Expand Down
592 changes: 592 additions & 0 deletions site/es/index.html

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Build validated radio codeplugs from shared RF data and versioned profiles. Export files or program selected supported radios directly." />
<link rel="canonical" href="https://codeplugger.chicagooffline.com/" />
<link rel="alternate" hreflang="en" href="https://codeplugger.chicagooffline.com/" />
<link rel="alternate" hreflang="es" href="https://codeplugger.chicagooffline.com/es/" />
<link rel="alternate" hreflang="x-default" href="https://codeplugger.chicagooffline.com/" />
<meta property="og:type" content="website" />
<meta property="og:title" content="codeplugger — a codeplug compiler for radio fleets" />
<meta property="og:description" content="Build validated, radio-neutral codeplugs from SSRF data and profiles. Export files or program selected radios directly." />
Expand All @@ -27,6 +30,7 @@
<a href="#ecosystem">Ecosystem</a>
<a href="#start">Quick start</a>
</nav>
<a class="lang" href="es/" lang="es" hreflang="es">Español</a>
<a class="repo-link" href="https://github.com/Chicago-Offline/codeplugger" target="_blank" rel="noopener">GitHub ↗</a>
</div>
</header>
Expand Down
3 changes: 3 additions & 0 deletions site/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ header nav a {
header nav a:hover { background: var(--bg-hover); color: var(--text); text-decoration: none; }
.repo-link { margin-left: auto; color: var(--text-dim); text-decoration: none; white-space: nowrap; }
.repo-link:hover { color: var(--accent); }
.lang { margin-left: auto; color: var(--text-dim); text-decoration: none; white-space: nowrap; font-size: 0.92rem; }
.lang:hover { color: var(--accent); }
.lang + .repo-link { margin-left: 0; }

/* hero */
.hero { padding: clamp(3rem, 8vw, 6rem) 0 clamp(2.5rem, 6vw, 4rem); }
Expand Down
74 changes: 74 additions & 0 deletions tests/test_site_codeplugs.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,77 @@ def test_every_configured_radio_is_publishable():
(REPO_ROOT / "radios" / radio_id / "capabilities.json").read_text()
)
assert build.formats_for_radio(radio_id, rows[radio_id], caps)


def test_source_translations_only_override_known_fields():
for source in build.load_sources():
for lang, text in source.get("i18n", {}).items():
assert lang in build.STRINGS, f"{source['id']}: no page for {lang!r}"
assert set(text) <= {"name", "description"}, f"{source['id']}: {lang}"


def test_every_language_has_every_format_label():
suffixes = {s for _fmt, s in build.SOURCE_FORMATS.values()}
suffixes |= {".benlink.json", ".html", ".resolved.json"}
for lang, s in build.STRINGS.items():
assert set(s["formats"]) >= suffixes, lang


_MANIFEST = {
"generated_at": "2026-01-01 00:00 UTC",
"sources": [
{
"id": "shared",
"name": "Shared net",
"description": "English blurb.",
"i18n": {"es": {"name": "Red compartida", "description": "Texto en español."}},
"repo": "https://example.invalid/profiles",
"revisions": {"profiles": "abcdef1234567890"},
"profiles": [
{
"profile_id": "first",
"profile_name": "First",
"radio": "radio_a",
"radio_name": "Radio A",
"dir": "shared/radio_a/first",
"zones": 1,
"channels": 2,
"resolved_sha256": "0" * 64,
"files": [{"name": "first.csv", "sha256": "f" * 64, "bytes": 1}],
}
],
}
],
}


def test_english_page_links_files_beside_it():
page = build.render_page(_MANIFEST, "en")
assert '<html lang="en">' in page
assert 'href="shared/radio_a/first/first.csv"' in page
assert 'href="shared/radio_a/first/SHA256SUMS"' in page
assert "1 zone · 2 channels" in page
assert "Shared net" in page and "Red compartida" not in page
assert 'href="../es/codeplugs/"' in page
assert 'hreflang="x-default" href="https://codeplugger.chicagooffline.com/codeplugs/"' in page


def test_spanish_page_links_back_into_the_english_files():
"""Only one copy of the artifacts is published; es/ links across to it."""
page = build.render_page(_MANIFEST, "es")
assert '<html lang="es">' in page
assert 'href="../../codeplugs/shared/radio_a/first/first.csv"' in page
assert 'href="../../codeplugs/shared/radio_a/first/SHA256SUMS"' in page
assert 'href="../../style.css"' in page
assert "1 zona · 2 canales" in page
assert "Red compartida" in page and "Texto en español." in page
assert "English blurb." not in page
assert 'href="../../codeplugs/"' in page # language switch
assert "Importar en CHIRP" in page


def test_language_without_source_translation_falls_back_to_english():
manifest = json.loads(json.dumps(_MANIFEST))
manifest["sources"][0].pop("i18n")
page = build.render_page(manifest, "es")
assert "Shared net" in page and "English blurb." in page
19 changes: 19 additions & 0 deletions tests/test_site_radio_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,25 @@ def test_committed_index_html_is_generated():
)


@pytest.mark.parametrize("lang", sorted(gen.PAGES))
def test_every_language_page_is_generated(lang):
"""Each translated landing page carries a current table in its language."""
path = gen.PAGES[lang]
current = path.read_text()
assert current == gen.render_index(current, lang=lang), (
f"{path.relative_to(REPO_ROOT)} is stale -- run: python tools/gen_radio_table.py"
)
assert f'<html lang="{lang}">' in current
assert gen.STRINGS[lang]["hw"] in current


def test_spanish_table_has_no_english_generator_strings():
tbody = gen.build_tbody(lang="es")
assert "Not supported" not in tbody
assert "Verified on a physical radio" not in tbody
assert gen.STRINGS["es"]["not_supported"] in tbody


def test_every_radio_has_a_site_row():
caps = gen.load_capabilities()
listed = {row["id"] for row in gen.load_rows()}
Expand Down
Loading
Loading