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