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
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:
@@ -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
@@ -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")
|
||||
|
||||
@@ -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'")
|
||||
|
||||
Reference in New Issue
Block a user