Marketing/help site for FT8AF — FT8/FT4/FT2 for Android.
The site is generated from templates + per-language message catalogs into static HTML
under public/, localized into 16 languages. English builds to the site root (so existing
URLs are unchanged); every other locale builds under /<locale>/.
node build.mjs # generate public/ (all locales) + sitemap.xml
python dev-server.py 8000 # serve public/ at http://127.0.0.1:8000 (clean URLs)The /store page needs FOURTHWALL_STOREFRONT_TOKEN at build time (public,
read-only token; see .env.example). Without it the site still builds and the
store page falls back to a plain link to the Fourthwall shop.
Open / for English, /es, /ja, /ar (right-to-left), etc. Re-run node build.mjs
after editing any template or catalog.
src/
pages/*.mjs # one template per route; structure/markup only
pages/_registry.mjs# build/sitemap order
partials/… # (nav, footer, head, language switcher live in build.mjs)
i18n/<locale>.json # translatable copy — en.json is the source of truth
data/site.mjs # locale registry (native names, og-locale, dir, font flag)
build.mjs # the generator
public/assets/ # committed static assets (CSS, JS, fonts, screenshots)
public/**/*.html # GENERATED — git-ignored, do not edit by hand
- Edit English in
src/i18n/en.json, then mirror the change into the other catalogs. - Any key missing from a locale catalog falls back to English, so a partial translation still renders a complete page.
- Check coverage with
npm run check:i18n(also runs in CI). It compares every catalog toen.jsonand fails on structural drift — keys a locale has thaten.jsondoesn't, or string-vs-object shape mismatches — while only warning about missing/verbatim strings (those fall back to English). Add--strict(or--min <pct>) to also gate on translation coverage. - Values may contain inline HTML (
<a>,<strong>,<code>); they are emitted raw, so keep them valid HTML fragments. - Internal links are written English-style (
/features) in templates and rewritten to the active locale (/es/features) at build time — don't hard-code locale prefixes.
Brand FT8AF; callsigns (K1AF, N0RC, BG7YOZ, N0BOY); mode names FT8/FT4/FT2; ham jargon
(POTA, DXCC, QSO, CAT, SWR, ALC, PSKReporter, QRZ, Cloudlog, Wavelog, "73"); prices; the
app screenshots and the inline UI mockups; shell commands and the GitHub issue template.
middleware.js is Vercel Routing Middleware that runs only on /, before the
cache:
- If the visitor has a
localecookie (set client-side inpublic/assets/ft8af.jswhenever they view a page or use the picker), it's honored. - Otherwise their
Accept-Languageis matched to the closest built locale. - English is served in place at
/; a non-English match gets a307to/<locale>.
Deep links (/features, /es, …) are never redirected. The redirect is per-visitor
(Cache-Control: no-store, Vary: Accept-Language, Cookie), so the static / and
/<locale> pages stay fully cacheable. The picker always wins — choosing English at
/ sets locale=en and stops the redirect — so there's an escape hatch from any
auto-detected language. The middleware derives its locale list from
src/data/site.mjs, so there's nothing to keep in sync.
Vercel runs node build.mjs (buildCommand in vercel.json) and serves public/.
The Vercel project's Root Directory is the repo root (not public/) so the build
command runs at the root. middleware.js deploys automatically.
CI (.github/workflows/ci.yml) builds first, then validates JSON, links and HTML5.