website: the language switch becomes a globe like the dashboard's (Magyar / English list, current marked, no JS); site.css v3; gate 13 checks the globe from one LANGS list; 5 decoys
gates / gates (push) Successful in 3m54s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
2026-10-08 09:13:23 +02:00
parent c7aade4079
commit 803af4798d
26 changed files with 227 additions and 53 deletions
+10
View File
@@ -1,3 +1,13 @@
## gates — the language globe (site_gates.py gates 3 and 13; 2026-10-08)
- One language list, `LANGS = [("hu", "Magyar"), ("en", "English")]`, with `GLOBE_LABEL`. Gate 3 normalises the
whole `<details class="lang-globe">` (its targets and current mark differ per page). Gate 13 `check_globe`: exactly
one globe; the label in the page's language; one `<a>` per `LANGS` entry in order with `lang`/`hreflang` equal to it;
only the page's own language is `current`/`aria-current`; on twin pages each item points at that language's page
(the page itself or its twin). NO_TWIN pages get the structure checks. Any leftover `class="lang-switch"` fails.
- Decoys (each with its own failure line): `site/globe-wrong-page`, `globe-two-current`, `globe-missing-language`,
`globe-wrong-order`, `leftover-text-switch`; `site/switch-wrong-twin` retired with the switch.
## gates — the website's two languages (site_gates.py gates 3, 13–18; 2026-10-08)
- `site_gates.py`: pages are registered as `TWINS` (Hungarian page → English twin) or by name in `NO_TWIN`
+60 -15
View File
@@ -8,7 +8,7 @@ Gates (all must pass; non-zero exit on any failure):
Windows grep false-negatives multibyte emoji, proven in D0)
3. nav — the <nav>…</nav> and <footer>…</footer> blocks are identical PER LANGUAGE SET
(Hungarian pages against index.html, English pages against en/index.html) after
stripping the active-link marker and the language switch's target
stripping the active-link marker and the language globe (gate 13 checks it)
4. analytics — the umami snippet is present on every public page (nonpublic draft exempt)
5. no-CDN — zero fonts.googleapis.com / fonts.gstatic.com references
6. banned — zero legacy hexes / 999px radius / box-shadow in pages + site.css
@@ -16,7 +16,9 @@ Gates (all must pass; non-zero exit on any failure):
8. cachebust — every site.css / icons.svg reference carries ?v=<N>
12. download — the two download pages name the same installer file and checksum (R-559)
13. twins — every public Hungarian page has its English twin and back; each pair carries the same
hu/en/x-default alternates; each page's language switch points at its own twin
hu/en/x-default alternates; each page's language GLOBE (one per page, every page) lists LANGS in
order, marks only the page's own language current, and points the other items at the twins;
no leftover text switch
14. lang — <html lang="hu"> on the Hungarian set, lang="en" on the English set
15. same apps — the app logos on /alkalmazasok and /en/apps are the same set
16. no HU — no Hungarian in the English pages: accent scan PLUS an ASCII-folded, word-bounded stem
@@ -87,15 +89,21 @@ def is_emoji(ch):
])
SWITCH_RE = re.compile(r'<a href="([^"]*)" lang="(hu|en)" hreflang="(hu|en)" class="lang-switch">([^<]*)</a>')
# The language globe (2026-10-08 ruling, 10-localisation.md §10.7): one <details> per page, its list built from THIS
# list — a third language is one more entry here plus its pages, and nothing else in the switch.
LANGS = [("hu", "Magyar"), ("en", "English")]
GLOBE_LABEL = {"hu": "Nyelv", "en": "Language"}
GLOBE_RE = re.compile(r'<details class="lang-globe">.*?</details>', re.S)
GLOBE_ITEM_RE = re.compile(r'<a href="([^"]*)" lang="([^"]*)" hreflang="([^"]*)" class="lang-globe-item( current)?"'
r'( aria-current="true")?>([^<]*)</a>')
def norm_block(b):
b = b.replace(' class="active"', "")
b = re.sub(r'\s*aria-current="[^"]*"', "", b)
# the language switch is the one nav item whose target differs per page (it points at the page's own
# twin) — gate 13 checks that target; here only its presence and wording must match
b = SWITCH_RE.sub(lambda m: '<a lang-switch="%s">%s</a>' % (m.group(2), m.group(4)), b)
# the globe is the one nav item whose content differs per page (its targets, its current mark, its label) —
# gate 13 checks all of that; here only its presence must match
b = GLOBE_RE.sub('<details class="lang-globe"/>', b)
return b
@@ -225,7 +233,43 @@ for p, s in pages.items():
if not m.group(2).startswith("?v="):
fail("%s: %s referenced without ?v= (got %r)" % (p, m.group(1), m.group(0)))
# gate 13: twins — both ways, matching alternates, the switch at the twin
# gate 13: twins — both ways, matching alternates, the globe pointing at the twins
def lang_of(p):
return "en" if p in EN_PAGES else "hu"
def check_globe(p, lang, urls=None):
"""Exactly one globe; its label in the page's language; one item per LANGS entry, in order, lang/hreflang equal to
the entry; the page's own language is the only current/aria-current item; with `urls`, each item's target."""
s = pages[p]
g = GLOBE_RE.findall(s)
if len(g) != 1:
fail("%s: %d language globes in the page — exactly one, in the nav" % (p, len(g)))
return
lab = re.search(r'<summary class="lang-globe-btn" aria-label="([^"]*)" title="([^"]*)">', g[0])
if not lab or lab.group(1) != GLOBE_LABEL[lang] or lab.group(2) != GLOBE_LABEL[lang]:
fail("%s: the globe's label is %r — on a %s page it is %r" % (p, lab and lab.groups(), lang, GLOBE_LABEL[lang]))
items = GLOBE_ITEM_RE.findall(g[0])
if len(re.findall(r"<a ", g[0])) != len(items):
fail("%s: the globe holds a link the gate cannot read — every item is <a href lang hreflang class=lang-globe-item>" % p)
got = [(it[1], it[5]) for it in items]
if got != LANGS:
fail("%s: the globe lists %r — every page lists exactly %r, in that order" % (p, got, LANGS))
return
for href, code, hreflang, cur, aria, name in items:
if hreflang != code:
fail("%s: the globe's %s item has hreflang=%r" % (p, code, hreflang))
is_cur = code == lang
if bool(cur) != is_cur or bool(aria) != is_cur:
fail("%s: the globe marks %r as current (class current=%s, aria-current=%s) — only the page's own language %r is"
% (p, code, bool(cur), bool(aria), lang))
if urls is not None and href != urls[code]:
fail("%s: the globe's %s item points at %r — %s" % (p, code, href,
"the page itself is %r" % urls[code] if is_cur else "its twin is %r" % urls[code]))
ALT_RE = re.compile(r'<link rel="alternate" hreflang="([^"]+)" href="([^"]+)">')
for hu, en in TWINS.items():
if hu not in pages or en not in pages:
@@ -236,16 +280,17 @@ for hu, en in TWINS.items():
got = dict(ALT_RE.findall(pages[p]))
if got != want:
fail("%s: hreflang alternates %s — the pair %s <-> %s needs exactly %s" % (p, got, hu, en, want))
sw = SWITCH_RE.findall(pages[p])
if len(sw) != 1:
fail("%s: %d language switch links in the page — exactly one, in the nav" % (p, len(sw)))
elif sw[0][0] != URL[other] or sw[0][1] != lang or sw[0][2] != lang:
fail("%s: the language switch points at %r (lang=%s) — its twin is %r (lang=%s)"
% (p, sw[0][0], sw[0][1], URL[other], lang))
check_globe(p, lang_of(p), {"hu": URL[hu], "en": URL[en]})
canon = re.search(r'<link rel="canonical" href="([^"]+)">', pages[p])
if not canon or canon.group(1) != SITE + URL[p]:
fail("%s: canonical %r — it must point at the page itself (%s)" % (p, canon and canon.group(1), SITE + URL[p]))
print(" twins: %d pairs checked both ways (hreflang, switch, canonical)" % len(TWINS))
print(" twins: %d pairs checked both ways (hreflang, globe, canonical)" % len(TWINS))
for p in sorted(NO_TWIN): # no twin: the globe's structure, current mark and label still hold
if p in pages:
check_globe(p, lang_of(p))
for p, s in pages.items():
if 'class="lang-switch"' in s:
fail("%s: a leftover text language switch (class=\"lang-switch\") — the globe replaced it" % p)
# gate 14: the lang attribute
for p, s in pages.items():
@@ -398,4 +443,4 @@ if fails:
print("\nSITE GATES FAILED: %d problem(s)" % len(fails))
sys.exit(1)
print("site gates OK — BOM, emoji=0, nav/footer per language, analytics, no CDN, no legacy tokens, no <style>, "
"cache-busted assets, twins + hreflang + switch, lang, same apps, no Hungarian in English, FAQ JSON-LD, clean tail")
"cache-busted assets, twins + hreflang + globe, lang, same apps, no Hungarian in English, FAQ JSON-LD, clean tail")
+18 -4
View File
@@ -514,10 +514,24 @@ decoy("site/hreflang-wrong", "site_gates.py",
replace_in(_EN("faq.html"), '<link rel="alternate" hreflang="en" href="https://felhom.eu/en/faq">',
'<link rel="alternate" hreflang="en" href="https://felhom.eu/en/apps">'),
must="en/faq.html: hreflang alternates")
decoy("site/switch-wrong-twin", "site_gates.py",
replace_in(_EN("apps.html"), '<a href="/alkalmazasok" lang="hu" hreflang="hu" class="lang-switch">',
'<a href="/" lang="hu" hreflang="hu" class="lang-switch">'),
must="en/apps.html: the language switch points at")
# the language globe (2026-10-08): a wrong target, two current items, a missing language, the wrong order, a leftover text switch
_GHU = '<a href="/alkalmazasok" lang="hu" hreflang="hu" class="lang-globe-item">Magyar</a>'
_GEN = '<a href="/en/apps" lang="en" hreflang="en" class="lang-globe-item current" aria-current="true">English</a>'
decoy("site/globe-wrong-page", "site_gates.py",
replace_in(_EN("apps.html"), _GHU, _GHU.replace('href="/alkalmazasok"', 'href="/"')),
must="en/apps.html: the globe's hu item points at '/' — its twin is '/alkalmazasok'")
decoy("site/globe-two-current", "site_gates.py",
replace_in(_EN("apps.html"), _GHU, _GHU.replace('class="lang-globe-item">', 'class="lang-globe-item current" aria-current="true">')),
must="en/apps.html: the globe marks 'hu' as current")
decoy("site/globe-missing-language", "site_gates.py",
replace_in(_EN("apps.html"), "<li>" + _GHU + "</li>", ""),
must="en/apps.html: the globe lists [('en', 'English')]")
decoy("site/globe-wrong-order", "site_gates.py",
replace_in(_EN("apps.html"), "<li>" + _GHU + "</li><li>" + _GEN + "</li>", "<li>" + _GEN + "</li><li>" + _GHU + "</li>"),
must="en/apps.html: the globe lists [('en', 'English'), ('hu', 'Magyar')]")
decoy("site/leftover-text-switch", "site_gates.py",
replace_in(_EN("apps.html"), '<li class="lang-globe-li">', '<li><a href="/alkalmazasok" lang="hu" hreflang="hu" class="lang-switch">Magyar</a></li><li class="lang-globe-li">'),
must='en/apps.html: a leftover text language switch')
decoy("site/lang-attr", "site_gates.py",
replace_in(_EN("backups.html"), '<html lang="en">', '<html lang="hu">'),
must="the English set needs lang='en'")